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; > > + } > > +} > >
