Hi Ashish,

I Just had a look at https://nightlies.apache.org/ofbiz/trunk/rat-output.html

Actually IMO there is more than gradle.lockfile.
I don't think a LICENSE file there is necessary .It's the only plugin with it, since there is already one LICENSE file at plugins root. Same for framework an applis. For README.md there is no Apache license in top. It's not mandatory since it's in rat-excludes.txt (tools repo). Some README.md have an Apache license inside.
Though I added this linethat in rat-excludes.txt in 2017, I wonder now if we 
should not remove this line and add the Apache license inside all README.md.

What do you (and people at large) think?

TIA

Jacques

Le 10/07/2026 à 15:46, [email protected] a écrit :
This is an automated email from the ASF dual-hosted git repository.

ashishvijaywargiya pushed a commit to branch trunk
in repositoryhttps://gitbox.apache.org/repos/asf/ofbiz-plugins.git


The following commit(s) were added to refs/heads/trunk by this push:
      new fe1a266d6 Add devreload plugin inside plugins directory (#316)
fe1a266d6 is described below

commit fe1a266d60425b3b73469f185cb68bc27012495d
Author: Ashish Vijaywargiya<[email protected]>
AuthorDate: Fri Jul 10 19:16:19 2026 +0530

     Add devreload plugin inside plugins directory (#316)
devreload plugin will give us option to work in Java files(events and
     services) and services.xml files without restarting ofbiz.
---
  devreload/LICENSE                                  | 201 +++++
  devreload/README.md                                |  56 ++
  devreload/build.gradle                             | 187 +++++
  devreload/gradle.lockfile                          |   4 +
  devreload/ofbiz-component.xml                      |  30 +
  .../apache/ofbiz/devreload/DevReloadContainer.java | 860 +++++++++++++++++++++
  .../org/apache/ofbiz/devreload/HotSwapAgent.java   | 109 +++
  7 files changed, 1447 insertions(+)

diff --git a/devreload/LICENSE b/devreload/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/devreload/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/devreload/README.md b/devreload/README.md
new file mode 100644
index 000000000..246d9495c
--- /dev/null
+++ b/devreload/README.md
@@ -0,0 +1,56 @@
+# devreload
+
+Development-only hot-reload for OFBiz. Edit a Java service/event method, a
+`services.xml` file, or add a brand-new method, and the change is live in 
under a
+second — no restart, ever.
+
+The plugin is entirely self-contained: dropping this directory into a checkout 
(or
+removing it) has zero effect on the rest of OFBiz either way.
+
+## Requirements
+
+This plugin needs a **DCEVM (Dynamic Code Evolution Virtual Machine)** JVM to 
run.
+Set it up once, before running `./gradlew ofbizDev --no-watch-fs`:
+
+1. Get a DCEVM-patched JVM. Easiest source: JetBrains Runtime (JBR), bundled 
with
+   IntelliJ IDEA under `<IDE install>/jbr` (`.../Contents/jbr` on macOS). 
Standalone
+   DCEVM builds work too.
+2. Point at it — set the `DCEVM_HOME` env var (e.g. in your shell profile) so 
every
+   future run picks it up automatically, or pass `-PdcevmHome=/path/to/jvm` 
each time
+   instead.
+
+Add this to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) so it's always 
set:
+
+```
+export DCEVM_HOME="/Applications/IntelliJ IDEA.app/Contents/jbr"
+```
+
+(adjust the path to wherever your DCEVM-patched JVM lives — on macOS this is 
IntelliJ's
+bundled JetBrains Runtime by default). Reload your shell (or run `source 
~/.zshrc`) and
+every future `./gradlew ofbizDev --no-watch-fs` picks it up automatically, 
with nothing
+else to set.
+
+## Usage
+
+```
+./gradlew ofbizDev --no-watch-fs
+```
+
+This is the only supported command — always run it exactly like this. It boots 
OFBiz
+and everything hot-swaps live, no restart: method-body edits, `services.xml` 
changes,
+and structural changes (new/removed methods or fields, changed signatures) 
alike.
+
+`--no-watch-fs` disables Gradle's own file-system watching, which otherwise 
competes
+with this plugin's `WatchService` for the same macOS per-process 
directory-watch
+ceiling on a full checkout — without it, some directories can silently go 
unwatched.
+It's harmless to include even on a smaller/scoped checkout, so it's part of 
the one
+command to remember.
+
+If DCEVM isn't set up, it fails immediately — before compiling or booting 
anything —
+with the same setup steps as above.
+
+Scope to specific components for a faster startup:
+
+```
+./gradlew ofbizDev --no-watch-fs -Photreload.components=devreload,party
+```
diff --git a/devreload/build.gradle b/devreload/build.gradle
new file mode 100644
index 000000000..148fcb902
--- /dev/null
+++ b/devreload/build.gradle
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Optional -Photreload.components=compA,compB, forwarded as
+// -Dofbiz.hotreload.components to DevReloadContainer, which then only watches 
those
+// components' servicedef/source directories instead of every component in the 
checkout.
+// See DevReloadContainer's `allowedComponents` javadoc for why: macOS's 
WatchService has
+// a practical per-process ceiling on watched directories well under a typical 
ulimit,
+// and a checkout the size of OFBiz trunk (~50+ components) can exceed it.
+static List<String> hotreloadComponentArgs(Project project) {
+    String components = project.findProperty('hotreload.components')
+    return components ? 
["-Dofbiz.hotreload.components=${components}".toString()] : []
+}
+
+// Optional -Photreload.watchBuildOutput=true, forwarded as
+// -Dofbiz.hotreload.watchBuildOutput to DevReloadContainer. Off by default: 
watching
+// build/classes/java/main only matters for picking up externally-produced 
.class files
+// (e.g. `./gradlew -t classes` running in a second terminal) -- in-process 
compiles hot-swap
+// directly and never need it -- and unlike source/servicedef watching, that 
tree isn't
+// narrowed by -Photreload.components, so on a full checkout it roughly 
doubles the total
+// directories watched for no benefit to the common (in-process-only) workflow.
+static List<String> hotreloadWatchBuildOutputArgs(Project project) {
+    Object watchBuildOutput = 
project.findProperty('hotreload.watchBuildOutput')
+    return watchBuildOutput ? 
["-Dofbiz.hotreload.watchBuildOutput=${watchBuildOutput}".toString()] : []
+}
+
+// DevReloadContainer's own in-process-compile output directory. Deliberately 
separate
+// from Gradle's own build/classes/java/main (see DevReloadContainer's 
hotReloadOutputDir
+// field javadoc for why) and placed ahead of the normal runtime classpath 
below, so a
+// class compiled here but never yet loaded in the running JVM picks up the 
live version
+// on its first load too, not just already-loaded ones. Passed down to the JVM 
as a system
+// property (see commonHotReloadJvmArgs) rather than hardcoded independently 
on the Java
+// side too, so this path exists as a single source of truth.
+static File hotReloadOutputDir(Project project) {
+    return new File(project.rootDir, 'build/devreload/classes')
+}
+
+// Base JVM args for ofbizDev: the hot-reload activation flags, the 
output-directory
+// location, and the optional component/watch scoping args.
+// -XX:+AllowEnhancedClassRedefinition is added separately by the caller, once 
a
+// DCEVM-patched JVM has actually been resolved (required -- see below).
+static List<String> commonHotReloadJvmArgs(Project project) {
+    List<String> args = ['-Dofbiz.hotreload=true', 
'-Djdk.attach.allowAttachSelf=true',
+            
"-Dofbiz.hotreload.outputDir=${hotReloadOutputDir(project).absolutePath}".toString()]
+    args += hotreloadComponentArgs(project)
+    args += hotreloadWatchBuildOutputArgs(project)
+    return args
+}
+
+// Locates a DCEVM-patched `java` executable from an explicit -PdcevmHome=... 
or
+// DCEVM_HOME env var -- deliberately not auto-detected by guessing per-OS IDE 
install
+// paths (IntelliJ's JBR bundles DCEVM and is a convenient source of one, but 
its
+// install location varies too much across OS/Toolbox/version to guess 
reliably). One
+// explicit way in is easier to reason about, document, and keep working than 
several
+// silent path guesses that can each go stale independently. `ofbizDev` below 
requires
+// this to resolve to something -- it fails fast with setup instructions 
otherwise.
+//
+// Two ways to set it once and forget it, so every future plain `./gradlew 
ofbizDev`
+// just works with no flag: export DCEVM_HOME in a shell profile (picked up via
+// System.getenv() below), or add a `dcevmHome=/path/to/jvm` line to
+// `~/.gradle/gradle.properties` (GRADLE_USER_HOME -- picked up via 
project.findProperty()
+// below, same as -PdcevmHome on the command line, just persisted instead of 
retyped).
+// The latter needs the Gradle property name `dcevmHome`, not `DCEVM_HOME` -- 
gradle.properties
+// holds Gradle properties, not env vars, so the two names aren't 
interchangeable there.
+def resolveDcevmJavaExecutable = {
+    String home = project.findProperty('dcevmHome') ?: 
System.getenv('DCEVM_HOME')
+    if (!home) {
+        return null
+    }
+    String exeName = 
org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'java.exe' : 
'java'
+    File exe = new File(home, "bin/${exeName}")
+    if (!exe.exists()) {
+        // macOS JDK/JBR app bundles nest an extra Contents/Home -- e.g. 
IntelliJ's bundled
+        // JetBrains Runtime lives at "<IDE 
install>/Contents/jbr/Contents/Home/bin/java",
+        // one level deeper than the jbr folder itself (which is what 
README/error-message
+        // guidance points DCEVM_HOME/-PdcevmHome at). Fall back to that 
layout before
+        // giving up, so the documented "point at <IDE install>/Contents/jbr" 
guidance
+        // actually resolves on macOS instead of silently finding nothing.
+        exe = new File(home, "Contents/Home/bin/${exeName}")
+    }
+    return exe.exists() ? exe.absolutePath : null
+}
+
+// Convenience task: `./gradlew ofbizDev` starts OFBiz with hot-reload enabled
+// (-Dofbiz.hotreload=true), plus the JVM flag DevReloadContainer's 
self-attaching
+// HotSwapAgent needs (-Djdk.attach.allowAttachSelf=true). Registered on the 
root
+// project, mirroring the classpath/mainClass wiring of the built-in `ofbiz` 
command
+// task, so this plugin needs no change to the root build.gradle to be usable.
+//
+// A DCEVM-patched JVM is required, not optional: stock-JDK hot-reload used to 
be a
+// supported fallback, but a stock JVM's redefineClasses can't hot-swap a 
new/removed
+// method or field anyway (see DevReloadContainer's class-level javadoc), so 
silently
+// running in that degraded mode just defers the surprise to whenever someone 
adds a
+// method. Simple by design: if DCEVM_HOME/-PdcevmHome resolves to a real JVM, 
this task
+// just runs -- no other flag, no mode to pick. If it doesn't resolve, running 
`ofbizDev`
+// fails immediately with setup instructions, before OFBiz attempts to boot -- 
but the
+// failure has to be raised from a doFirst task action, not directly in this
+// configuration block: this block runs whenever the project is evaluated, 
which in this
+// build happens on every `./gradlew <anything>` invocation, not just when 
`ofbizDev`
+// itself is requested (confirmed empirically -- even `./gradlew help` 
realizes this
+// task, most likely because the Develocity plugin applied in settings.gradle 
touches
+// every registered task for build-scan data). Throwing here unconditionally 
would break
+// every Gradle command whenever DCEVM isn't set up, which is exactly the 
"zero effect on
+// anyone not using it" guarantee this plugin promises everywhere else (see 
README).
+//
+// JavaExec's `executable` property is pointed at the resolved DCEVM JVM 
binary during
+// task configuration -- no toolchain is configured anywhere in this project 
for that to
+// conflict with. One non-obvious wrinkle: `executable` has to be set directly 
in the
+// task's configuration block, not from a task action (e.g. inside doFirst). 
Doing it
+// from a task action fails with "Toolchain from executable property does not 
match
+// toolchain from javaLauncher property" -- by the time a task action runs, 
javaLauncher's
+// convention has already been finalized for execution, and overriding 
executable at that
+// point conflicts with it. Setting it during configuration, before that 
convention
+// finalizes, works cleanly. That's also *why* the DCEVM lookup itself still 
has to happen
+// eagerly in this block (only the throw is deferred to doFirst): `executable` 
can't be
+// set any later.
+rootProject.tasks.register('ofbizDev', JavaExec) {
+    group = 'OFBiz Server Commands'
+    description = 'Start OFBiz with hot-reload (requires a DCEVM-patched JVM 
-- see README). Java source, ' +
+            'services.xml, and structural changes (new/removed methods or 
fields, changed signatures) all ' +
+            'reload live with no restart. Point -PdcevmHome=/path/to/jvm or 
set DCEVM_HOME to enable; fails ' +
+            'fast with setup instructions if neither resolves. Optionally 
scope to specific components with ' +
+            '-Photreload.components=compA,compB, or watch 
build/classes/java/main too (for externally-produced ' +
+            '.class files) with -Photreload.watchBuildOutput=true.'
+    classpath = rootProject.files(hotReloadOutputDir(project)) + 
rootProject.sourceSets.main.runtimeClasspath
+    mainClass = rootProject.application.mainClass
+    List<String> jvmArgsList = 
rootProject.application.applicationDefaultJvmArgs + commonHotReloadJvmArgs(project)
+    String dcevmJava = resolveDcevmJavaExecutable()
+    if (dcevmJava) {
+        // Only wired up when actually needed: compiling everything first just 
to fail in
+        // doFirst afterwards would make the "no DCEVM" error slow to reach on 
a cold build.
+        dependsOn rootProject.tasks.named('classes')
+        jvmArgsList = jvmArgsList + ['-XX:+AllowEnhancedClassRedefinition']
+        logger.lifecycle("ofbizDev: DCEVM-patched JVM resolved at 
${dcevmJava}")
+        executable = dcevmJava
+    } else {
+        // Deferred to a task action (doFirst) instead of thrown directly 
here: this
+        // configuration block runs whenever this project is evaluated, which 
in this build
+        // happens for every `./gradlew <anything>` invocation, not just when 
`ofbizDev`
+        // itself is requested (confirmed empirically -- even `./gradlew help` 
realizes this
+        // task's configuration block, most likely due to the Develocity 
plugin applied in
+        // settings.gradle touching every registered task for build-scan 
data). Throwing here
+        // directly would break every Gradle command whenever DCEVM isn't set 
up, violating
+        // this plugin's "zero effect on anyone not using it" guarantee (see 
README). A
+        // doFirst block, by contrast, only actually runs when `ofbizDev` is 
the task Gradle
+        // is executing.
+        String attemptedHome = project.findProperty('dcevmHome') ?: 
System.getenv('DCEVM_HOME')
+        String detail = attemptedHome
+                ? "DCEVM_HOME/-PdcevmHome is set to '${attemptedHome}', but no 
'bin/java' (or macOS " +
+                        "'Contents/Home/bin/java') was found there -- double-check 
the path."
+                : "Neither -PdcevmHome nor the DCEVM_HOME env var is set."
+        doFirst {
+            throw new GradleException(
+                    "ofbizDev requires a DCEVM-patched JVM -- plain stock-JDK 
hot-reload is not supported.\n\n" +
+                    "${detail}\n\n" +
+                    "To fix this:\n" +
+                    "  1. Get a DCEVM-patched JVM. The easiest source is JetBrains 
Runtime (JBR), which " +
+                    "bundles DCEVM and ships with IntelliJ IDEA under '<IDE 
install>/jbr' " +
+                    "('.../Contents/jbr' on macOS). Standalone DCEVM builds work 
too.\n" +
+                    "  2. Point at it -- set the DCEVM_HOME env var once (e.g. in 
your shell profile, so " +
+                    "every future run picks it up automatically) or pass 
-PdcevmHome=/path/to/jvm each " +
+                    "time.\n\n" +
+                    "See plugins/devreload/README.md for the full setup 
guide.")
+        }
+    }
+    jvmArgs(jvmArgsList)
+}
+
+dependencyLocking {
+    lockAllConfigurations()
+}
diff --git a/devreload/gradle.lockfile b/devreload/gradle.lockfile
new file mode 100644
index 000000000..b9b85588a
--- /dev/null
+++ b/devreload/gradle.lockfile
@@ -0,0 +1,4 @@
+# This is a Gradle generated file for dependency locking.
+# Manual edits can break the build and are not advised.
+# This file is expected to be part of source control.
+empty=pluginLibsCompile,pluginLibsCompileOnly,pluginLibsRuntime
diff --git a/devreload/ofbiz-component.xml b/devreload/ofbiz-component.xml
new file mode 100644
index 000000000..c9ecab1fb
--- /dev/null
+++ b/devreload/ofbiz-component.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<ofbiz-component name="devreload" enabled="true"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+        
xsi:noNamespaceSchemaLocation="https://ofbiz.apache.org/dtds/ofbiz-component.xsd";>
+    <resource-loader name="main" type="component"/>
+
+    <!-- Development hot-reload container: a no-op unless started with
+         -Dofbiz.hotreload=true, so it is safe to leave enabled in every
+         environment. See README.md for usage. -->
+    <container name="dev-reload-container" loaders="main" 
class="org.apache.ofbiz.devreload.DevReloadContainer"/>
+</ofbiz-component>
diff --git 
a/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java 
b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
new file mode 100644
index 000000000..1f9539cb2
--- /dev/null
+++ b/devreload/src/main/java/org/apache/ofbiz/devreload/DevReloadContainer.java
@@ -0,0 +1,860 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.devreload;
+
+import java.io.IOException;
+import java.lang.instrument.ClassDefinition;
+import java.lang.instrument.Instrumentation;
+import java.lang.management.ManagementFactory;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.tools.JavaCompiler;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.ofbiz.base.component.ComponentConfig;
+import org.apache.ofbiz.base.config.GenericConfigException;
+import org.apache.ofbiz.base.container.Container;
+import org.apache.ofbiz.base.container.ContainerException;
+import org.apache.ofbiz.base.start.StartupCommand;
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.cache.UtilCache;
+
+/**
+ * Development-only container that watches Java sources/classes and {@code 
services.xml}
+ * files and applies changes to a running OFBiz instance without a restart.
+ *
+ * <h2>Activation</h2>
+ * Add {@code -Dofbiz.hotreload=true -Djdk.attach.allowAttachSelf=true} to 
your JVM
+ * arguments, then start OFBiz normally (or run {@code ./gradlew ofbizDev}, 
provided by
+ * this plugin, which sets both automatically — the only supported command). 
That Gradle
+ * task requires a DCEVM-patched JVM and fails fast with setup instructions if 
none is
+ * resolvable; see "Structural changes" below for why.
+ *
+ * <h2>How Java hot-reload works</h2>
+ * <ol>
+ *   <li>At startup this container self-attaches {@link HotSwapAgent} to the 
current JVM
+ *       via the Attach API, obtaining a live {@link Instrumentation} instance 
— no
+ *       {@code -javaagent} flag needed.</li>
+ *   <li>A {@link WatchService} thread monitors every component's {@code 
src/main/java}
+ *       directory. On save, changed {@code .java} files are compiled 
in-process (a JDK,
+ *       not just a JRE, is required) into {@code build/devreload/classes/} — 
a directory
+ *       private to this plugin, deliberately <em>not</em> Gradle's own
+ *       {@code build/classes/java/main/}. Writing into Gradle's managed 
output would
+ *       leave its incremental-build cache unaware of the change: if a source 
file is
+ *       later reverted to content Gradle already has a snapshot for (e.g. via
+ *       {@code git checkout}) while no hot-reload session is watching, 
Gradle's next
+ *       {@code compileJava} would see matching input and skip recompiling, 
silently
+ *       leaving the stale hot-swapped {@code .class} file in place. This 
plugin's output
+ *       directory is cleared and recreated on every {@link #init}, so a fresh 
run never
+ *       inherits a previous session's bytecode, and is placed ahead of 
Gradle's own
+ *       output on the classpath (see {@code build.gradle}) so it always wins 
when a class
+ *       exists in both places. Running {@code ./gradlew -t classes} 
externally in a
+ *       second terminal still works — the same WatchService also monitors 
Gradle's own
+ *       output directory directly for externally-produced {@code .class} 
files, when
+ *       {@code -Dofbiz.hotreload.watchBuildOutput=true}.</li>
+ *   <li>Changes are debounced for 300 ms so a single compile run is handled 
as one batch.
+ *       Each changed class already loaded in the JVM is updated in place via
+ *       {@link Instrumentation#redefineClasses}, the same mechanism an IDE 
debugger uses
+ *       for HotSwap. Because the {@link Class} object's identity never 
changes, every
+ *       existing reference to it — including caches inside {@code 
JavaEventHandler} and
+ *       {@code StandardJavaEngine} — automatically executes the new method 
bodies on the
+ *       next call. No framework code needs to know this plugin exists.</li>
+ *   <li>Brand-new classes need no special handling at all: they simply get 
loaded
+ *       normally, from this plugin's build output directory, the first time 
something
+ *       references them.</li>
+ * </ol>
+ *
+ * <h2>Structural changes</h2>
+ * On a stock JVM, {@code redefineClasses} can only replace method bodies and 
static
+ * initializers of a class that is already loaded — adding/removing methods or 
fields,
+ * changing a method signature, or changing the class hierarchy still requires 
a restart.
+ * This is a hard JVM limitation, not something this plugin can work around on 
a stock
+ * JVM. Running on a DCEVM-patched JVM with {@code 
-XX:+AllowEnhancedClassRedefinition}
+ * lifts that restriction transparently: this class calls the exact same
+ * {@code redefineClasses} API either way, so structural changes just work 
when that flag
+ * is present. Because a stock JVM would silently defer that surprise to 
whenever someone
+ * adds a method, {@code ./gradlew ofbizDev}'s build.gradle task treats a 
DCEVM-patched
+ * JVM as required, not optional: it resolves one via {@code -PdcevmHome}/
+ * {@code DCEVM_HOME}, launches with that flag automatically, and fails fast 
with setup
+ * instructions instead of starting OFBiz at all if none is resolvable. Plain 
stock-JDK
+ * operation is not a supported outcome of that command.
+ *
+ * <h2>How services.xml changes are handled</h2>
+ * Every component's {@code servicedef/} directory is also watched; on change, 
the
+ * {@code service.ModelServiceMapByModel} {@link UtilCache} entry is cleared 
directly, so
+ * the new/edited definition is re-read on the next service call.
+ *
+ * <h2>Directory watch limits</h2>
+ * The OS may refuse to watch a directory once a process-wide ceiling is 
reached (most
+ * commonly hit on macOS on a full checkout). This container does not try to 
work around
+ * that itself: a directory whose registration fails is simply left unwatched, 
with a
+ * warning naming it, rather than silently falling back to some slower 
alternate
+ * mechanism. Narrow the set of directories with {@code 
-Dofbiz.hotreload.components}
+ * (or {@code -Photreload.components=compA,compB} with the {@code ofbizDev} 
Gradle task)
+ * to fit under the ceiling — see this component's README for the full 
guidance.
+ *
+ * <h2>Scope</h2>
+ * This container is intentionally dev-only. It has no effect when the system 
property is
+ * absent, so it is safe to leave the registration in this component's
+ * {@code ofbiz-component.xml} for all environments.
+ */
+public class DevReloadContainer implements Container {
+
+    private static final String MODULE = DevReloadContainer.class.getName();
+    private static final String SERVICE_MODEL_CACHE_NAME = 
"service.ModelServiceMapByModel";
+
+    private String name;
+    private WatchService watchService;
+    private Thread watchThread;
+    private ScheduledExecutorService debounceExecutor;
+    private Instrumentation instrumentation;
+
+    private final Debouncer<String> classReloadDebouncer = new 
Debouncer<>(this::applyReload);
+    private final Debouncer<Path> xmlReloadDebouncer = new 
Debouncer<>(this::applyServiceXmlReload);
+    private final Debouncer<Path> compileDebouncer = new 
Debouncer<>(this::applyCompile);
+
+    // Counts across 
registerServicedefDirs()/registerSourceDirs()/registerAll(), so
+    // start() can emit one aggregated warning instead of leaving individual 
failures
+    // scattered in the log where they're easy to miss.
+    private int watchDirsAttempted = 0;
+    private int watchDirsFailed = 0;
+
+    /** Gradle's own compiled-output directory, {@code 
build/classes/java/main}. Read-only
+     *  from this class's perspective: only ever watched (when {@code 
watchBuildOutput} is
+     *  on) for externally-produced {@code .class} files, never written to 
directly. */
+    private Path classesDir;
+
+    /**
+     * This plugin's own compiled-output directory, {@code 
build/devreload/classes},
+     * deliberately separate from Gradle's {@link #classesDir}. In-process 
compiles
+     * (see {@link #applyCompile}) write here instead of into Gradle's managed 
output, so
+     * Gradle's incremental {@code compileJava} up-to-date check is never 
confused by
+     * writes it didn't make itself. Cleared and recreated fresh on every 
{@link #init},
+     * and placed ahead of Gradle's output on the runtime classpath (see
+     * {@code plugins/devreload/build.gradle}) — so when a class exists in both
+     * directories, this one always wins (see {@link #resolveClassFile}).
+     */
+    private Path hotReloadOutputDir;
+
+    // Populated in start() before the watch thread launches; read-only after 
that.
+    private final Set<Path> servicedefDirs = new HashSet<>();
+    private final Set<Path> sourceRootDirs = new HashSet<>();
+
+    /**
+     * Component names to watch, from {@code -Dofbiz.hotreload.components}; 
{@code null}
+     * means watch every component. Set this property to a comma-separated 
list of
+     * component names to keep the total number of watched directories under 
the OS's
+     * per-process ceiling on a large checkout.
+     */
+    private Set<String> allowedComponents;
+
+    /**
+     * Whether {@link #classesDir} ({@code build/classes/java/main}) itself is 
watched, from
+     * {@code -Dofbiz.hotreload.watchBuildOutput}; defaults to {@code false}. 
This tree mirrors
+     * every component's source tree and, unlike {@link #sourceRootDirs}, is 
<em>not</em>
+     * narrowed by {@link #allowedComponents} (compiled output isn't organized 
per component),
+     * so on a full checkout it roughly doubles the total directories watched. 
It only exists to
+     * pick up externally-produced {@code .class} files (e.g. running {@code 
./gradlew -t
+     * classes} in a second terminal); in-process compiles hot-swap directly 
and never need it.
+     * Off by default so unscoped runs need meaningfully fewer real watch 
registrations.
+     */
+    private boolean watchBuildOutput;
+
+    @Override
+    public void init(List<StartupCommand> ofbizCommands, String name, String 
configFile) throws ContainerException {
+        this.name = name;
+
+        if (!"true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload"))) {
+            Debug.logInfo("DevReloadContainer is disabled. Use 
-Dofbiz.hotreload=true to enable.", MODULE);
+            return;
+        }
+
+        parseHotReloadProperties();
+        attachHotSwapAgent();
+
+        if (!prepareClassesDir()) {
+            return;
+        }
+        if (!prepareHotReloadOutputDir()) {
+            return;
+        }
+        createWatchService();
+        registerBuildOutputWatchIfEnabled();
+        startDebounceExecutor();
+
+        Debug.logInfo("DevReloadContainer ready — compiled output at " + 
hotReloadOutputDir.toAbsolutePath(), MODULE);
+    }
+
+    /**
+     * Reads {@code -Dofbiz.hotreload.components} and {@code 
-Dofbiz.hotreload.watchBuildOutput},
+     * populating {@link #allowedComponents} and {@link #watchBuildOutput}.
+     */
+    private void parseHotReloadProperties() {
+        String componentsProperty = 
System.getProperty("ofbiz.hotreload.components");
+        if (componentsProperty != null && !componentsProperty.isBlank()) {
+            allowedComponents = Arrays.stream(componentsProperty.split(","))
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .collect(Collectors.toSet());
+            Debug.logInfo("Hot-reload: scoped to components " + 
allowedComponents
+                    + " (set via -Dofbiz.hotreload.components)", MODULE);
+        }
+
+        watchBuildOutput = 
"true".equalsIgnoreCase(System.getProperty("ofbiz.hotreload.watchBuildOutput"));
+    }
+
+    /**
+     * Self-attaches {@link HotSwapAgent} so Java class redefinition is 
available. Failure
+     * here is non-fatal and never aborts {@link #init}: Java hot-swap is 
simply disabled
+     * while {@code services.xml} auto-reload still works.
+     */
+    private void attachHotSwapAgent() {
+        try {
+            instrumentation = HotSwapAgent.install();
+            Debug.logInfo("Hot-reload: self-attached HotSwapAgent — Java class 
redefinition is available.", MODULE);
+            if (enhancedRedefinitionRequested()) {
+                Debug.logInfo("Hot-reload: -XX:+AllowEnhancedClassRedefinition 
detected — running on a JVM "
+                        + "(e.g. JetBrains Runtime) that can also hot-swap 
structural changes (added/removed "
+                        + "methods or fields, changed signatures), not just method 
bodies.", MODULE);
+            } else {
+                Debug.logWarning("Hot-reload: structural changes (added/removed 
methods or fields, changed "
+                        + "signatures) will require a restart on this JVM. 
'./gradlew ofbizDev' requires a "
+                        + "DCEVM-patched JVM and refuses to start without one, so 
you're most likely seeing "
+                        + "this because hot-reload was activated some other way on 
a stock JDK. See "
+                        + "plugins/devreload/README.md to set up DCEVM_HOME.", 
MODULE);
+            }
+        } catch (Exception e) {
+            Debug.logWarning("Hot-reload: could not self-attach HotSwapAgent 
(" + e.getMessage()
+                    + "). Add -Djdk.attach.allowAttachSelf=true to JVM args. "
+                    + "Java class changes will require a restart; services.xml 
auto-reload still works.", MODULE);
+        }
+    }
+
+    /** Sets {@link #classesDir}. Returns {@code false} (after logging) if it 
doesn't exist yet. */
+    private boolean prepareClassesDir() {
+        classesDir = Paths.get("build/classes/java/main");
+        if (!Files.exists(classesDir)) {
+            Debug.logWarning("Hot-reload: classes directory not found at " + 
classesDir.toAbsolutePath()
+                    + ". Run './gradlew classes' first, then restart.", 
MODULE);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Sets {@link #hotReloadOutputDir} and clears/recreates it. Returns 
{@code false} (after
+     * logging) if the directory could not be prepared -- without a writable 
output directory,
+     * in-process compilation cannot work at all, so the whole container is 
disabled up front
+     * instead of continuing to a misleading "ready" log.
+     */
+    private boolean prepareHotReloadOutputDir() {
+        // build.gradle passes this down as -Dofbiz.hotreload.outputDir, 
computed from the
+        // same value it prepends to the classpath, so the path exists in 
exactly one place
+        // rather than being hardcoded independently here too. The literal 
default below is
+        // only a fallback for the (unsupported) case of starting this 
container without
+        // going through the ofbizDev Gradle task.
+        hotReloadOutputDir = Paths.get(System.getProperty("ofbiz.hotreload.outputDir", 
"build/devreload/classes"));
+        try {
+            FileUtils.deleteDirectory(hotReloadOutputDir.toFile());
+            Files.createDirectories(hotReloadOutputDir);
+            return true;
+        } catch (IOException e) {
+            Debug.logWarning("Hot-reload: could not prepare this plugin's own 
output directory "
+                    + hotReloadOutputDir.toAbsolutePath() + ": " + 
e.getMessage(), MODULE);
+            return false;
+        }
+    }
+
+    /** Creates {@link #watchService} on {@link #classesDir}'s filesystem. */
+    private void createWatchService() throws ContainerException {
+        try {
+            watchService = classesDir.getFileSystem().newWatchService();
+        } catch (IOException e) {
+            throw new ContainerException("DevReloadContainer: failed to initialise 
WatchService", e);
+        }
+    }
+
+    /**
+     * Registers {@link #classesDir} with the WatchService when {@link 
#watchBuildOutput} is
+     * on, so externally-produced {@code .class} files (e.g. {@code ./gradlew 
-t classes} in
+     * a second terminal) are picked up too.
+     */
+    private void registerBuildOutputWatchIfEnabled() {
+        if (watchBuildOutput) {
+            try {
+                registerAll(classesDir);
+            } catch (IOException e) {
+                // registerAll() already logs a warning and skips individual 
directories
+                // that fail to register; reaching here means something more 
fundamental
+                // broke walking the tree at all (e.g. can't even list 
classesDir).
+                Debug.logWarning("Hot-reload: could not fully walk " + classesDir + 
": " + e.getMessage(), MODULE);
+            }
+            Debug.logInfo("Hot-reload: watching compiled-output directory " + 
classesDir.toAbsolutePath()
+                    + " for externally-produced .class files (set via 
-Dofbiz.hotreload.watchBuildOutput=true).",
+                    MODULE);
+        } else {
+            Debug.logInfo("Hot-reload: not watching " + classesDir.toAbsolutePath() + 
" (this tree isn't "
+                    + "narrowed by -Dofbiz.hotreload.components and roughly doubles 
the total directories "
+                    + "watched). In-process edits still hot-swap normally; running 
'./gradlew -t classes' in a "
+                    + "second terminal will not be picked up unless you set "
+                    + "-Dofbiz.hotreload.watchBuildOutput=true (or 
-Photreload.watchBuildOutput=true with the "
+                    + "ofbizDev Gradle task).", MODULE);
+        }
+    }
+
+    /** Starts the daemon executor backing every {@link Debouncer}. */
+    private void startDebounceExecutor() {
+        debounceExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+            Thread t = new Thread(r, "ofbiz-hot-reload-debouncer");
+            t.setDaemon(true);
+            return t;
+        });
+    }
+
+    /**
+     * Picks whichever of this plugin's own compiled output ({@code overlay}, 
under
+     * {@link #hotReloadOutputDir}) or Gradle's ({@code fallback}, under 
{@link #classesDir})
+     * is the right one to redefine from. {@code overlay} always wins when it 
exists,
+     * matching its position ahead of {@code fallback} on the runtime 
classpath (see
+     * {@code build.gradle}) — a class compiled here is always the one a 
caller would load.
+     * Returns {@code null} if neither exists.
+     */
+    private Path resolveClassFile(Path overlay, Path fallback) {
+        if (Files.exists(overlay)) {
+            return overlay;
+        }
+        if (Files.exists(fallback)) {
+            return fallback;
+        }
+        return null;
+    }
+
+    @Override
+    public boolean start() throws ContainerException {
+        if (watchService == null) {
+            return true; // disabled
+        }
+        registerServicedefDirs();
+        registerSourceDirs();
+        if (watchDirsFailed > 0) {
+            Debug.logWarning("Hot-reload: " + watchDirsFailed + " of " + 
watchDirsAttempted + " directory watch "
+                    + "registrations hit file-descriptor/watch exhaustion (see 
warnings above for which ones) "
+                    + "and are NOT being watched — changes there will not 
hot-reload until you restart. Scope "
+                    + "hot-reload to just the components you're working on with 
"
+                    + "-Dofbiz.hotreload.components=compA,compB (or 
-Photreload.components=compA,compB with "
+                    + "the ofbizDev Gradle task) to fit under the OS watch 
limit.", MODULE);
+        }
+        watchThread = new Thread(this::watchLoop, "ofbiz-hot-reload-watcher");
+        watchThread.setDaemon(true);
+        watchThread.start();
+        Debug.logInfo("DevReloadContainer started. Edit any Java or services.xml 
file and changes go live without a restart.", MODULE);
+        return true;
+    }
+
+    /**
+     * Registers every directory that contains a component service-definition 
XML file
+     * (type="model") with the WatchService so that edits to those files are 
detected.
+     * Called once from {@link #start()}, before the watch thread launches.
+     */
+    private void registerServicedefDirs() {
+        for (ComponentConfig.ServiceResourceInfo sri : 
ComponentConfig.getAllServiceResourceInfos("model")) {
+            if (allowedComponents != null && 
!allowedComponents.contains(sri.getComponentConfig().getComponentName())) {
+                continue;
+            }
+            try {
+                URL url = sri.createResourceHandler().getURL();
+                if (!"file".equals(url.getProtocol())) {
+                    continue; // skip non-filesystem resources (classpath 
jars, etc.)
+                }
+                Path dir = Paths.get(new URI(url.toString())).getParent();
+                if (dir != null && Files.isDirectory(dir) && 
servicedefDirs.add(dir)) {
+                    watchDirsAttempted++;
+                    try {
+                        dir.register(watchService,
+                                StandardWatchEventKinds.ENTRY_CREATE,
+                                StandardWatchEventKinds.ENTRY_MODIFY);
+                        Debug.logInfo("Hot-reload: watching servicedef directory 
" + dir, MODULE);
+                    } catch (IOException e) {
+                        warnUnwatched(dir, e);
+                    }
+                }
+            } catch (GenericConfigException | URISyntaxException e) {
+                Debug.logWarning("Hot-reload: could not register servicedef dir for 
"
+                        + sri.getLocation() + ": " + e.getMessage(), MODULE);
+            } catch (Throwable t) {
+                // Defensive: a single component's servicedef registration 
must not be able
+                // to abort the loop and leave every subsequent component's 
servicedef
+                // directory unwatched.
+                Debug.logError(t, "Hot-reload: unexpected error registering 
servicedef dir for "
+                        + sri.getLocation(), MODULE);
+            }
+        }
+    }
+
+    /**
+     * Registers every component's {@code src/main/java} directory with the 
WatchService
+     * so that saving a {@code .java} file triggers in-process compilation via
+     * {@link ToolProvider#getSystemJavaCompiler()}. Falls back gracefully 
when running
+     * on a JRE (compiler unavailable) — source watching is simply skipped.
+     */
+    private void registerSourceDirs() {
+        if (ToolProvider.getSystemJavaCompiler() == null) {
+            Debug.logWarning("Hot-reload: javax.tools.JavaCompiler not available 
(JRE, not JDK?). "
+                    + "Java source auto-compilation disabled — use './gradlew -t 
classes' in a second terminal.", MODULE);
+            return;
+        }
+        for (ComponentConfig cc : ComponentConfig.getAllComponents()) {
+            if (cc.rootLocation() == null) {
+                continue;
+            }
+            if (allowedComponents != null && 
!allowedComponents.contains(cc.getComponentName())) {
+                continue;
+            }
+            Path srcDir = cc.rootLocation().resolve("src/main/java");
+            if (Files.isDirectory(srcDir) && sourceRootDirs.add(srcDir)) {
+                try {
+                    registerAll(srcDir);
+                    Debug.logInfo("Hot-reload: watching source directory " + 
srcDir, MODULE);
+                } catch (IOException e) {
+                    // registerAll() already logs a warning and skips 
individual directories
+                    // that fail to register; reaching here means something 
more fundamental
+                    // broke walking the tree at all (e.g. can't list srcDir).
+                    Debug.logWarning("Hot-reload: could not walk source dir " + srcDir + 
": " + e.getMessage(), MODULE);
+                }
+            }
+        }
+        if (!sourceRootDirs.isEmpty()) {
+            Debug.logInfo("Hot-reload: Java source auto-compilation active — save a 
.java file and it reloads automatically.", MODULE);
+        }
+    }
+
+    @Override
+    public void stop() throws ContainerException {
+        if (debounceExecutor != null) {
+            debounceExecutor.shutdownNow();
+        }
+        if (watchService != null) {
+            try {
+                watchService.close();
+            } catch (IOException e) {
+                Debug.logError(e, "DevReloadContainer: error closing 
WatchService", MODULE);
+            }
+        }
+        if (watchThread != null) {
+            watchThread.interrupt();
+        }
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Watch loop
+    // 
-------------------------------------------------------------------------
+
+    private void watchLoop() {
+        while (!Thread.currentThread().isInterrupted()) {
+            WatchKey key;
+            try {
+                key = watchService.take();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            } catch (ClosedWatchServiceException e) {
+                break;
+            }
+
+            Path dir = (Path) key.watchable();
+            for (WatchEvent<?> event : key.pollEvents()) {
+                WatchEvent.Kind<?> kind = event.kind();
+                if (kind == StandardWatchEventKinds.OVERFLOW) {
+                    continue;
+                }
+
+                @SuppressWarnings("unchecked")
+                Path changed = dir.resolve(((WatchEvent<Path>) 
event).context());
+
+                if (kind == StandardWatchEventKinds.ENTRY_CREATE && 
Files.isDirectory(changed)) {
+                    // New package directory created during compilation — 
register it.
+                    try {
+                        registerAll(changed);
+                    } catch (IOException e) {
+                        Debug.logError(e, "DevReloadContainer: failed to register 
new directory: " + changed, MODULE);
+                    }
+                } else if ((kind == StandardWatchEventKinds.ENTRY_CREATE || 
kind == StandardWatchEventKinds.ENTRY_MODIFY)
+                        && changed.toString().endsWith(".class")) {
+                    // Only react to written/updated class files. Ignore 
ENTRY_DELETE so
+                    // that removing a source file (and its .class output) 
does not cause
+                    // a redefinition attempt against a now-missing file.
+                    reloadClassFile(classesDir, changed);
+                } else if ((kind == StandardWatchEventKinds.ENTRY_CREATE || 
kind == StandardWatchEventKinds.ENTRY_MODIFY)
+                        && changed.toString().endsWith(".xml")
+                        && servicedefDirs.stream().anyMatch(dir::startsWith)) {
+                    xmlReloadDebouncer.add(changed);
+                } else if ((kind == StandardWatchEventKinds.ENTRY_CREATE || 
kind == StandardWatchEventKinds.ENTRY_MODIFY)
+                        && changed.toString().endsWith(".java")
+                        && sourceRootDirs.stream().anyMatch(dir::startsWith)) {
+                    compileDebouncer.add(changed);
+                }
+            }
+            if (!key.reset()) {
+                Debug.logWarning("Hot-reload: watch key became invalid (directory 
deleted?): "
+                        + key.watchable() + ". WatchService will no longer detect 
changes in that directory.", MODULE);
+            }
+        }
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Debounced batching
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Coalesces rapid-fire change notifications into one action, so a single 
compile run
+     * that touches many files (e.g. one with inner/anonymous classes, or a 
Gradle build
+     * writing several {@code .class} files at once) is handled as a single 
batch instead
+     * of one action per file. Shared by all three change pipelines (class 
reload,
+     * {@code services.xml} reload, Java compile) instead of each hand-rolling 
its own
+     * pending-set/cancel/reschedule bookkeeping.
+     */
+    private final class Debouncer<T> {
+        private final Set<T> pending = new HashSet<>();
+        private final Consumer<Set<T>> action;
+        private ScheduledFuture<?> scheduled;
+
+        Debouncer(Consumer<Set<T>> action) {
+            this.action = action;
+        }
+
+        synchronized void add(T item) {
+            pending.add(item);
+            if (scheduled != null) {
+                scheduled.cancel(false);
+            }
+            try {
+                // Wait 300 ms after the last change so a burst of related 
changes (e.g. a
+                // single Gradle compile run writing multiple .class files) is 
handled as
+                // one batch instead of one action per file.
+                scheduled = debounceExecutor.schedule(this::fire, 300, 
TimeUnit.MILLISECONDS);
+            } catch (RejectedExecutionException e) {
+                // Container is shutting down; pending changes will not be 
applied.
+            }
+        }
+
+        private synchronized void fire() {
+            if (pending.isEmpty()) {
+                return;
+            }
+            Set<T> batch = new HashSet<>(pending);
+            pending.clear();
+            action.accept(batch);
+        }
+    }
+
+    /** Resolves {@code changed} to a class name relative to {@code baseDir} 
and, if valid, queues it for reload. */
+    private void reloadClassFile(Path baseDir, Path changed) {
+        String className = toClassName(baseDir, changed);
+        if (className != null) {
+            classReloadDebouncer.add(className);
+        }
+    }
+
+    private void applyReload(Set<String> batch) {
+        Debug.logInfo("Hot-reload: detected changes in " + batch, MODULE);
+
+        if (instrumentation == null) {
+            Debug.logWarning("Hot-reload: HotSwapAgent not attached — " + batch
+                    + " compiled but not applied to the running JVM. Restart to 
pick it up.", MODULE);
+            return;
+        }
+
+        List<ClassDefinition> defs = new ArrayList<>();
+        for (String className : batch) {
+            Path relative = Paths.get(className.replace('.', '/') + ".class");
+            Path overlayFile = hotReloadOutputDir.resolve(relative);
+            Path gradleFile = classesDir.resolve(relative);
+            Path classFile = resolveClassFile(overlayFile, gradleFile);
+            if (classFile == null) {
+                Debug.logWarning("Hot-reload: detected a change for " + 
className
+                        + " but could not find its compiled output in either "
+                        + hotReloadOutputDir.toAbsolutePath() + " or " + 
classesDir.toAbsolutePath(), MODULE);
+                continue;
+            }
+            try {
+                Class<?> loaded = findLoadedClass(className);
+                if (loaded == null) {
+                    // Never loaded yet in this JVM — nothing to redefine. It 
will simply
+                    // load fresh, with the new bytecode, the first time 
something
+                    // references it, from whichever directory 
resolveClassFile() would
+                    // pick (overlay first on the classpath too, see 
build.gradle).
+                    continue;
+                }
+                defs.add(new ClassDefinition(loaded, 
Files.readAllBytes(classFile)));
+            } catch (IOException e) {
+                Debug.logError(e, "Hot-reload: failed to read class file for " 
+ className, MODULE);
+            }
+        }
+
+        if (defs.isEmpty()) {
+            Debug.logInfo("Hot-reload: nothing already loaded to redefine for 
" + batch, MODULE);
+            return;
+        }
+
+        try {
+            instrumentation.redefineClasses(defs.toArray(new 
ClassDefinition[0]));
+            // Clear service definition cache so newly added service methods 
are discovered.
+            // We deliberately do NOT clear webapp.Controller caches here — 
controller.xml
+            // has not changed, only .class files have, and clearing those 
caches triggers
+            // Groovy re-compilation of screen expressions which can fail 
unexpectedly.
+            UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME);
+            Debug.logInfo("Hot-reload complete for: " + batch, MODULE);
+        } catch (UnsupportedOperationException e) {
+            // ./gradlew ofbizDev only ever runs on a DCEVM-patched JVM (see 
build.gradle),
+            // which already lifts the plain-JVM restriction to method bodies 
only, so
+            // add/remove method-or-field and signature changes normally 
succeed here. This
+            // still fires for the narrower set of changes DCEVM itself can't 
apply either
+            // (e.g. a changed class hierarchy) -- the same remaining limit an 
IDE debugger's
+            // HotSwap has even on a capable JVM.
+            Debug.logWarning("Hot-reload: " + batch + " contains a structural 
change (added/removed "
+                    + "method or field, changed signature, changed hierarchy) that 
the JVM cannot "
+                    + "hot-swap. Restart OFBiz to pick it up. (" + e.getMessage() + 
")", MODULE);
+        } catch (Throwable e) {
+            Debug.logError(e, "Hot-reload failed for " + batch, MODULE);
+        }
+    }
+
+    /**
+     * Best-effort detection of whether this JVM was launched with
+     * {@code -XX:+AllowEnhancedClassRedefinition} (e.g. a JetBrains Runtime), 
which is
+     * what allows {@link Instrumentation#redefineClasses} to also apply 
structural
+     * changes instead of just method bodies. Purely informational — the actual
+     * capability is exercised (and, if absent, reported) when a redefinition 
is
+     * attempted in {@link #applyReload}.
+     */
+    private static boolean enhancedRedefinitionRequested() {
+        return 
ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
+                .anyMatch(arg -> 
arg.contains("AllowEnhancedClassRedefinition"));
+    }
+
+    /** Searches classes already loaded in the JVM for one matching {@code 
className}. */
+    private Class<?> findLoadedClass(String className) {
+        for (Class<?> c : instrumentation.getAllLoadedClasses()) {
+            if (c.getName().equals(className)) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    private void applyServiceXmlReload(Set<Path> batch) {
+        Debug.logInfo("Hot-reload: service XML changed " + batch + " — clearing 
service model cache", MODULE);
+        try {
+            UtilCache.clearCache(SERVICE_MODEL_CACHE_NAME);
+            Debug.logInfo("Hot-reload: service model cache cleared; definitions 
will be re-read on next service call", MODULE);
+        } catch (Throwable e) {
+            Debug.logError(e, "Hot-reload: failed to clear service model 
cache", MODULE);
+        }
+    }
+
+    private void applyCompile(Set<Path> batch) {
+        Debug.logInfo("Hot-reload: compiling " + batch, MODULE);
+        try {
+            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+            if (compiler == null) {
+                return;
+            }
+            try (StandardJavaFileManager fm = 
compiler.getStandardFileManager(null, null, null)) {
+                fm.setLocation(StandardLocation.CLASS_OUTPUT,
+                        List.of(hotReloadOutputDir.toAbsolutePath().toFile()));
+                // Reuse the running JVM's classpath — it already contains all 
OFBiz jars.
+                List<String> options = Arrays.asList("-cp", 
System.getProperty("java.class.path"), "-proc:none");
+                var units = fm.getJavaFileObjectsFromPaths(batch);
+                boolean ok = compiler.getTask(null, fm, null, options, null, 
units).call();
+                if (ok) {
+                    Debug.logInfo("Hot-reload: compilation successful", 
MODULE);
+
+                    // Collect all .class files produced by this compilation 
round.
+                    // Each source file can produce multiple .class files when 
it contains
+                    // inner or anonymous classes (e.g. Foo$Bar.class, 
Foo$1.class).
+                    // All of them must be redefined too — otherwise the inner 
class still
+                    // resolves through its stale, previously-loaded bytecode.
+                    for (Path src : batch) {
+                        Path cf = sourceToClassFile(src); // relative path for 
the outer class
+                        if (cf == null) {
+                            continue;
+                        }
+                        String outerName = cf.getFileName().toString().replace(".class", 
"");
+                        Path absOutputDir = cf.toAbsolutePath().getParent();
+                        try (var dirStream = Files.list(absOutputDir)) {
+                            dirStream.filter(absFile -> {
+                                String fn = absFile.getFileName().toString();
+                                // Match Foo.class and Foo$Inner.class / 
Foo$1.class
+                                return fn.endsWith(".class")
+                                        && (fn.equals(outerName + ".class")
+                                                || fn.startsWith(outerName + 
"$"));
+                            }).forEach(absFile -> {
+                                // Convert absolute output path back to a 
relative path that
+                                // is rooted at CWD (same type as 
hotReloadOutputDir) so that
+                                // toClassName(hotReloadOutputDir, relPath) — 
which calls
+                                // relativize — does not throw 
IllegalArgumentException.
+                                Path rel = hotReloadOutputDir.resolve(
+                                        
hotReloadOutputDir.toAbsolutePath().relativize(absFile));
+                                reloadClassFile(hotReloadOutputDir, rel);
+                            });
+                        } catch (IOException e) {
+                            // Output dir unreadable; fall back to the outer 
class only.
+                            reloadClassFile(hotReloadOutputDir, cf);
+                        }
+                    }
+
+                    // Re-register class directories so external compilations 
(./gradlew classes
+                    // run by a developer in a separate terminal) still reach 
the class watcher.
+                    // Only relevant if that watch is enabled in the first 
place (watchBuildOutput);
+                    // otherwise there is nothing registered under classesDir 
to refresh.
+                    // Catch Exception (not just IOException) because 
ClosedWatchServiceException
+                    // extends IllegalStateException, which is a 
RuntimeException — it can be
+                    // thrown here if OFBiz is shutting down while a compile 
finishes.
+                    if (watchBuildOutput) {
+                        try {
+                            registerAll(classesDir);
+                        } catch (Exception e) {
+                            Debug.logWarning("Hot-reload: could not re-register 
class dirs: " + e.getMessage(), MODULE);
+                        }
+                    }
+                } else {
+                    Debug.logWarning("Hot-reload: compilation failed — fix the 
error and save again", MODULE);
+                }
+            }
+        } catch (Throwable e) {
+            Debug.logError(e, "Hot-reload: compilation error", MODULE);
+        }
+    }
+
+    /**
+     * Maps a {@code .java} source file to the corresponding {@code .class} 
output file
+     * under {@link #hotReloadOutputDir}. Returns {@code null} if the source 
file is not
+     * under any registered source root.
+     */
+    private Path sourceToClassFile(Path sourceFile) {
+        for (Path srcRoot : sourceRootDirs) {
+            if (sourceFile.startsWith(srcRoot)) {
+                Path relative = srcRoot.relativize(sourceFile);
+                String name = relative.toString();
+                if (name.endsWith(".java")) {
+                    String classRelative = name.substring(0, name.length() - 
".java".length()) + ".class";
+                    return hotReloadOutputDir.resolve(classRelative);
+                }
+            }
+        }
+        return null;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Helpers
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Recursively registers every directory under {@code start} with the 
WatchService.
+     * A directory whose registration fails (e.g. watch/descriptor exhaustion) 
is logged
+     * and left unwatched via {@link #warnUnwatched} instead of aborting the 
whole walk,
+     * so one overloaded directory never leaves the rest of the tree unwatched.
+     */
+    private void registerAll(Path start) throws IOException {
+        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, 
BasicFileAttributes attrs) {
+                watchDirsAttempted++;
+                try {
+                    dir.register(watchService,
+                            StandardWatchEventKinds.ENTRY_CREATE,
+                            StandardWatchEventKinds.ENTRY_MODIFY);
+                } catch (IOException e) {
+                    warnUnwatched(dir, e);
+                } catch (Throwable t) {
+                    // register() itself should only throw IOException, but 
nothing here is
+                    // worth crashing the whole startup over.
+                    Debug.logError(t, "Hot-reload: unexpected error registering 
watch for " + dir
+                            + " -- this directory will not be watched.", 
MODULE);
+                }
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    /**
+     * Records that {@code dir} could not get a WatchService registration 
(most commonly
+     * the OS's per-process watch ceiling, e.g. macOS's kqueue-per-directory 
cost) and
+     * logs why. The directory is simply left unwatched: changes there require 
a restart
+     * (or {@code -Dofbiz.hotreload.components}/{@code -Photreload.components} 
to narrow
+     * the watched set below the ceiling) rather than falling back to some 
slower
+     * alternate mechanism.
+     */
+    private void warnUnwatched(Path dir, IOException cause) {
+        watchDirsFailed++;
+        Debug.logWarning("Hot-reload: could not watch " + dir + " (" + 
cause.getMessage() + ") -- changes "
+                + "there will not be picked up until OFBiz is restarted. Narrow the 
watched set with "
+                + "-Dofbiz.hotreload.components=compA,compB (or 
-Photreload.components=compA,compB with the "
+                + "ofbizDev Gradle task) to fit under the OS watch limit.", 
MODULE);
+    }
+
+    /**
+     * Converts a {@code .class} file path relative to {@code baseDir} into a
+     * binary class name.
+     *
+     * <p>Example: {@code com/example/Foo.class} → {@code com.example.Foo}
+     */
+    private static String toClassName(Path baseDir, Path classFile) {
+        Path relative = baseDir.relativize(classFile);
+        String s = relative.toString();
+        if (!s.endsWith(".class")) {
+            return null;
+        }
+        // Normalise path separator to '.' and strip the '.class' suffix
+        return s.substring(0, s.length() - ".class".length())
+                .replace(classFile.getFileSystem().getSeparator(), ".");
+    }
+}
diff --git 
a/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java 
b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java
new file mode 100644
index 000000000..f3542a5bf
--- /dev/null
+++ b/devreload/src/main/java/org/apache/ofbiz/devreload/HotSwapAgent.java
@@ -0,0 +1,109 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
*******************************************************************************/
+package org.apache.ofbiz.devreload;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.instrument.Instrumentation;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.jar.Attributes;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+
+import com.sun.tools.attach.VirtualMachine;
+
+/**
+ * Self-attaching Java agent that gives {@link DevReloadContainer} access to
+ * {@link Instrumentation#redefineClasses}, the same JVM mechanism an IDE 
debugger uses
+ * for HotSwap — it replaces the bytecode of an already-loaded {@link Class} 
object in
+ * place, so every existing reference to it (including cached {@code Class} 
objects in
+ * framework classes like {@code JavaEventHandler}) picks up the new method 
bodies on the
+ * very next call.
+ *
+ * <p>The agent jar is built on the fly from this class's own compiled 
bytecode and
+ * attached to the current process via the JDK Attach API — no {@code 
-javaagent} JVM
+ * flag, and no framework code changes. This class and {@link 
DevReloadContainer} are the
+ * only two moving parts.
+ *
+ * <p>Requires the JVM flag {@code -Djdk.attach.allowAttachSelf=true} (a JDK 
9+ safeguard
+ * against a process attaching to itself). The {@code ofbizDev} Gradle task 
provided by
+ * this plugin sets it automatically.
+ */
+public final class HotSwapAgent {
+
+    private static volatile Instrumentation instrumentation;
+
+    private HotSwapAgent() { }
+
+    /** Invoked by the JVM once {@link VirtualMachine#loadAgent} loads this 
class as an agent. */
+    public static void agentmain(String agentArgs, Instrumentation inst) {
+        instrumentation = inst;
+    }
+
+    /**
+     * Builds a minimal agent jar wrapping this class and self-attaches it to 
the current
+     * JVM, returning the resulting {@link Instrumentation}. Safe to call more 
than once;
+     * later calls return the instance obtained by the first successful attach.
+     */
+    static synchronized Instrumentation install() throws Exception {
+        if (instrumentation != null) {
+            return instrumentation;
+        }
+        Path agentJar = buildAgentJar();
+        try {
+            String pid = String.valueOf(ProcessHandle.current().pid());
+            VirtualMachine vm = VirtualMachine.attach(pid);
+            try {
+                vm.loadAgent(agentJar.toAbsolutePath().toString());
+            } finally {
+                vm.detach();
+            }
+        } finally {
+            Files.deleteIfExists(agentJar);
+        }
+        return instrumentation;
+    }
+
+    private static Path buildAgentJar() throws IOException {
+        String resource = HotSwapAgent.class.getName().replace('.', '/') + 
".class";
+        Path jar = Files.createTempFile("devreload-hotswap-agent", ".jar");
+
+        Manifest manifest = new Manifest();
+        Attributes attrs = manifest.getMainAttributes();
+        attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
+        attrs.put(new Attributes.Name("Agent-Class"), 
HotSwapAgent.class.getName());
+        attrs.put(new Attributes.Name("Can-Redefine-Classes"), "true");
+        attrs.put(new Attributes.Name("Can-Retransform-Classes"), "true");
+
+        try (InputStream in = 
HotSwapAgent.class.getClassLoader().getResourceAsStream(resource);
+                OutputStream fos = Files.newOutputStream(jar);
+                JarOutputStream jos = new JarOutputStream(fos, manifest)) {
+            if (in == null) {
+                throw new IOException("Could not locate compiled class on 
classpath: " + resource);
+            }
+            jos.putNextEntry(new ZipEntry(resource));
+            in.transferTo(jos);
+            jos.closeEntry();
+        }
+        return jar;
+    }
+}

Reply via email to