Hi Jacques,

I'm in favour of removing the line from rat-excludes.txt and adding
the license headers to the README.md files.

Thanks,
Anahita

Il giorno lun 20 lug 2026 alle ore 11:52 Jacques Le Roux via dev
<[email protected]> ha scritto:
>
> 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