Documentation menu
Complete technical reference

Usage Guide

Reclazz supports four modes of operation. You can use them independently or combine them depending on your workflow.


Option 1: Manual Setup

Use this option when you run the SAP Commerce server outside of IntelliJ (e.g., from the terminal with ./hybrisserver.sh) or when you want full control over JVM arguments.

How It Works

You add the -javaagent flag to your Hybris Tomcat configuration. The agent loads inside the SAP Commerce JVM, watches for compiled .class files, and hot-swaps them when you run ant build.

Setup

Edit hybris/config/local.properties and add the agent to tomcat.javaoptions, preserving any existing options in that value:

Any JDK 17+ (works for structural reloads too):

properties
tomcat.javaoptions=-javaagent:/path/to/reclazz-agent.jar=hybrisHome=/path/to/hybris

Structural hot-reload (adding/removing methods and fields) works on any JDK 17+ via Reclazz's companion-class reloader, no extra JVM flags required. On standard JVMs, the new members live on a hidden nestmate and are reached through invokedynamic routed from hot-compiled callers; reflective access (Class.getMethod(...), ModelService) is limited to members that existed at boot time. See Companion-class caveat below for details.

JetBrains Runtime / DCEVM (full reflective visibility):

properties
tomcat.javaoptions=-javaagent:/path/to/reclazz-agent.jar=hybrisHome=/path/to/hybris -XX:+AllowEnhancedClassRedefinition

Enhanced redefinition applies structural changes to the original Class object itself, so reflection and reflective caches see new members without restart.

Tip: If you already have debug options configured, you can append the agent to tomcat.debugjavaoptions instead. This property is applied when you start the server in debug mode:

properties
tomcat.debugjavaoptions=-javaagent:/path/to/reclazz-agent.jar=hybrisHome=/path/to/hybris -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n

Finding the Agent JAR Path

The location of reclazz-agent.jar depends on how you installed Reclazz:

  • Installed via IntelliJ plugin: The agent JAR is bundled inside the plugin directory. You can find it at:

    text
    # macOS
    ~/Library/Application Support/JetBrains/IntelliJIdea<version>/plugins/reclazz/agent/reclazz-agent.jar
    
    # Linux
    ~/.local/share/JetBrains/IntelliJIdea<version>/plugins/reclazz/agent/reclazz-agent.jar
    
    # Windows
    %APPDATA%\JetBrains\IntelliJIdea<version>\plugins\reclazz\agent\reclazz-agent.jar
    
  • Downloaded from GitHub: Use the absolute path wherever you saved the JAR.

  • Built from source: Use the absolute path to agent/build/libs/reclazz-agent.jar.

Agent Arguments

Arguments are passed as a comma-separated string after the = sign:

properties
-javaagent:/path/to/reclazz-agent.jar=hybrisHome=/opt/hybris,watchExtensions=mycore;mystorefront,autoImpex=true,verbose=true
Argument Default Description
hybrisHome auto-detect Absolute path to the hybris/ directory
watchExtensions all custom Semicolon-separated extension names to watch (empty = all)
excludePatterns (none) Semicolon-separated glob patterns to exclude (e.g., *Test.class;*Mock*)
autoCompile false Compile .java files internally instead of watching .class files
autoImpex false Auto-import changed .impex files
debounceMs 500 Milliseconds to wait before processing changes (batches rapid file writes)
startupDelaySec 30 Seconds to wait after agent startup before watching files
verbose false Enable verbose logging in the console, including each framework step's own line after a reload (the reload line carries the summary either way)
statusPort 0 TCP port for plugin communication (0 = auto-assign)
portFile (none) Path where agent writes its actual port after binding
watchDirs auto-detect Semicolon-separated class output directories to watch, for a project the detection does not know
excludeClasses (none) Semicolon-separated class name patterns the transform leaves alone; the way out when instrumenting one class is the problem
impexAllowRemove false Let auto-imported ImpEx files run REMOVE lines
autoUpdateRunningSystem false After an items.xml change regenerates the models, run the schema-only Update Running System (add columns, no data, never drop) so a new attribute is live without a manual HAC click. A saved *-locales_<iso>.properties also writes the type, attribute and enumeration names it keys onto the types, as "Localize types" would for those keys only. Writes to the live database, so opt-in and dev-only
jpaRefresh false Opt-in mapping rebuild. Existing fields require enhanced redefinition; new entities also work on stock JDKs, including supported Hibernate validate with a prepared schema. See scope
structuralReload true The companion engine that adds and removes members on a stock JDK; false leaves method-body reloads only
platform auto Skip detection and name the platform (hybris, spring, generic)
wrapOutput auto Wrap console lines to the terminal width: auto, true, false
transformDumpDir (none) Write every transformed class file here, for looking at what the agent emitted
verifyTransform false Run the bytecode verifier over every transformed class and print what it says
sessionLog (none) Append every status line to this file with an ISO timestamp and level, no colour: the session's record, to read back or attach to a report
reloadBoundary immediate request waits for synchronous MVC dispatches and supported Spring 5 javax and Spring 6 Jakarta MVC async requests before applying a class reload batch; requires -javaagent at JVM startup. See Reload between requests
refreshOwnersOnHelper (none) Opt-in helperFqn>owner1;owner2|helper2>owner3: after a helper class reloads, recreate the named Spring singleton cache-owner beans so a custom cache they hold is rebuilt through the fresh helper logic. Owners are recreated through the normal guarded bean-reload lifecycle; non-singletons, FactoryBeans and event-listener owners are declined. See Refreshing a custom cache after a helper reload

Arguments are never removed or renamed within a major version: a line that worked with an older 1.x agent works with a newer one. An argument the agent does not know is named in the console at start-up and ignored, not fatal, so a line written for a newer agent still starts an older one. Put such arguments first: the line is split at a comma only where a known argument follows, so that a value may contain a comma, which means an unknown argument placed after another becomes part of that one's value (and is still named). The plugin's own test (AgentArgumentContractTest) keeps what it passes inside this table, and the agent's (AgentArgumentsAreDocumentedTest) keeps this table equal to what the agent accepts.

Message bundles and cached templates

Reclazz invalidates the cached text when a recognized physical resource is saved in a watched directory. Current P5 observes actual formatted message lookups with ResourceBundleMessageSource and ReloadableResourceBundleMessageSource on Spring 5.3.39 and isolated Spring 6.1.14. The tests warm permanent caches, edit messages_tr.properties, then read new UTF-8 text through the existing context and retain fallback keys from messages.properties. Both ordinary and child application loaders are exercised. Arbitrary custom message basenames are not automatically discovered by the filename classifier. Mock ancestor-reset method preference is component evidence; a real parent/child source watcher is unverified.

Current P5 also observes FreeMarker 2.3.34 file-template rendering and Thymeleaf 3.1.3.RELEASE HTML rendering with FileTemplateResolver and the standard dialect. Caches remain enabled; native controls show a file edit alone still serves the old result until explicit invalidation. Repeated watched saves change the rendered output. A malformed template produces a native rendering error; fixing the file recovers. This does not promise retention of the last valid template on errors. The child-loader case places the application in a child loader; engine jars stay in the parent. Child-isolated engines and arbitrary custom resolvers/dialects, Spring MVC view-resolver integration and packaged JAR/WAR resource edits have no current acceptance proof. The tests edit the actual runtime file; automatic source-resource copying is not established by these observations.

These observations use stock SapMachine JDK 17/21 on macOS arm64 and startup agent attachment. They do not certify other versions, operating systems, late attachment or native SAP tenant behavior. Engine libraries are test-only and are not packaged with the production agent. See the P5 registry for separate watcher, component, refusal and unverified claims.

Loaded Spring XML resources

Alongside *-spring.xml, Reclazz recognizes local .xml resources that Spring's XML reader successfully loaded for an active application context after agent startup. This includes nested imports and files such as spring-mvc-config.xml and web-application-config.xml, including files with no bean definitions of their own. It uses the resolved file path and owning context, not a basename match or inspection of arbitrary XML content.

Files under existing watch roots use the normal watcher. Other captured local XML files are checked once per second without watching their neighbouring files; module and filename exclusions still apply. Closed contexts and temporary parser factories do not grant resource ownership. JAR entries, remote resources and XML loaded before the agent's reader hook are not covered by this discovery path. Reserved logging, code-generation and backoffice filename rules retain priority.

Recognition routes supported bean changes through the existing XML safety checks; it does not promise arbitrary MVC/security/filter-chain rebuilding or a context refresh. Import-topology changes are not a full live configuration reload: a new import seen only by the temporary parser does not become a captured live resource. Unsupported bean changes still require the reported restart. Use the actual loaded file; editing a different source copy with the same name does not update it.

For an already-instantiated ordinary bean, a failed property resolution, conversion or setter call keeps the previous raw property in its bean definition and reports the failure. Other supported properties and beans can still update. A setter may change application state before throwing; those side effects are not rolled back. Uninstantiated lazy beans retain native deferred validation: their requested metadata is updated without creating them, and invalid values can fail when Spring later creates the bean. Only that bean's cached merged definition is invalidated so native creation reads the requested metadata; other beans' caches stay intact.

For existing ordinary XML beans, changes to depends-on, primary, autowire, autowire-candidate or the effective lazy-init flag require restart. Spring's parsed flag determines equivalence, including default-lazy-init and explicit defaults. Changing the flag does not create a cold bean or undo an already created instance. The affected bean's properties and raw definition stay unchanged, including for a lazy bean; supported changes to independent beans can still apply. Unchanged declarations continue to allow property edits. This diagnostic does not rebuild initialization order or reinject existing consumers; new beans retain Spring's native metadata handling and the existing guidance for consumers created before their addition.

Ordinary XML property values are compared by their declared configuration, including inner bean properties/constructor values and local versus parent bean references. An unchanged inner bean keeps its live identity; generated inner names, source locations and resolved type caches do not trigger reconstruction. Changed simple inner beans still use Spring's native value resolution. Changes that would reconstruct resource, lifecycle, factory or otherwise unsupported inner compositions require restart before any property of that owner is updated; independent supported beans can still update. Comparison has bounded depth and size and refuses unknown raw values. This does not destroy or rewire arbitrary nested live objects, and user constructor/setter side effects are not rolled back.

Existing unsupported XML definitions do not report restart merely because the file was saved again: a standard definition proven unchanged is left alone, including abstract templates and prototype beans. Real edits, new or removed unsupported definitions still require the existing restart or recreation checks. This proof includes declared values, property order and alias sets and refuses custom definition subclasses, instance suppliers or unverifiable nested metadata; their prior conservative diagnostics remain. It does not make unsupported shapes reloadable or reconstruct XML that arbitrary post-processors have rewritten.

OCC field-set mappings

For existing SAP Commerce FieldSetLevelMapping beans, supported XML property edits now refresh the standard DefaultFieldSetLevelHelper consumers and their manager-backed fieldSetCache. BASIC/DEFAULT/FULL selections can add or remove existing DTO fields. Descendant contexts that see the changed mapping are included; unrelated contexts and other named caches are left alone, and nothing is created in them: SAP's lazy web beans stay uncreated. The edited file is parsed again with the document reader its context used, so SAP's lazy web singletons do not read as a lazy-init change, and a file under a symlinked extension (bin/custom linked to a checkout) is matched to the context that loaded it through its target. Repeated unchanged saves do not rebuild the table. Multiple declarations for one DTO are rebuilt from fresh copies of their inline levelMapping maps, avoiding accumulated merges.

This requires the standard helper and mapping implementation and an existing fieldSetCache exposed by a Spring CacheManager. Custom helpers, custom mappings, referenced/inherited/programmatic maps or unavailable cache invalidation report restart guidance. Custom cache resolvers, asynchronous cache loading and arbitrary inherited/programmatic mapping configuration are outside this acceptance. Fields a DTO gains are the next section's. An XML property may have changed even when consumer refresh reports a restart requirement; follow that diagnostic.

WsDTO properties added in beans.xml

A property added to a data class and its WsDTO in *-beans.xml reaches OCC responses once the DTOs regenerate (autoCompile=true). Orika's mapping for the pair is refreshed with the added property; the mapper it generates is compiled against the class file the reload applied, because SAP's loader finds models.jar, which holds the class as it was at startup, before the directory the regen writes. The JAXB (EclipseLink MOXy) contexts the OCC message converters cached for a changed class, or for a response type that maps it, are dropped and built again on the next response, JSON and XML alike. ?fields= and field-set levels can then name the property. Removing it again makes ?fields= that name it answer as they would on a fresh server.

OCC request mappings and overrides

For SAP Commerce CommerceHandlerMapping, saving a controller rebuilds the override-priority table and bean-discovered handler registrations together. Existing or JVM-visible methods can change paths or @RequestMappingOverride annotations; removing or lowering the winning override makes the remaining eligible handler available. SDK API-version filtering remains in charge. Request lookups wait under the mapping registry's write lock while the complete registry is rebuilt. A changed priority property (priorityProperty, or the derived requestMappingOverride.<class>.<method>.priority) in local.properties rebuilds the registries whose handlers read it, so the handler it now favours answers without a restart.

On a registration failure, Reclazz attempts to restore the previous mappings and priority table and reports a restart requirement. This restores routing registrations, not the already redefined controller class or bean state. Missing reflection hooks or the registry lock cause refusal before registrations change.

Synthetic endpoints added through companion methods are outside this support. Such an attempt leaves the registry unchanged, reports SAP OCC mappings need a restart, and prevents further rebuilds of that registry until its context is replaced or the application restarts. Existing instance registrations, including generated endpoint adapters, also cause refusal. Acceptance covers SDK bean discovery; custom handler mapping lifecycle overrides and programmatic bean-name registrations are outside it. There is no new property-file hot reload: priority values must already be available through the application's configuration service. Ordinary Spring MVC retains its existing controller rescan and added-endpoint behavior.

SAP populator and list-merge directives

Edits to existing ModifyPopulatorList and ListMergeDirective XML definitions require restart. Changing a directive's setter does not rebuild its target list; replaying its initialization can retain obsolete entries or add duplicates. Reclazz rejects these edits before changing the live definition or directive and reports that the effective target lists were not refreshed. This includes property, ordering and depends-on edits, inherited definitions and subclasses. Lazy directives are not instantiated by the check, and unchanged definitions do not generate a warning.

Unrelated ordinary XML edits in the same resource can still apply. Newly added directives retain the existing bean-addition behavior; this diagnostic does not promise automatic list reconstruction or broaden support for new directives.

Refreshing a custom cache after a helper reload

A reloaded method body takes effect wherever it runs, but a value a bean already computed and kept in its own cache, a Guava cache or a plain field, does not recompute on its own. When the logic lives in a plain helper class that is not a Spring bean, editing the helper updates its code yet leaves that cached value as it was, because the agent has no way to know which bean's cache a given helper feeds and will not guess a cache field or empty every cache.

refreshOwnersOnHelper lets you say so explicitly. Map a helper class to the Spring singleton cache-owner beans it feeds:

text
-javaagent:reclazz-agent.jar=refreshOwnersOnHelper=com.acme.PriceHelper>com.acme.PriceCache;com.acme.QuoteCache

After com.acme.PriceHelper reloads, each listed owner is recreated through the same guarded lifecycle an ordinary bean reload uses, so its cache is rebuilt and the next read runs the fresh helper logic. Separate several mappings with |. An owner is resolved in the reloaded helper's own classloader, so the same class name in another context is never touched. Owners that are not recreatable singletons, FactoryBeans or event listeners are declined with a message rather than refreshed, and the feature is off until you set the mapping. It recreates the owner; externally held references to the old instance and in-flight work are the owner's own concern, the same as any bean reload.

XML singleton recreation

For instantiated ordinary beans, simple physical XML properties use Spring's write-method descriptor and generic method metadata. Admitted scalar conversions include strings, primitives/wrappers, Class, enum, decimal/integer numbers and URI; typed list/set/map compositions of admitted scalars use the live factory's standard SimpleTypeConverter. Generic inheritance uses the resolved target type, and setter selection does not execute a getter body. Registered editors and conversion services participate on admitted targets; their own side effects, like setter/resolver side effects, are not rolled back. A failed conversion or setter leaves the previous raw definition value intact, while independent supported properties can still apply.

Whole bean references compatible with the resolved runtime target retain their identity, including custom collection beans, without newly iterating/converting them. Compatible custom elements, existing arrays and raw/wildcard collection elements retain direct assignment; resolved generic element types are checked. New custom-object/array conversions, conversions of unresolved generic elements and lazy custom collection carriers are outside this support. New native conversion is limited to bounded literal compositions with standard JDK collection carriers. The new conversion route refuses unknown custom TypeConverter implementations. Resource/stream/path/URL target properties and unsupported explicit XML type conversions are refused before value resolution; explicit arrays of admitted scalar types retain their prior native resolution. This does not prevent application code in a supported resolver, editor, converter or setter from producing its own side effects.

Non-public unambiguous physical setters and proxies retain only the earlier primitive/string/compatible-value assignment boundary, with declared generic type validation; no proxy targets are unwrapped. Nested/indexed paths and logical companion-only setters are not newly supported. Uninstantiated lazy beans retain native deferred creation/validation, and the lifecycle, FactoryBean, merge and explicit singleton recreation guards below still apply.

Ordinary XML beans implementing Spring InitializingBean, DisposableBean, BeanPostProcessor or BeanFactoryPostProcessor require restart when their declarations change. Safety checks keep Spring's already-resolved merged class, or resolve its name through the live factory's application loader, and also inspect an existing singleton's type, so application classes invisible to the watcher and lifecycle interfaces hidden by a JDK proxy are not treated as ordinary mutable beans. Inherited lazy classes are resolved without initialization or bean creation; unavailable type metadata requires restart. The affected bean and definition remain unchanged, proven unchanged standard declarations stay quiet, and supported sibling edits can still apply. This does not replay lifecycle callbacks or extend the existing constructor/factory/explicit-lifecycle recreation rules below.

Existing ordinary XML FactoryBean definition changes require restart before factory properties or definition metadata are updated. A setter on the factory does not refresh Spring's cached product or references already held by consumers. The refusal also covers unwarmed/lazy factories and nonsingleton products: Reclazz does not infer that those products have no consumers. It does not query or create the product to make this decision. Proven unchanged standard definitions remain quiet, new definitions retain native creation, and independent ordinary bean updates can still apply. Product eviction, factory close and consumer rewiring are outside this support; constructor/factory/lifecycle and prototype guards retain their existing restrictions.

For an existing bean in a watched Spring XML resource, edits to constructor arguments, a public static or instance factory method, or explicit init-method and destroy-method metadata can recreate the singleton without restarting:

xml
<bean id="client" class="example.Client" init-method="start" destroy-method="stop">
    <constructor-arg value="30"/>
</bean>

Changing 30 to 60 replaces the definition and lets Spring create and initialize the new product. The old product receives its old destroy callback; the new definition controls subsequent destruction. Arguments and properties support scalar values, bean references and the live factory's placeholder resolution. Unchanged definitions do not recreate products. A lazy singleton that has not been created stays lazy. Ordinary setter-only edits retain the existing in-place path and bean identity.

Reclazz snapshots Spring's dependent graph before replacement. Supported plain dependents are recreated too. Writable direct fields in known singleton holders are updated by identity, including previously replaced plain holders still held by application code. These old holders are remembered weakly across saves. Their own state is not reset or globally copied from the new holder.

Preflight requires exact file ownership, the same concrete product type, stable class/factory-bean/alias metadata and an ordinary singleton definition. An instance factory must already be a local singleton; factory methods must be public and unambiguous with a concrete return type. Scalar/reference argument and property edits, factory-method selection and explicit init/destroy names are the allowed metadata changes. Scope, parent, autowire policy, depends-on, method overrides, custom suppliers, infrastructure metadata and other definition changes are refused.

New constructor/factory/lifecycle definitions and bean removal still require a restart. Recreated definitions cannot use nested beans or managed collections. FactoryBean, proxies, AutoCloseable, Thread, Executor, DataSource, Spring lifecycle interfaces and infrastructure types are excluded. Dependents must have plain definitions without constructor/factory/lifecycle metadata or lifecycle annotations. Known direct final fields and collection/map/array holders referencing replaced beans are refused before destruction. Local variables, static fields, nested object graphs, inaccessible fields and holders outside the owning factory are not repaired; a raw reference to the old target remains the old object.

If creation fails, Reclazz attempts to restore the previous definitions, recreate the affected instances and repair holders. Both the original failure and a failed restoration are reported. Fix the input or dependency and save again to retry. This is not rollback of constructor or callback side effects; restoration can run callbacks again. Spring retains its own callback-error handling. Replacement is not a transaction with concurrent requests, registry edits or external resources.

Verified with Spring 5.3.39 and SapMachine 21, including startup-agent reloads in ordinary and child classloaders, factory edits, callback counts and repeated saves. Multiple owning contexts resolve placeholders independently, using the same Spring classes. Other Spring versions, separate Spring installations, SAP Commerce runtime integration and a JDK 17 runtime have not been verified for this extension.

New component classes after startup

A new class with a direct or composed Spring component stereotype can register in a running context, using Spring's merged annotation names (including @AliasFor). Singleton and prototype scopes are resolved through Spring's own scope metadata resolver. A lazy singleton is instantiated on its first lookup; a prototype is instantiated on each lookup. Registration itself creates neither. An eager singleton still initializes immediately, and a failed eager creation removes its definition. On context close, Spring destroys created singletons; prototype cleanup remains the caller's responsibility.

Class-level @Profile and @Conditional, including composed annotations, now use the application's native Spring condition evaluator before registration. Profile expressions and default profiles follow the current environment. Custom conditions receive the live bean factory and definition registry, the application's captured classloader, and resource lookup through the context. Both parse and registration phases are checked, with Spring's condition ordering and short-circuit behavior. A false result records a filtered component; an evaluation error records a failure. Neither proceeds to registration or the ordinary reload path that would initialize the rejected class. Condition callbacks themselves are application code: their own side effects cannot be rolled back.

This is a registration-time decision against the current context, not a repeat of startup configuration processing. Custom scan filters, imports, auto-configuration ordering and re-evaluation of existing or previously filtered classes on later profile/property saves remain outside this support. Web/custom scopes and scoped proxies are still refused. Added factory methods have separate metadata support, including direct method-level @Conditional. The condition adapter uses an internal Spring API and refuses conditional registration if that API is unavailable or inaccessible.

The condition contract is tested on Spring 5.3.39 and 6.1.14. Real-agent tests on Spring 5.3.39 cover normal and child classloaders, class-file watching and automatic source compilation, including rejected classes' static initialization. Lazy/prototype identity and destruction are separately tested on both application classloaders.

Lifecycle methods added after startup

On a stock JDK, supported @PostConstruct and @PreDestroy methods added to a running Spring singleton now participate in its lifecycle, in the javax.annotation namespace on Spring 5 and the jakarta.annotation namespace on Spring 6. Verified with real Spring 5.3.39 and, on a separate dependency graph, real Spring 6.1 with jakarta annotations and a startup agent, including private callbacks and a resource field added in the same save.

The lifetime policy is per instance:

  • Adding init never runs it retroactively on the old bean. Normal singleton recreation destroys that bean, then injects the new one before invoking its added init callback. Existing annotation init callbacks run first; the added callback runs before InitializingBean.afterPropertiesSet and configured init.
  • A newly created instance retains the added destroy entry body associated with its initialization. Editing or removing the method/annotation affects future instances; it does not erase cleanup owed to an existing instance. A destroy callback added without an init callback also applies only to new instances.
  • Existing annotation destroy callbacks remain Spring's responsibility. Added destroy runs after those and before DisposableBean.destroy and configured destroy methods. Normal later bean recreation and context close use the same registration, with one callback invocation per instance.
  • An added init exception fails bean creation. Spring's existing refresh path has already destroyed the previous singleton; this is not a rollback to it. As with Spring annotation initialization, a failed init does not automatically receive destroy, so partially acquired resources need cleanup in the init's own failure path. An added destroy exception is reported and does not prevent remaining normal destruction callbacks or other beans from closing.

The supported added methods are direct, nonstatic, no-argument void methods, at most one added method per phase, with no other method annotations except @Deprecated. Init and destroy must be distinct methods. Existing reflected methods are not subject to these added-method restrictions. The bean must be an already initialized local singleton, have its exact declared runtime class and Object superclass, and not be a factory-method product or Spring infrastructure bean. The saved class may carry direct Spring component/service/repository/ controller/rest-controller stereotypes and @Deprecated; other class annotations are outside this subset. A callback also named as a configured init/destroy method is refused to avoid duplicate invocation. One standard, unmodified CommonAnnotationBeanPostProcessor using the standard JSR-250 annotations, in either the javax or the jakarta namespace, must be present.

Proxy, inherited, lazy/uninitialized, non-singleton, custom lifecycle processor, additional advice/annotation cases are not covered by this adapter. Unsupported additions are named as requiring a restart. Validation runs before singleton recreation and does not replace a previously installed callback plan on refusal. The compiled application code has already reloaded; this preflight is not a transaction over the entire reload.

Only the added callback entry body is captured. Methods it calls, field access, injected collaborators and the rest of the application keep their normal reload behavior; this does not freeze an object graph or guarantee cleanup after incompatible changes to resource state. Existing callback bodies also retain their existing live dispatch behavior.

Regression tests check exact callback order and instance identity through six saves, method/annotation removal and restoration, external bean recreation and context close. Real NIO channels must close, with zero left open. Annotation API 1.3.2 is a test-only dependency and is not included in the production agent.

Bean methods added after startup

On a stock JDK with the agent attached at startup, add a factory to an existing configuration class, compile, and look up the new singleton through Spring:

java
@Configuration(proxyBeanMethods = false)
public class ClientConfiguration {
    // Add this method after the application has started:
    @Bean
    public Client client(Transport transport) {
        return new Client(transport);
    }
}

The new method lives in the companion. A hidden supplier calls its current body, using the current configuration singleton for an instance method or a receiver-free static call for a static method. A Spring bean definition creates the product. Spring applies dependency injection, bean postprocessors and lifecycle callbacks. A later save recreates the added product, including body-only edits. Removing the method or its @Bean annotation removes its owned definition. Changing its name removes the previous owned name and registers the new one. Closing the context runs normal product destruction. Repeated saves leave one registration per supported name.

Definitions from the same save are registered before products are initialized, so an added product can inject another added bean regardless of declaration order, including through factory parameters. Only contexts containing the edited configuration participate.

Factory parameters can be required reference beans. Spring selects candidates by type, honors primary candidates and direct parameter @Qualifier("fast"), and uses the original parameter name to break ties when javac preserved it with -parameters or debug local-variable information. Without a preserved name, no name is invented: unique type/primary/qualifier selection still works, and an unresolved ambiguity is reported. Parent-context candidates and dependency proxies are passed through as the objects Spring resolves.

Concrete generic parameters also work: List<T>, Set<T>, Collection<T>, Map<String,T>, Optional<T>, and Spring ObjectProvider<T>/ObjectFactory<T>. One-dimensional reference arrays such as Transport[] are supported. Spring filters generic element types and qualifiers, orders lists/arrays, and supplies empty containers or an empty optional when no candidate exists. Providers keep Spring's lazy lookup semantics: creating the added product does not instantiate their target, and subsequent provider calls look up the current bean.

Direct parameter @Value uses the application's Spring value resolution and conversion, including all eight primitive types and explicit reference nulls:

java
@Bean
Client client(List<Transport> transports, Optional<Metrics> metrics,
              ObjectProvider<Token> tokens, @Value("${client.retries:3}") int retries) {
    return new Client(transports, metrics, tokens, retries);
}

Values are resolved when the product is created or recreated. Property-only edits can now recreate an existing unproxied singleton when its scalar @Value arguments change, after candidate validation; see the exact scope below. For shapes outside that scope, compile the configuration again to apply a new factory argument value.

Added factory methods can also carry direct @Primary and @Qualifier. For example, a method named remoteTransport carrying @Bean @Qualifier("fast") can supply a parameter declared as @Qualifier("fast") Transport transport: the qualifier value need not be the bean name and does not create an alias. With several candidates of the required type, @Primary selects the default; an explicit parameter qualifier first restricts the matching candidates. If several candidates match that qualifier, one primary candidate can break the tie. The default empty @Qualifier value is preserved, distinct from no qualifier.

Selection metadata is installed before any added products are initialized, so these rules also apply to providers added in the same save. Newly created ordinary Spring consumers can use it too. Moving, changing or removing either annotation and compiling again updates selection for newly created products. Missing matches or unresolved ambiguity are reported; correcting the annotations and compiling again recovers. Existing singleton holders are not globally re-injected.

Arguments are resolved on every product creation. Dependency names are registered with the creating bean factory so its normal dependency destruction applies, including for dependencies that were already cached singletons. If an argument is required but missing, ambiguous, or cannot be converted, the factory body is not called and its failed owned definition is removed. The message identifies the parameter and Spring's failure. Fix the dependency or qualifier and compile the configuration again to recover; registering a missing dependency alone does not restore a definition removed after a failed reload.

Static factories are supported too, including private methods and required bean arguments. For example, add this method to the supported configuration:

java
@Bean({"staticClient", "legacyStaticClient"})
private static Client staticClient(Transport transport) {
    return new Client(transport);
}

Static and instance factories share argument resolution, alias registration, selection metadata and product lifecycle. Registration still requires the configuration singleton described below; supporting a static method does not enable configuration-free registration or early infrastructure factories.

Supported factories carry direct @Bean and return a non-generic object (not a primitive, array or void). Private instance methods work with @Configuration(proxyBeanMethods=false); static methods can also be private. Native and abstract methods do not work. The class must carry direct @Configuration, extend only Object, implement no interfaces and have exactly one local configuration singleton in each affected bean factory. Lite configuration (proxyBeanMethods=false) requires an unproxied instance; default configuration requires Spring's direct enhanced subclass as described below. Extra runtime class annotations are limited to @Deprecated; methods additionally allow direct @Primary, @Qualifier, @Lazy, @Scope, @Profile, @Conditional and the three Boot condition annotations described below. A direct @Lazy makes the added bean lazy, so it is created on first access rather than when the save is applied; @Lazy(false) keeps it eager. A direct @Scope("prototype") gives every lookup a new instance; @Scope("singleton") is the default. A direct @Profile registers the bean only when the running environment accepts its expression, evaluated by Spring's own Environment, and a later save that changes the expression re-evaluates it, so a bean can join or leave on an edit. Web or custom scopes, scoped-proxy @Scope, advice, class-level primary/qualifier policies, other composed annotations and additional proxies require a restart. A prototype bean is not tracked for destruction by Spring, so its destroy method and close() are not called when the context closes; a lazy singleton is destroyed normally once it has been created.

A direct @Conditional(MyCondition.class) on a supported added factory uses Spring's native condition evaluator in the REGISTER_BEAN phase, just as a startup @Bean method does. Conditions receive native MethodMetadata read from the saved bytecode, with the original declaring class, method name, return type, static flag and annotations. This also selects the correct overloaded method; it does not reflect on a generated delegate. The condition context supplies the current environment and bean factory/registry, application classloader and context resource lookup. Classpath resources use the application's loader when the context has no explicit classloader for resource lookup; custom context protocol resolvers remain active. A direct @Profile on the same method participates in Spring's condition ordering and short-circuit evaluation.

Recompiling the configuration re-evaluates its added factories. As with other added-factory reloads, prior Reclazz-owned products and aliases are removed first. A false condition leaves no replacement definition or aliases; an evaluation error reports a failure and leaves that factory unregistered until a later successful save. Old products and condition callback side effects are not rolled back. Conditions see definitions already registered at that point in source order, not a completed dependency graph of all factories from the save. Property-only changes do not trigger this registration pass.

Direct Boot @ConditionalOnProperty, @ConditionalOnBean and @ConditionalOnMissingBean on these factories use the same native evaluation path. Boot reads the saved annotation attributes and live environment/bean registry; property prefixes, multiple names, havingValue and matchIfMissing retain Boot's semantics. Bean conditions can select a name/type, search the parent context, or infer a non-generic type from the saved factory return type. This does not require the added method to become visible through reflection. Existing lazy definitions participate in bean-presence checks without forcing their creation. A factory with @ConditionalOnMissingBean does not veto itself on a later save: its old owned definition is retired before the replacement is evaluated. Unrelated beans remain untouched. The three conditions can combine with each other and direct @Profile. Verified with Boot 2.7.18 / Spring 5.3.39, using native-startup comparisons and real agent reloads on lite/default configurations with normal/child classloaders.

Other Boot condition annotations, custom composed conditions, class-level conditions on the configuration, repeated configuration parsing and Boot auto-configuration ordering remain unsupported. Changes to unrelated beans do not automatically re-evaluate conditions; save the configuration again. Generic return deduction is outside the existing supported factory signatures. Boot 3 condition behavior has not been verified here. An unavailable Boot annotation or native condition API refuses the factory. The underlying direct @Conditional tests cover Spring 5.3.39 and 6.1.14; their real-agent activation/error/removal/restoration tests cover Spring 5.3.39 with lite/default configurations on normal and child classloaders.

Parameters allow direct @Qualifier and @Value. Primitive parameters without @Value, primitive/multidimensional arrays, raw generic parameters, wildcard or unresolved type variables, generic return types and generic factory methods remain unsupported. Collection/map implementations other than the interfaces listed above, maps with non-String keys, streams, suppliers, custom provider interfaces and JSR provider APIs are also unsupported. Extra runtime parameter/type annotations such as a parameter-level @Lazy, nullable annotations and composed qualifiers require a restart. Parameter names come from the saved bytecode; custom name-discovery policies are not used by this path. No-argument factories remain supported.

Default @Configuration (including explicit proxyBeanMethods=true) supports added instance factories that are neither private nor final, including the same supported parameters as lite/static factories. For example:

java
@Configuration
public class Clients {
    @Bean({"transport", "legacyTransport"})
    public Transport transport() { return new Transport(); }

    @Bean
    public Client client(Transport transport, @Value("${client.limit:5}") int limit) {
        return new Client(transport, limit);
    }

    public Client currentClient() { return client(null, 0); }
}

Container creation resolves the dependency and value arguments through Spring. A call to client(...) on the enhanced configuration resolves the container's singleton, including through an existing configuration reference, another instrumented application class or a method reference created in the edited configuration. If the singleton already exists, explicit arguments do not replace it. On first creation, non-null explicit arguments are used; if any argument is null, Spring's singleton-reference policy resolves all arguments instead. Thus client(null, 0) resolves both the transport and the configured limit. For a supported prototype factory, each call creates a new product using its explicit arguments, including null. Aliases still identify the same definition.

The container's supplier executes the direct method body; nested factory calls return through Spring. Supplied arguments are isolated between calls, threads and contexts, and a failed call cannot carry its arguments into the next lookup. A method reference created in the edited configuration, such as this::client, follows later saves even when retained. Removing the factory deletes its Spring registration; old retained callers follow the general removed-method policy and can run their captured Java body outside the container, which may predate the latest edit. Restoring the factory brings back its container reference route. Reflection, arbitrary method handles and method references created in other classes remain outside this added-factory scope. Spring tracks dependencies and rejects unresolvable creation cycles. Correcting a factory and saving again recovers. Static factory calls and manually constructed configurations retain ordinary Java semantics, including their supplied arguments; a direct static or manual-config call can create another object.

This supports argument passing through configuration-method calls. Explicit arguments passed directly to BeanFactory.getBean(name, args) are not forwarded to supplier-backed added factories; ordinary getBean(name) resolves their parameters. It does not add new supported parameter types or annotations, and it does not implement Spring's general circular-reference permissiveness. Full configuration must be Spring's direct enhanced subclass with its native bean factory binding; additional AOP proxies, user subclasses, inheritance and changing configuration mode at runtime are outside this scope. Existing lifecycle and replacement rules below still apply: a saved configuration can recreate existing products, so references to old products are not promised to stay current.

The default method name or explicit name/value names are supported, along with initMethod and destroyMethod. For @Bean({"transport", "legacyTransport"}), the first name identifies the bean definition and the remaining names are aliases for the same singleton. The method name is not another implicit alias. Aliases are available before added products initialize, including for a parameter such as @Qualifier("legacyTransport") Transport transport. Editing the names replaces the owned name/alias registrations; removing the method or its @Bean annotation removes them. Each product has one lifecycle regardless of how many names it has.

Every name must be nonblank, must not start with &, and must be distinct within the factory. Conflicting name and value arrays are refused. The default inferred close/shutdown destruction and an explicit empty destroy method are preserved. Other explicit @Bean options are refused. FactoryBean, bean postprocessors and bean factory postprocessors cannot be introduced through this path, including when hidden behind an Object return. Null results are refused.

An existing definition, singleton, local alias (even a dangling one) or parent bean blocks any requested name or alias; none of that factory's names are installed when a collision is found. Overlapping names/aliases across supported added factories in the same save refuse every affected factory. Before removal, Reclazz checks that the definition and any live singleton still belong to its registration; externally replaced registrations and their aliases are left alone. Alias cleanup removes only aliases registered by Reclazz that still point directly to the owned name. Externally retargeted aliases and externally added alias chains are preserved; they can remain dangling if their target was removed. Replacing an alias with an identical direct binding cannot be distinguished from leaving it unchanged. If current direct alias bindings cannot be inspected, aliased factories are refused before registration. Failed alias registration or product initialization cleans up the owned definition and its unchanged aliases. Registration is not a transaction with other threads concurrently changing the Spring registry. A product recreated by Spring from the owned definition remains reloadable, including a product wrapped by the application's existing bean postprocessors. Runtime changes to the postprocessor chain are outside this support.

Factories are created eagerly. Replacement/removal destroys the old product and may destroy its dependents through Spring. Existing holders, collections and local variables are not globally rewired by this feature; retrieve the current product from the context. Factory or initialization failure is reported and the failed owned definition is removed. The previous product may already have been destroyed, and callback side effects are not rolled back. Correct the factory and compile again to recover. The ordinary configuration-bean refresh still runs before this registration step, with its existing lifecycle effects. This feature does not rerun configuration parsing or add @Bean to methods that existed at startup.

The richer argument path is covered with Spring 5.3.39 on SapMachine 21, including real startup-agent reloads in both application and child classloaders. Those tests cover successive saves, same-save dependencies, lazy lookup, conversion failure, removal, recovery and destruction. Other Spring versions and a JDK 17 runtime have not been verified for this extension.

Exception handlers added after startup

On a stock JDK, add a handler to an already running plain controller or @ControllerAdvice / @RestControllerAdvice class and compile it:

java
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> invalidInput(IllegalArgumentException failure) {
    return ResponseEntity.status(422).body(failure.getMessage());
}

The next matching exception can reach this new method without restarting the context. Spring still selects the handler: local controller methods take priority over global advice, exception specificity and causes are matched by Spring, and advice order and scope selectors keep their normal role. An endpoint added by Reclazz is associated with its original controller for this lookup, so its local handlers and controller-specific advice apply too.

The adapter supplies saved handler metadata and delegates to the actual controller/advice instance chosen by Spring. Runtime method and parameter annotations, saved parameter names, concrete generic signatures such as ResponseEntity<String>, and class/method @ResponseStatus are available to Spring's normal argument and response pipeline. Both public and private handler methods are covered. Class-level response-body metadata is copied as well.

Later saves update the metadata and method body. Removing the method or its @ExceptionHandler annotation removes it from the next cache rebuild; restoring it makes it eligible again. The complete declared handler set is validated by Spring before publishing a metadata adapter, so ambiguous exception mappings are rejected. Rejection disables that class's added-handler adapter and reports the failure; handlers still visible on the original class remain available to the ordinary scan. This is not rollback of already reloaded method bodies. Exception-cache rescans also preserve the existing response-body advice list, so repeated handler saves do not accumulate advice registrations.

The supported scope uses the standard MVC exception resolver and plain controller/advice receivers. The adapter refuses proxy/subclass receivers instead of unwrapping them and bypassing advice. Controller class/interface inheritance, class or method type variables, static/abstract/native/bridge handlers, composed annotations and extra class/method advice annotations are outside this path. Accepted class metadata is the direct MVC controller/advice stereotypes, @Component, @RequestMapping, @ResponseBody, @ResponseStatus, @Order and @Deprecated; handler metadata is direct @ExceptionHandler, @ResponseBody, @ResponseStatus and @Deprecated. Declared asynchronous, reactive and streaming return types are refused. Async behavior hidden behind an Object return or performed inside a body, scoped/prototype configurations, custom resolver subclasses and changes to advice class ordering/selectors were not validated here. Newly added @InitBinder / @ModelAttribute methods use the separate adapter described below.

Spring sees a hidden metadata class for an adapted handler: its reflective declaring/containing class is not the original controller. Custom argument, return-value or response-advice code relying on that exact class identity is not covered. Original application reflection still cannot find the added method. Ordinary bean recreation and lifecycle callbacks remain in effect; this feature does not preserve destroyed bean resources or make metadata publication atomic with concurrent requests.

Current P3b evidence runs on stock SapMachine JDK 17 and 21 with Spring Framework 5.3.39 and 6.1.14. Real-agent tests send loopback HTTP through MockMvc/DispatcherServlet, checking local/advice handlers, existing/new endpoints, selectors and local priority across five saves and normal/ child loaders. This is an HTTP bridge, not a deployed servlet-container test. Start with -javaagent before MVC loads; late attach is unverified.

Spring Framework 6.1.14 uses a constructor-reference Function for the local exception resolver. Reclazz adapts its input to the saved metadata while retaining Spring's factory, cache and handler selection. The exact measured factory shape is checked; an unknown resolver shape refuses the hook instead of partially installing it. The Spring6 repair evidence uses the same four local/advice and normal/child-loader HTTP observers as Spring5. The original P3b failure is retained as historical evidence. Other Spring versions and native servlet-container exception-handler deployment remain unverified. The modern watcher observations cover synchronous ResponseEntity<String> status/body selection; the separate component-only cause/request-argument and response-annotation cells remain limited to their recorded Spring5 observations. No production dependency is added.

Registered MVC interceptors: current singleton routing

Method-body saves on registered MVC interceptors still use Spring's native request chain. For named plain singleton beans with local definitions, completed class reloads also route that chain to the current recreated interceptor and its current dependencies. If native dependency destruction recreates a handler mapping, the supported DispatcherServlet request lookup and public mapping view select the current mapping. Existing native registrations retain their order and MappedInterceptor include/exclude matching; registrations are not rebuilt.

The current P3b repair evidence measures Spring5.3.39 and6.1.14 on stock SapMachine17/21, parent/web contexts and normal/child application loaders. Six watcher cases per Spring graph cover lookup, mapped and injected registration, repeated interceptor/helper saves, current dispatched bean identity and constructor-owned dependency values, ordered pre/post/completion callbacks, unrelated identities and source-only no-op controls. Requests use MockMvc/DispatcherServlet; native servlet-container deployment is unverified. The old stale-dependency/mapping observation remains historical evidence.

Only independently owned instances are certified; receivers shared between definitions or contexts remain unverified. Start with -javaagent before MVC loads and publish compiled class files. The adapter accepts only its measured native bytecode sites; unsupported routing records a restart concern. Prototype/scoped beans, FactoryBean products, proxies, unowned programmatic receivers, custom strategy/chain overrides, servlet filters/security policies, late attach and unlisted versions/OS are outside this claim. A class batch publishes its final identities after native bean recreation/cascade/healing. This does not make bean destruction atomic with concurrent/in-flight requests; the existing opt-in request-boundary policy remains separate. Changes to registration declarations, order, path-filter configuration or chain structure are not certified by this repair. No dependency or new agent option is added.

Binding and model methods added after startup

A supported plain controller or controller advice can gain entirely new @InitBinder and @ModelAttribute methods on a stock JDK. For example, compile these additions to a running @RestController:

java
@InitBinder("name")
public void trimName(WebDataBinder binder) {
    binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}

@ModelAttribute("greeting")
public String greeting() {
    return "Hello";
}

Spring's normal binder and model pipeline sees the saved callback metadata. Binder names still restrict which arguments/objects a binder affects. Global callbacks run before local ones; advice ordering and controller selectors are applied by Spring. Existing model callbacks stay in the scan, and model attribute dependencies, return values and void methods that populate Model use Spring's normal behavior. Model-annotated request mappings are excluded from initialization, as in Spring's own method filter.

Public and private callbacks delegate to the actual controller/advice bean. Method/parameter annotations, MethodParameters names and concrete generic signatures are copied. Use explicit annotation argument names (for example @ModelAttribute("base")) or compile with -parameters when names are needed; local-variable-table parameter-name discovery is not reproduced. Existing endpoints and Reclazz-added endpoints share the original controller's callbacks and advice scope. Later saves can edit, remove, restore or remove the annotation from an added callback. Cache rebuilding uses the existing advice reloader.

The scope is plain receivers whose class directly extends Object without interfaces or type variables. Supported class metadata is direct MVC controller/advice stereotypes, @Component, @RequestMapping, @ResponseBody, @ResponseStatus, @Order and @Deprecated; callback annotations are direct @InitBinder, @ModelAttribute and @Deprecated. Extra callback advice annotations, static/abstract/native/bridge/synthetic callbacks, type-variable signatures, non-void binders and declared async/reactive/streaming model returns are refused. Proxy/subclass receivers are refused at invocation, so callback bodies do not bypass their advice. @SessionAttributes, composed metadata, custom adapter subclasses, scoped/prototype beans, changes to advice class selectors/order and asynchronous request behavior are outside this support.

A rejected publication clears that class's added-callback adapter; methods still visible on the original class remain available to Spring. This does not roll back already reloaded bodies. The hidden metadata class differs from the original controller's reflective declaring/containing class, so custom argument resolvers relying on exact class identity are not covered. Normal bean recreation/lifecycle effects remain; this does not guarantee atomic metadata publication with concurrent requests. Added methods remain absent from ordinary application reflection.

Current P3b evidence exercises Spring Framework 5.3.39 and 6.1.14 on stock SapMachine JDK 17 and 21. Real-agent loopback HTTP bridges into MockMvc/DispatcherServlet observe local/advice binders and model callbacks on old/new endpoints across five saves and normal/child loaders. These bridges do not certify a deployed servlet container. Void/generic model-return variants and refusal shapes have separately labelled component-only observations; they are not watcher acceptance for those variants. Start with -javaagent before MVC loads. Late attach, other Spring versions and native servlet-container deployment remain unverified. No production dependency is added.

Kafka listeners added after startup

Current broker observations, component-only contracts and unverified variants are listed separately in P4 messaging evidence.

With Spring Kafka already configured, compile a new listener method on a running plain singleton @Component / @Service / @Repository:

java
@KafkaListener(id = "orders-created", topics = "orders", groupId = "orders-service")
public void onOrder(String payload) {
    processOrder(payload);
}

The saved metadata is passed to the application's Kafka listener processor and container factory. Spring performs message conversion and argument resolution; @Payload, @Header, @Headers and concrete generic parameter signatures such as ConsumerRecord<Integer, String> are carried. Public and private void instance methods with one or more arguments are supported. Calls delegate to the current plain singleton; a missing or proxied replacement throws instead of silently treating the message as handled.

Unsupported Kafka infrastructure does not block unrelated bean reloads. Before the edited bean is recreated, Reclazz identifies its standard Kafka record adapters, requests consumer stop and waits for the stop callbacks before unregistering their container IDs. Normal Spring initialization can then re-register existing methods without ID collisions. The new methods register afterward. Later saves update bodies/topics, remove listeners or annotations, and restore them. Unrelated containers retain their identity. A duplicate ID belonging to another container is refused. A failed registration attempt cleans up the containers it created; failures are reported in the restart ledger.

The supported addition uses a literal nonempty id, and topics that are each a literal, ${property} placeholder or #{SpEL} expression resolved by the application's native processor. Topic expressions may return a String, String array or iterable of strings; groupId expressions must resolve to a String (native null/default behavior is retained). Optional literal containerFactory, idIsGroup, autoStartup and positive integer concurrency are accepted. Measured Kafka2.9.13 late registration follows the native registry policy: autoStartup=false can still start when alwaysStartAfterRefresh=true (the native default); setting that registry policy to false preserves the annotation's start flag. Omitted groupId and #{null} fall back to the literal listener ID when idIsGroup=true, otherwise to the configured consumer-factory group. An explicit nonempty group overrides that choice. Exact groupId="" now follows the same native fallback after watched addition and body/topic edits, with both idIsGroup values measured against ordinary native listeners. Whitespace-only group literals remain unsupported; this proof applies to the named Kafka2.9.13/Spring5.3.39 and JDK17/21 fixture. Other explicitly supplied @KafkaListener options are refused. The class directly extends Object, has no interfaces/type variables and carries only the component stereotypes above or @Deprecated. Callback annotations are direct @KafkaListener and optional @Deprecated; extra advice annotations, non-void/static/abstract/native/bridge/synthetic methods and type-variable signatures are refused. Parameter names require explicit annotation names or compilation with -parameters.

topics = "#{__listener.topics}" and groupId = "#{__listener.group}" read properties on the real listener singleton, while messages still invoke the saved callback through its delegate. Named application beans and property/expression combinations also use the native resolver. The temporary listener binding is restored after registration, including expression errors. Custom beanRef options remain unsupported. A later listener save re-evaluates current topic/group values; property or bean changes alone do not reroute an existing consumer. Expression syntax/type errors occur during registration after old consumers have retired. Partial additions are removed and a valid save retries, but old consumers and user expression side effects are not rolled back. A different group may replay retained messages according to the application's offset policy.

Use the standard KafkaListenerAnnotationBeanPostProcessor, KafkaListenerEndpointRegistry and ConcurrentKafkaListenerContainerFactory in record mode. Added listeners refuse annotation enhancers, retry-topic configuration, filtered/retry-template/batch factories and container customizers. Proxy/subclass receivers are not unwrapped past advice. This scope does not cover class-level or repeatable listeners, expressions/placeholders in other listener attributes, container groups, arbitrary wrapper/container types, scoped/prototype beans or sharing one registry between multiple contexts.

If consumer stop does not complete within 30 seconds, container entries remain registered and this class's bean refresh/framework follow-up is deferred with a diagnostic. Already reloaded method bodies are not rolled back. The direct-listener coordination here runs through the Spring orchestrator. A separately bounded Kafka helper/property transaction recreates one plain local leaf listener after helper-body or scalar constructor property saves, including a same-key scalar on the listener constructor. It is measured on Framework5.3.39/Kafka2.9.13 and Framework6.1.14/Kafka3.2.4, JDK17/21, with nontransactional RECORD and native-default BATCH acknowledgment. BATCH acknowledgment is distinct from batch-listener callbacks; other reconstruction paths and broader closures remain outside this transaction. Existing Spring bean recreation/lifecycle effects remain. The hidden method's declaring class differs from the original, so custom conversion or argument factories depending on exact declaring-class identity are outside the verified scope. This is not an atomic reload transaction or a Kafka exactly-once delivery guarantee; application acknowledgments, retries and offset policy continue to determine redelivery.

Verified with Spring Kafka 2.9.13, Spring Framework 5.3.39 and a real embedded Kafka 3.2.3 broker on stock JDK 17 and 21 on macOS arm64. Real-agent tests cover both the first listener on an already used bean and addition beside an existing listener, unique test message delivery across six saves, topic changes, retired consumer state, unrelated container identity and context close. Expression tests on normal and child classloaders cover self-referencing topics/groups, expression failure, multiple-topic recovery, group replay and removal. Native startup comparison also covers named beans and property/expression arrays. Start with -javaagent before application classes load. Beyond the named legacy and modern lanes, other broker/framework versions, external clusters, transactions and rebalance/failure delivery guarantees were not exercised. Kafka libraries and the embedded broker are test dependencies only. Beans mixing Rabbit, JMS or Kafka listeners require a restart before any of their consumers are retired.

Modern compatibility is also measured with Spring Kafka 3.2.4, Spring Framework 6.1.14 and an embedded Kafka 3.7.1 broker on stock JDK17/21. This exact version uses native endpoint-ID preparation before registration and no longer has the factory retryTemplate field. Added listeners follow six saves, including topic changes, private restoration, removal, retired consumers and unrelated container identity. Native startup controls precede each watcher observer. The factory absence exception and endpoint-ID preparation apply only to package version 3.2.4; this does not certify every Kafka 3.x release. Filtered, batch and custom factories retain their restart requirements. Native owner-expression controls verify context separation, prior-binding restoration and failed-registration cleanup. Modern Kafka reply, transactions, rebalance, late attach, hosted clusters and direct child-loader watcher behavior remain outside this measured scope.

JMS listeners added after startup

Current broker observations, component-only contracts and unverified variants are listed separately in P4 messaging evidence.

With Spring JMS already configured, add a listener to a running plain singleton @Component, @Service or @Repository and compile:

java
@JmsListener(id = "orders-created", destination = "orders")
public void onOrder(@Payload String payload, @Header("tenant") String tenant) {
    processOrder(tenant, payload);
}

Spring's actual listener processor and container factory register the saved metadata. Public/private instance methods with arguments may declare void, String, byte[], java.util.Map, javax.jms.Message or org.springframework.messaging.Message as their return type. Spring resolves the payload, headers and JMS message/session arguments. Calls reach the current plain singleton; a missing/proxied replacement throws rather than silently consuming a message. Later saves can edit bodies or queue destinations, remove a method or its annotation, and restore it. Original listeners on the same bean are registered again after recreation. Containers belonging to unrelated beans keep their identity.

For request/reply, return a supported value and optionally declare a default reply queue directly on the method:

java
@JmsListener(id = "order-status", destination = "order-status-requests")
@SendTo("${order.status.replies}")
public String status(String orderId) {
    return findOrderStatus(orderId);
}

Spring's native adapter converts the result and sends the reply. The request's JMSReplyTo takes precedence over @SendTo; without @SendTo, the caller must supply JMSReplyTo for a non-null result. Reply correlation uses the incoming JMSCorrelationID, or its JMSMessageID when no correlation ID was supplied. A null result sends no reply. Direct method @SendTo accepts one destination, including an application-resolved ${property} placeholder. Later class saves can change it; property-only reply-route refresh is outside this scope. The application's converter remains responsible for payload support, including Map entries and Spring Message payloads. Arbitrary DTO/Object, concrete message subtype declarations, primitive, Future and reactive return types are excluded. Class-level or composed @SendTo is not supported.

Use a literal nonempty id and a destination that is a literal or a ${property} placeholder resolved by the application's own value resolver (a #{SpEL} expression stays out of scope). An optional selector may also be a literal or ${property} placeholder; optional literal containerFactory and positive concurrency (a number or ascending range) are supported. The class must directly extend Object, have no interfaces/type variables, and carry only the stereotypes above or @Deprecated. Added callback annotations are direct @JmsListener, optional direct @SendTo and @Deprecated; parameter annotations are @Payload, @Header and @Headers. Extra advice, static/synthetic methods, type-variable signatures and other explicitly supplied listener options are refused. Parameter names require explicit annotation names or compilation with -parameters.

The verified infrastructure is the standard JmsListenerAnnotationBeanPostProcessor, JmsListenerEndpointRegistry, DefaultJmsListenerContainerFactory and DefaultMessageListenerContainer with Spring's MessagingMessageListenerAdapter. The application factory retains its conversion, selector and local session transaction settings. Added listeners refuse custom factories/executors, external transaction managers and topic/durable/shared subscription factories. Proxies and subclass receivers are not unwrapped. Repeatable/composed listeners, scoped/prototype beans and registries shared between contexts are outside this scope. Custom argument factories that depend on the exact original declaring class are not verified: the added method is reflected on a hidden metadata delegate.

Before singleton recreation, Reclazz identifies owned containers and waits for complete destruction, including their consumers, sessions and connection. Spring 5.3 has no registry unregister API; matching entries are removed under the registry's registration lock only after destruction finishes. A duplicate ID on another container is refused. Failed registration cleans up containers registered by that attempt. The direct-listener preflight leaves unrelated bean reloads on their ordinary path when optional JMS infrastructure is unsupported. The indirect helper transaction conservatively refuses uninspectable JMS ownership. A bean mixing Kafka and JMS consumers, or a JMS bean with uninspectable Kafka ownership, is deferred before shutdown.

Shutdown waits up to 30 seconds. On timeout or interruption, registry ownership is retained and bean recreation/framework follow-up is deferred with a restart ledger diagnostic. Destruction continues in the background; a later save waits for that same operation before releasing the ID. Retained entries may therefore refer to already stopped containers until retry or application restart. Already reloaded bodies are not rolled back. This coordination covers direct class reloads through the Spring orchestrator. A separately bounded helper/property JMS transaction is described in constructor dependency refresh; other indirect recreation paths remain outside scope. The indirect transaction refuses a still-retiring consumer on a later save and requires restart, rather than using the direct path's same-operation retry. It does not make a reload atomic or promise global exactly-once delivery. Broker and application acknowledgment/redelivery policies still apply.

Verified with Spring JMS 5.3.39 (javax.jms), a real embedded ActiveMQ Classic 5.19.11 broker, and stock JDK 17 and 21 on macOS arm64 using startup -javaagent. Tests cover the first listener on an already used bean, addition beside an original listener, six saves, payload/header/message conversion, local transaction rollback/redelivery, selector filtering, consumer retirement, and context close. A deliberately blocked real consumer proves timeout/interruption and retry using a shorter internal test wait. Request/reply tests cover native Spring controls, text/bytes/ map/native and Spring messages, correlation, JMSReplyTo priority, null results, rollback followed by a reply on redelivery, and invalid @SendTo recovery. Actual-agent reply tests cover normal/child class loaders, repeated saves, private restoration and reply queue changes. Other JMS providers and Spring versions, late attach, XA and durable topics were not exercised in that legacy lane.

A separate modern lane measures Spring JMS 6.1.14, Jakarta Messaging 3.1.0 and ActiveMQ Classic 6.1.3 on stock JDK17/21. Added callbacks returning a native jakarta.jms.Message, String or void follow six saves. Native-message and String reply tests observe actual replies, correlation, JMSReplyTo priority, rollback/redelivery, private restoration and retired containers on normal and child classloaders; void delivery also runs on both loader shapes. Each watcher observer first checks fresh agentless native delivery. Unsupported return/advice shapes remain refused. The existing javax.jms.Message support is retained. This does not establish XA, durable/shared topics, other JMS providers or property/dependency-cascade consumer recreation.

Spring JMS and ActiveMQ are isolated test dependencies. Existing tests retain their Jackson/SLF4J versions. :agent:unitTest, :agent:e2eTest and :agent:test also run the matching isolated JMS suites; those results have separate XML reports under jmsUnitTest, jmsE2eTest and jmsTest. Run just JMS with ./gradlew :agent:jmsTest --rerun. No JMS/broker classes enter the production JAR.

Rabbit listeners added after startup

Current broker observations, component-only contracts and unverified variants are listed separately in P4 messaging evidence.

With Spring Rabbit already configured, add a method to a running plain singleton @Component, @Service or @Repository, then compile:

java
@RabbitListener(id = "orders-created", queues = "orders")
public void onOrder(@Payload String payload, @Header("tenant") String tenant) {
    processOrder(tenant, payload);
}

The application’s real processor and SimpleRabbitListenerContainerFactory register the saved metadata. Spring converts payloads/headers and supplies the AMQP message. The callback uses the current plain singleton; missing or proxied receivers fail explicitly. Public/private instance void methods with arguments are supported. Later saves can edit bodies and queues, remove methods or their annotations, and restore them. Original listeners are registered again after bean recreation; unrelated containers retain their identity.

For newly added methods, use an explicit nonempty literal id and queues that are each a literal, ${property} placeholder or #{SpEL} expression resolved by the application's own Spring processor. Expressions may produce queue names, Spring AMQP Queue objects, String arrays or iterables of those supported values, naming existing queues. For example, queues = "#{@routing.queues}" reads an application bean's queue list; queues = "#{'${orders.queues}'.split(',')}" combines a property with an expression. Saving the listener again resolves the current values and replaces its consumers. Changing a routing bean or property alone does not move existing consumers. Resolution errors leave the failed added registration absent and clean up partial registrations from that pass; unrelated container IDs remain. A later valid save retries. Old containers and expression side effects are not rolled back. Optional literal containerFactory and positive concurrency (number or ascending range) are accepted. Explicit autoStartup is outside this stage: Spring's late registration can start a container even when that flag is false. The class directly extends Object, has no interfaces/type variables, and uses only the stereotypes above and optional @Deprecated. Callback annotations are direct @RabbitListener and optional @Deprecated; parameter annotations are @Payload, @Header and @Headers. Composed/repeatable/class-level handlers, expression IDs/factories/concurrency, inline queue/exchange/binding declarations, return/reply/async methods, proxies, method advice and inheritance are outside this support.

Existing methods are re-scanned by Spring and do not use the added-method option allowlist: an existing @RabbitListener(queues="orders") keeps working with Spring-generated IDs, including beside a new listener. Existing ackMode="AUTO" and autoStartup metadata also stay on Spring's ordinary registration path. Spring's late registration may start an existing autoStartup="false" container; Reclazz preserves that framework behavior.

Both original and added consumers require the standard RabbitListenerAnnotationBeanPostProcessor, RabbitListenerEndpointRegistry, MessagingMessageListenerAdapter, a plain receiver and SimpleMessageListenerContainer, with AUTO acknowledgment and the default SimpleAsyncTaskExecutor. Newly added consumers additionally require the standard simple factory without a customizer. Local channel transactions are preserved. Manual/automatic-without-ack modes, external transaction managers, container advice, custom executors, direct/stream/batch containers and custom listener adapters are refused for retirement of either original or added consumers. Unsupported static metadata shapes, foreign added IDs and mixed Rabbit/JMS/Kafka owners are checked before owned consumers are retired. Queue expression parsing and evaluation occur later during native registration, after retirement; an expression failure does not restore the old consumers. Original consumers are checked against the runtime lifecycle restrictions above, not the added-method metadata restrictions. Unknown broker ownership also requires a restart. Unsupported Rabbit infrastructure does not block unrelated beans.

Start with -javaagent. The agent tracks each actual simple-container worker from construction through its complete run() exit, including queued tasks and channel/transaction cleanup. Before recreation it prevents new workers on the retired container, starts destruction, and waits up to 30 seconds for both destruction and worker completion. A zero Spring consumer count alone is not accepted: channel shutdown can release that counter while a callback still runs. An unavailable startup hook, timeout or interruption retains registry ownership, defers bean recreation and records a restart diagnostic. A later save waits for the same retirement; it may find stopped containers still holding their IDs.

This direct-listener path runs through the Spring orchestrator. A separately bounded Rabbit helper/property transaction recreates one plain local leaf listener after helper-body or scalar constructor property saves, including a same-key scalar on the listener constructor. That indirect path is measured on Framework5.3.39/AMQP2.4.17 and Framework6.1.14/AMQP3.1.7, JDK17/21, with native default AUTO acknowledgment and nontransacted channels. It does not extend the direct path's local-channel transaction support to helper/property reconstruction. Already reloaded bodies are not rolled back, and this does not make reload atomic or promise exactly-once delivery. Broker/application acknowledgment and redelivery policies still apply. Custom conversion depending on the callback’s exact declaring-class identity is outside the verified scope because added callbacks use a hidden delegate.

Verified locally with Spring Rabbit 2.4.17, Spring Framework 5.3.39, RabbitMQ 4.3.5 and stock JDK 17 and 21 on macOS arm64. Real-agent tests cover six saves both on an already used bean with no listeners and beside both explicitly named and ID-less original listeners, including payload/header conversion, rollback/redelivery and context close. Queue-expression tests on normal and child classloaders cover bean-driven rerouting, invalid result types, recovery to multiple queues and removal, with real message delivery. Native annotation comparison also covers Queue values, mixed lists and property/expression composition. A blocked real original callback checks force-close, timeout, repeat, interruption and retry. Beyond the named legacy and modern lanes, other framework/broker versions and late attach were not verified locally. Linux CI is configured for JDK 17/21; those runs are separate evidence.

A separate modern lane measures Spring Rabbit 3.1.7 with Spring Framework 6.1.14 and the same pinned RabbitMQ4.3.5 broker on stock JDK17/21. Direct first, existing and ID-less listener additions follow the six-save delivery/retirement sequence, with fresh agentless public/private/routing controls before each watcher observer. This is evidence of existing behavior and requires no Rabbit runtime repair. Modern Rabbit reply/async, direct child-loader watcher behavior and property/dependency-cascade consumer recreation are not established here.

Spring Rabbit is a test dependency; no AMQP/framework/broker classes ship in the agent. The :agent:rabbitTest suite requires Docker with Linux containers and a pre-pulled official image. Prepare and run it with:

bash
docker pull rabbitmq@sha256:3486d98205df3d6395ed70e7924baa13b561cbac54116c0ddae5b0b7382bbabd
./gradlew :agent:rabbitTest --rerun

Tests create uniquely named temporary containers, bind random loopback ports and remove their own resources on completion. Missing Docker/image fails the suite. :agent:test and :agent:e2eTest include it and report results separately under agent/build/test-results/rabbitTest. Count these alongside test and jmsTest for the full gate. Portable Rabbit checks run in :agent:unitTest; the blocked broker test belongs to rabbitTest. Windows CI explicitly omits only this Linux broker suite with -Preclazz.skipRabbitBroker=true, retaining portable tests. That option reports NOT RUN and is not a complete broker validation.

Operations on added service methods

On a stock JDK, add a public instance method to a running singleton service and compile it together with its caller:

java
@Transactional(rollbackFor = Exception.class)
public void submit(int id) throws Exception {
    jdbc.update("insert into orders (id) values (?)", id);
    if (!accepted(id)) throw new Exception("order rejected");
}

@Cacheable(cacheNames = "orders", key = "#p0")
public Order load(int id) {
    return repository.find(id);
}

Calls from other watched application classes now run the application's real Spring transaction/cache interceptors around the added companion method. This covers direct @Transactional, @Cacheable, @CachePut, @CacheEvict and @Caching metadata, plus class-level transaction settings and @CacheConfig. Ordinary service methods also accept composed transaction and composed cache annotations. Spring owns manager selection, rollback rules, cache keys, conditions, unless and advisor ordering. Saved parameter names (when compiled with -parameters) and indexed arguments such as #p0 are available. A configured key generator receives the actual target and saved method metadata.

Both a direct singleton gaining its first operation method and an existing supported CGLIB proxy are covered. A call through an old proxy reaches its real target, rather than running against the proxy object's fields. Same-class self-invocation retains its existing bypass of Spring advice. Repeated saves replace metadata and bodies. Removing an operation method makes its retained external call sites fail explicitly; restoring it makes those sites usable again. Removing only its annotation removes that interception. Application cache values retain normal region/key semantics; metadata refresh does not by itself flush their values.

The ordinary service-bean refresh still applies: editing a @Service can replace the registered bean and run its lifecycle callbacks. The adapter captures old singleton identities before that refresh and discovers the replacement for new calls. It does not undo destruction callbacks or guarantee that resources on a destroyed old target remain usable. Factory/programmatically registered classes without a Spring stereotype retain their existing bean-refresh behavior.

Supported calls require a captured live singleton, a direct target or mutable CGLIB proxy with SingletonTargetSource, and one standard Spring InfrastructureAdvisorAutoProxyCreator or AnnotationAwareAspectJAutoProxyCreator. The configured candidate/proxy advisor sets must contain only the standard transaction/cache advisors, interceptors and annotation sources, plus the classic or modern security infrastructure described below. Additional aspect or custom advisors are refused, including candidate advisors that have not been attached to this proxy. Missing infrastructure, unknown receivers and unsupported metadata also cause an explicit exception before the operation body runs. No unmanaged/prototype instance is assumed to be a singleton merely because its class matches one.

This does not add interface methods to existing JDK proxies, add reflective methods to the original class, or cover new overrides of already-resolvable inherited methods. Static/private/final methods, final service classes, generic metadata, composed annotations outside the transaction/cache/security scopes below, additional advice annotations, scoped/dynamic/opaque/nested proxies and reactive return types are outside this path. Future-returning transaction/cache operations require the direct async service scope below; without @Async, those advised Future operations remain unsupported. Existing methods follow their existing dispatch; this boundary is installed only for external calls whose method is absent from the loaded class. Framework reflection calls, method references/lambdas, and async values hidden behind a declared Object result are not validated by this feature.

Spring sees a separate metadata Method: its name, parameters and annotations are supplied, but its declaring class is a collectible hidden metadata class. Custom code depending on the exact declaring class is not given the original class's reflective identity. Metadata caches are cleared on saves to release old metadata. Bean/context references kept by the adapter are weak. Reloading is not an atomic transaction with concurrent application calls; this feature does not add a concurrent consistency guarantee.

Verified with Spring Framework 5.3.39, H2 2.2.224 and a stock JDK 21: real database commit/checked-exception rollback, cache hit/put/evict, combined transaction/cache, parameter names, custom key generation, annotation changes, method removal and restoration, and old/new bean references. Spring 6 transaction/cache advice combined with authorization is verified in the next section; a JDK 17 runtime was not exercised. Spring JDBC/TX and H2 are test-only dependencies of Reclazz and are absent from the production agent.

Authorization on added service methods

Direct @PreAuthorize and @PostAuthorize, and supported composed security markers on added ordinary service methods, use the application's native security interceptors. Verified configurations:

  • Spring 5.3.39 / Security 5.7.11 with @EnableGlobalMethodSecurity(prePostEnabled=true, proxyTargetClass=true).
  • Spring 6.1.14 / Security 6.3.4 with @EnableMethodSecurity(proxyTargetClass=true) and its default pre/post support. Native AuthorizationManager interception, lazy advisor wrappers and standard Micrometer authorization observations are preserved.

Supported class defaults and method overrides follow Spring's precedence. Saved parameter names compiled with -parameters, indexed arguments such as #p0, authentication, the actual target (this) and returnObject are available to native expressions.

A denied @PreAuthorize call never enters the added body. @PostAuthorize checks its result after the body runs; it does not prevent that body's side effects. The native authentication context and exception types are preserved. An already held singleton/CGLIB reference follows subsequent policy/body changes, annotation removal, method removal and restoration. Unrelated existing methods retain their normal Spring interception. Same-class self-invocation retains its existing advice bypass; this feature is an external added-method boundary.

The transaction/cache singleton and proxy restrictions above still apply. For security, the service must directly extend Object without interfaces. Classic security requires the standard shared pre/post annotation source, affirmative decision manager/voters and default expression handlers. Modern security requires the native pre/post managers, standard pointcuts and expression handlers; a method requiring both policies must have a matching advisor for each. Missing one cannot be treated as a public operation. Native permitAll() keeps its lazy authentication semantics. Disabled/missing infrastructure, custom sources/managers, expression-handler subclasses or extra advice are refused before the body runs. The application's advisor order is preserved: when combining authorization and caching, configure security before cache advice if cached results must be checked on every call. The classic regression uses orders 0/1/2; the modern regression uses native pre-authorization order 200, transactions 300 and caching 400. Reclazz does not reorder the application's security policy.

This scope excludes @Secured/JSR-250 annotations on added methods, @PreFilter/@PostFilter, @AuthorizeReturnObject, custom denial handlers, async methods and framework callbacks. Modern default filter/result advisors may remain present only when they do not match the added call; matching unsupported advice is refused. Other Security versions/configurations were not validated. Unsupported security annotations refuse rather than silently becoming public; added event/scheduled callbacks also refuse class-level security, including composed annotations. Original-class reflection still cannot discover added methods, and custom code that needs the original declaring Method is outside this scope.

Composed security annotations on added services

An application can keep a fixed authorization rule in a runtime marker annotation and use it on a newly added ordinary service method:

java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@PreAuthorize("hasAuthority('ADMIN')")
public @interface AdminOnly { }

@AdminOnly
public String administrativeReport(String account) {
    return buildReport(account);
}

Supported markers declare no attributes and contain only @PreAuthorize, @PostAuthorize, nested supported markers and Java annotation metadata (@Target, @Retention, @Documented, @Inherited, @Deprecated). One marker can carry both pre- and post-authorization. Method and class policies keep native Spring precedence. A repeated pre or post policy on the same declaration, including through different nested paths, is refused. Attribute-bearing annotations, @AliasFor parameters, expression templates, mixed advice/framework markers, unknown markers and cycles are outside this scope.

Changing which marker is used on a service, removing it and restoring it follows later saves. Annotation types must already resolve through the application loader; editing the marker type's own definition is not covered. Existing direct transaction and cache annotations can remain separate on the service method, subject to the advisor ordering and receiver restrictions above. Composed security does not extend support to async methods, framework callbacks, inheritance or generic operations.

Verified on both security configurations listed above: native nested-policy controls, deny-before-body, post-authorization after side effects, class/method precedence, cache/database protection and rejection of mixed or ambiguous policies. Modern tests remove each required pre/post advisor independently and prove that the body cannot run with only the other advisor. Actual-agent tests cover six saves through retained proxies, normal and child class loaders, rule/body edits, removal and restoration.

Composed transaction annotations on added services

An added ordinary service method can use an existing runtime annotation that centralizes transaction rules:

java
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
public @interface Work {
    @AliasFor(annotation = Transactional.class, attribute = "readOnly")
    boolean readOnly() default false;
}

// Added to a supported singleton service after startup:
@Work(readOnly = true)
public Order findOrder(long id) { /* ... */ }

Nested transaction composition, class defaults and method overrides are supported. The saved annotation values are passed to Spring's native transaction attribute source. Spring owns @AliasFor merging, manager selection, propagation, read-only hints and rollback rules. Editing the service's annotation usage refreshes that metadata; removing just the annotation removes transaction interception, and removing/restoring the method follows the existing retained-call-site contract. Read-only remains Spring's transaction hint, not a new database write prohibition.

The annotation graph must contain only transaction semantics and Java @Target, @Retention, @Documented, @Inherited or @Deprecated metadata. Custom markers, stereotypes or other framework semantics combined into the same annotation, repeatable containers and cyclic graphs are outside this scope. A recognized transaction graph with unsupported companion metadata is refused before its body runs. Annotation classes must already be resolvable through the application loader. Editing the annotation type's own definition is not covered; save edits to its usage on the service instead. Pure composed cache annotations have the separate support below. Pure composed security markers have the scope above. Composed async annotations and composed transaction callbacks remain separate limitations. All ordinary operation receiver, proxy, advice, visibility and generic restrictions above still apply.

Verified with Spring 5.3.39, H2 2.2.224 and JDK 21: native attribute comparisons, checked-exception rollback, read-only hints, manager aliases and REQUIRES_NEW inside an outer rollback. Real agent tests cover five saves through a held CGLIB proxy in ordinary and child classloaders, including annotation removal and method removal/restoration. No new Spring 6, JDK 17 or live SAP compatibility claim.

Composed cache annotations on added services

An ordinary added synchronous service method can use an application annotation composed from @Cacheable, @CachePut, @CacheEvict or @Caching:

java
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Cacheable(cacheNames = "catalog", key = "#p0")
public @interface CachedCatalog {
    @AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
    String[] regions() default {"catalog"};
}

// Added after startup to a supported singleton service:
@CachedCatalog(regions = "products")
public Product findProduct(String code) { /* ... */ }

Nested annotation aliases, class-level cache operation defaults, method overrides and direct @CacheConfig defaults are retained in saved metadata. Spring's own operation source merges the annotations; the existing native interceptors own cache keys, conditions, unless, put/evict behavior and grouped @Caching operations. A custom key generator receives the actual target and saved method. Named keys require saved MethodParameters (for example javac -parameters); indexed arguments such as #p0 do not require parameter names.

Editing annotation usage on the service updates the operation metadata. Removing the annotation removes that interception, while method removal/restoration follows the retained-call-site contract above. Metadata refresh itself does not flush cache values; normal region/key semantics and the separate code-change invalidation path still apply. Calls through captured plain singleton and supported old CGLIB proxy references use the actual target, including its existing state.

The custom annotation graph must contain only cache operation semantics and Java @Target, @Retention, @Documented, @Inherited or @Deprecated metadata. Mixed transaction/async/security/framework semantics within the same annotation, custom markers, repeatable containers, cyclic graphs and custom @CacheConfig-only annotations are outside this scope. Recognized cache graphs with unsupported metadata refuse before the service body. Composed cache advice on added event, transactional-event or scheduled callbacks is refused, including class-level policies on private callbacks. Future/generic operations are also excluded here. The ordinary receiver/proxy/advisor restrictions above remain in force. Annotation types must already resolve through the application's loader; editing the annotation type definition itself is not covered, so save changes to its usage.

Verified with Spring 5.3.39: native operation comparisons, alias/default selection, condition/unless checks, before-invocation eviction on a throwing body and grouped cache operations. Real agent tests run seven saves through a held CGLIB proxy with normal and child classloaders. No broader Spring-version compatibility is claimed for composed cache operations here.

Edited aspect pointcuts

Change the expression on an existing advice method and compile the aspect:

java
@Around("execution(* com.example.OrderService.submit(..))")
public Object observe(ProceedingJoinPoint call) throws Throwable {
    return call.proceed();
}

If OrderService already has a supported Spring proxy, previously injected references now use the edited pointcut. The same proxy and target remain alive: service fields are not reset and the service is not destroyed/recreated by this AOP step. An existing proxy can gain this aspect's advice even if it previously carried only another advisor. Widening, narrowing, removing all matches and restoring matches update the chain without duplicating advice.

Spring resolves matching with the bean name, supplies its AspectJ invocation context and orders the changed advice. Unrelated advisors retain their identity and relative order. If Spring's sorting would reorder those advisors, the bean is declined with a reason. All captured contexts are checked, including child contexts using an aspect defined in a parent. Aspects registered by factories or programmatically reach this path even without @Component.

The supported shape is a mutable JDK or CGLIB singleton proxy with a direct SingletonTargetSource, and one standard AnnotationAwareAspectJAutoProxyCreator in that context. Previously unproxied singletons cannot acquire a proxy in place; matching beans are named in the log and restart ledger. Frozen, opaque, nested and dynamic/custom-target proxies, custom/multiple auto-proxy creators, per-clause aspects and introductions are outside this path. The scan does not instantiate lazy/prototype service beans; already-issued prototype/scoped instances and FactoryBean products are not tracked. Adding new advice methods on a stock JDK, changing proxy interfaces or removing the @Aspect annotation is not covered.

Invalid pointcut syntax is detected before changing living chains; a corrected save can recover. This is not an atomic update across concurrent invocations: Spring's advisor mutation API clears method-chain caches, but an invocation already running can finish with its old chain. Calls overlapping the mutation can observe an intermediate chain, including both old and new aspect advisors; concurrent refresh is not covered by the single-call correctness tests. The existing request-boundary mode has its own documented scope. The aspect's ordinary bean refresh, when applicable, retains its existing lifecycle effects.

Verified with Spring Framework 5.3.39, AspectJ 1.9.22.1 and a stock JDK 21 using real JDK/CGLIB proxies and HTTP calls across four aspect saves. Other Spring/ AspectJ versions and a JDK 17 runtime were not exercised for this feature. AspectJ is only a test dependency of Reclazz; the production agent ships neither Spring nor AspectJ and does not enable AspectJ weaving.

Jackson properties added after startup

Start with -javaagent, then add a getter to an already loaded DTO and compile:

java
private int count = 7;
public int getCount() { return count; }

The next JSON response from a Spring-managed Jackson mapper includes "count":7, including when the endpoint returns an object created before the edit and the field has a supported initialiser. Add a setter too and a JSON request can populate the new property:

java
public void setCount(int count) { this.count = count; }

Direct instance fields work as well, including private annotated fields:

java
@JsonProperty("display_name")
@JsonAlias("label")
private String displayName;

Jackson writes the same added-field storage that application code reads. Both deserializing a new mutable DTO and readerForUpdating an object created before the reload are covered. Repeated saves update metadata and bodies; renaming, removing and restoring a property changes fresh mapper discovery in both directions.

Jackson still decides which properties to read and write. Reclazz supplies saved field/method metadata and routes calls to the current companion or field storage. @JsonProperty, @JsonGetter, @JsonSetter, @JsonAlias, @JsonIgnore, read/write-only access, null policies, inclusion, naming and field/method mix-ins reach the normal discovery path. Concrete generic collections retain their element types. Private members follow each mapper's access-override policy; one mapper granting access does not grant it to another. Custom DTO and property serializers/deserializers remain in charge. Invalid input conversions and throwing setters/getters retain Jackson's mapping-error path. An ignored field can suppress its associated getter.

This covers concrete, non-static, no-argument, non-void methods named getX, boolean isX, or directly annotated with @JsonProperty/@JsonGetter. Synthetic/bridge methods are excluded. Added setters must be nonstatic, concrete, single-argument void methods named setX or directly annotated with @JsonProperty/@JsonSetter. Nonstatic, nonsynthetic fields carry their saved annotations, type annotations and generic signature. Jackson retains its normal visibility/transient filtering. Final added fields can be read but cannot be written, even with access overriding enabled. Fluent setters, new constructor/record creators and builder mutation are outside this addition. The original class's ordinary reflection still cannot see the added members. The Jackson hook must load before Jackson databind; attaching after Jackson loaded does not install it retroactively.

Cache refresh discovers ObjectMapper beans in captured Spring contexts. A mapper created privately outside those contexts can retain its previous shape. An ObjectWriter retained from before a change can retain its property list, while its added getters dispatch to updated bodies; obtain a new writer after reload for the new shape. Retained ObjectReaders can likewise keep an earlier input shape; obtain a fresh reader after reload. In-flight conversion is not paused or rolled back. Application setter side effects are not a transaction: an error in a later property does not undo earlier writes to an object being updated.

Historical acceptance used stock JDK 21 with Jackson 2.13.5 and Spring 5.3.39: real loopback HTTP, old/new DTO objects, three mapper policies, nested/generic values, private getters, null/empty inclusion, custom serializers and four successive edits. Input/field regressions additionally cover four saves with real HTTP JSON, ordinary and child application classloaders, alias/name changes, removal/restoration, private setters and fields, generic conversion, null skipping, custom deserializers and original reflection staying unchanged. Current P3b acceptance uses Jackson 2.22.3 with Spring 5.3.39 and 6.1.14 on stock SapMachine JDK 17/21. It sends loopback HTTP directly to Spring-managed ObjectMappers; it does not prove every MVC message-converter configuration. Getter-output policies run on normal loaders; setter/field input-output runs on normal and child loaders. Mix-ins, mapper access restrictions, final-write refusal and retained-writer behavior have separately labelled component-only observations. Jackson 3, other unlisted Jackson 2 versions and accessor-generating extensions such as Afterburner are not verified; this support targets Jackson's normal reflective accessor path.

Scheduled methods added after startup

With the agent attached at startup, add this method to an existing singleton @Service or @Component, compile, and it starts running without a restart. The bean may already be a standard transaction, cache or async proxy: the task is unwrapped to run on the real target, so it reads the live fields. Supported public callbacks may also carry direct transaction/cache annotations, as described below, and direct @Async within the scheduled async scope. A frozen proxy, a non-singleton target source, or a proxy carrying a non-standard advisor is named and left for a restart. Scheduling must already be enabled in the application, for example with @EnableScheduling.

java
@Scheduled(fixedDelayString = "${cleanup.delay:1000}")
public void cleanup() {
    cache.clear();
}

On a stock JDK the new method lives in a companion and reflection on the original class cannot see it. Reclazz supplies Spring with a hidden adapter carrying the scheduling annotation and delegating to the current implementation. Spring resolves the timing attributes. Each successful save replaces the previous adapter registration. Each invocation reads the current singleton, so replacing that bean through a dependency refresh does not leave the task calling a destroyed instance. Invocations skip a temporarily absent bean without creating it. Removing the method or its scheduling annotation cancels that registration. Closing the Spring context cancels it too. Reload does not pause background scheduling while bytecode and Spring metadata are updated. Cancellation takes effect during scheduling re-registration; callbacks that start before cancellation may finish. Scheduled work is outside the MVC request reload boundary.

The added-method path supports direct @Scheduled and repeated @Scheduled declarations (@Schedules) on no-argument, void instance methods, including private methods, on singleton classes recognized by the agent's Spring stereotype detection (@Service, @Component, and the other directly supported Spring stereotypes). It also works when the application had no scheduled tasks at startup. The existing scheduler, unrelated tasks and scheduling configuration are retained.

Static methods, parameters, non-void/reactive return types, proxies other than the standard transaction/cache/async singleton proxies above, other subclass instances, and additional runtime method annotations outside @Deprecated and the operation subset below are reported as unsupported for the added-method path. In particular, Reclazz does not schedule an added method while silently bypassing its advice. Custom composed scheduling annotations and classes registered only through XML or @Bean without a recognized class stereotype are outside this support. Existing methods still use the ordinary Spring scheduling path.

If registration fails, for example because of invalid cron syntax, the previous adapter is cancelled and any partially registered replacement tasks are cancelled too. The failure is reported; the previous schedule is not restored. Correct the declaration and compile again to retry.

Event listener methods added after startup

With the agent attached at startup, add a listener to an existing singleton @Service or @Component and compile. It can be the class's first listener. The bean may already be a standard transaction or cache proxy: the listener is unwrapped to run on the real target. Supported public callbacks may also carry direct transaction/cache annotations. A frozen proxy, a non-singleton target source, or a proxy carrying a non-standard advisor is named and left for a restart. Spring's event listener processor must already be present (as in an annotation-configured Spring context).

java
@EventListener(condition = "#a0.priority > 5")
@Order(10)
public void onOrder(OrderCreated event) {
    notifications.send(event);
}

The added method receives matching events without a restart. Spring selects the event type, unwraps payload events, evaluates condition, and applies @Order. classes/value and explicit listener id attributes are carried across. Indexed conditions (#a0, #p0) work without compiler parameter names; named parameters also work when the class file includes -parameters or debug local variable metadata.

An added listener can also return a single event object:

java
@EventListener
private OrderAccepted onOrder(OrderCreated event) {
    return new OrderAccepted(event.id());
}

Spring publishes the returned object through the same application context; it retains its identity as an ApplicationEvent or a payload event's payload. A null return, a rejected condition or an absent/skipped singleton publishes no result. User exceptions follow Spring's normal delivery behavior. The return can be declared as Object, but its actual value is checked too: primitive/array, Iterable/collection, Map, stream, Future/CompletionStage, JDK Flow.Publisher and reactive-streams Publisher returns are outside this single-event scope. Unsupported declared types prevent registration; an unsupported actual result throws before Spring can publish, expand or subscribe to it. The handler's preceding side effects are not rolled back. Existing reflected listeners keep their usual Spring return behavior. Use distinct input/output event types when chaining listeners; Reclazz does not prevent application event feedback loops.

Saving again replaces the added registrations. Removing the method or its annotation stops it from receiving subsequent publications after reload has completed. Refreshing existing listeners scans only the affected beans; other beans and manually registered listeners keep their registrations. Multiple singleton instances and multiple contexts are handled separately. Each invocation reads the current singleton, so dependency refreshes can replace the bean without leaving its listener on a destroyed instance. An absent bean is skipped without being created; an unsupported replacement proxy is skipped and reported.

The added-method scope is a direct @EventListener on an instance method returning void or a single event object, including a private method, with exactly one reference event parameter and no generic method signature. The bean class must carry a Spring stereotype recognized by Reclazz. Extra runtime method annotations are limited to @Order, @Deprecated and the operation subset below. Added @TransactionalEventListener methods have the separate scope below. Direct @Async on a public void ordinary listener has the scope below. Composed listener annotations, generic signatures, unsupported proxies/subclasses, prototype beans, and classes registered only through XML or @Bean without a recognized stereotype require a restart. The added path requires the default Spring event listener factory; the standard transactional factory can coexist with it. Custom factories are reported as unsupported for additions. Existing reflected listeners continue to use the application's factories.

Event delivery is not paused during reload. Retired added registrations skip processing; a callback body already executing can finish. The MVC request boundary does not cover event publication from other threads. If registration fails, previous added registrations and any partially registered replacements for that class are removed in the affected context. There is no rollback to the previous listener declaration. Correct the declaration and compile again. Spring expression syntax and user exceptions remain subject to Spring's normal event delivery behavior.

This path is runtime-tested on Spring 5.3.39 with JDK 21. Spring 6 compatibility has source-level checks but has not been verified in a running application.

Transaction and cache advice on added callbacks

A supported public @Scheduled or ordinary @EventListener method added after startup can carry direct @Transactional, @Cacheable, @CachePut, @CacheEvict or @Caching. It uses the same application transaction/cache interceptors as added service operations. Scheduling and event annotations stay on the framework delegate; operation annotations stay on the saved method metadata, so the delegate does not add a second advice chain. Event conditions and @Order still apply. A cached returned event is still published on a cache hit, although the callback body is not executed again.

For example, a newly added cleanup job can commit its writes and evict a cache only after a successful invocation:

java
@Scheduled(fixedDelay = 60000)
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = "orders", allEntries = true)
public void cleanup() throws Exception {
    orderStore.removeExpired();
}

The application must already enable the relevant Spring operation infrastructure. Its configured advice order, propagation, rollback and cache rules are preserved; this does not make an ordinary cache transactional. For eviction after commit, configure cache advice outside transaction advice. A failure remains subject to the configured rollback/eviction rules; the example's checked exceptions roll back.

The advised callback must be public, concrete, non-final and non-generic, on a non-final class directly extending Object without interfaces. The existing singleton, standard-advisor and direct-stereotype constraints still apply. Unadvised private callbacks retain their previous support. Additional advice on @TransactionalEventListener, class-level advice, composed operation annotations, arbitrary aspects and method-security annotations are outside this extension. Direct async callbacks have separate scheduled and ordinary event scopes below. Unsupported operation infrastructure fails before the callback body.

Real-agent tests with Spring 5.3.39 and H2 exercise ordinary and child classloaders, commit/checked rollback, successful versus failed cache eviction, event cache hits and returned-event publication, removal of advice, removal/restoration of callbacks, and old proxy references over four saves. They invoke Spring's registered scheduled Runnable explicitly to control the database assertions; timer behavior remains covered by the separate scheduling reload tests. Same-class self calls retain the existing service-operation behavior and do not acquire transaction interception.

Async scheduled methods added after startup

A supported singleton can gain a periodic job using the application's existing async infrastructure. Enable scheduling and async processing before the save:

java
@Scheduled(fixedDelayString = "${cleanup.delay:1000}")
@Async("backgroundJobs")
@Transactional(rollbackFor = Exception.class)
public void cleanup() throws Exception {
    expiredOrders.remove();
}

Direct @Async is supported on a newly added public, concrete, non-final, non-generic, no-argument void method with direct @Scheduled or repeated @Scheduled declarations. The non-final owner directly extends Object without interfaces and uses a supported direct Spring stereotype. Receivers are plain singletons or mutable CGLIB singleton proxies with standard transaction/cache advisors and optionally the application's configured async advisor, once and first. Class-level/composed async annotations, custom advisors, other target sources and non-void/reactive scheduled methods remain unsupported. Class-level async metadata, including composed and inherited declarations, is refused even for private scheduled methods; it is not silently bypassed.

The saved executor qualifier is resolved by Spring's native async interceptor; omitting it uses the configured default selection. Direct transaction/cache advice runs on the worker, with native rollback/eviction rules. Void-body failures go to the configured async uncaught-exception handler. Missing/wrong executors and rejected submissions fail on the scheduler's submitting thread and follow its error policy; they do not execute the body synchronously as a fallback.

Each scheduled tick resolves the current singleton. A later save changes the metadata/executor for subsequent submissions. Already accepted work retains its admitted target and direct body, and may finish after reload or removal; nested calls are not frozen. Removing @Async restores synchronous scheduled execution. Removing the method/schedule or closing the context cancels future scheduled ticks. The application owns worker shutdown and queued-work policy; retaining an old target does not restore resources destroyed during bean recreation.

With @Async, fixedDelay measures completion of submission, not completion of the worker body. Work can overlap; executor capacity and rejection policy still matter. This does not add a drain boundary around scheduled work.

Verified on Spring 5.3.39 with stock JDK 21, including normal and child classloaders, executor changes, queued work across reload/removal, checked-exception transaction rollback, error handling, synchronous restoration and existing proxy advice. The agent tests explicitly invoke Spring's registered scheduled Runnable on a scheduler probe thread for deterministic queue checks. Existing scheduling tests cover actual timers. Other Spring versions are not claimed by these tests.

Async event listeners added after startup

A supported singleton can gain a public void listener that runs on the application's Spring executor without restarting:

java
@EventListener
@Async("notifications")
@Transactional(rollbackFor = Exception.class)
public void accepted(OrderAccepted event) throws Exception {
    notifications.record(event.id());
}

Enable Spring's async infrastructure beforehand and supply the named executor. Omitting the name uses Spring's configured default executor selection. Reclazz uses the existing AsyncAnnotationBeanPostProcessor advisor and its native interceptor; it does not create a separate executor. Conditions run before submission. @Order orders listener admission, not completion of work on different executor threads. The configured async uncaught-exception handler receives void callback failures; executor lookup errors and rejected submissions still fail on the submitting thread.

Direct transaction/cache annotations from the callback subset above can coexist. Async advice runs first, so a transaction begins on the worker thread. The publisher's thread-bound transaction is not carried to that thread. Native rollback and cache rules still apply; cache work is not automatically rolled back with a database.

This scope requires a public, concrete, non-final void method with one reference parameter, no generic signature, and direct @Async plus ordinary @EventListener. The non-final owner uses direct @Component, @Service or @Repository metadata and directly extends Object without interfaces. Supported receivers are plain singletons and mutable CGLIB singleton proxies carrying only standard transaction/cache advisors and, optionally, the same standard async advisor first. Unknown, duplicated or reordered async advisors and custom/uninitialized async postprocessors are refused. Class/composed async annotations, Future or event-returning async event methods and async transactional-phase listeners remain outside this scope. Scheduled callbacks have the separate scope above. Normal service methods can use the separate async service scope below.

Each newly submitted invocation uses current metadata, executor selection and singleton. Already submitted work retains its admitted target and direct callback body across reload or removal. It can finish after the old listener is retired; removing the listener stops subsequent event submissions. This does not freeze every method the body calls or provide atomic event delivery across a reload. The application owns executor shutdown and pending-work policy. Ordinary bean recreation may already have run destruction callbacks on an admitted old target; its resources are not restored by retaining the callback. Superseded hidden method entries are removed from native executor-resolution caches without clearing unrelated methods.

Verified with Spring 5.3.39 and stock JDK 21: configured and named default executors, qualifier changes, conditions, native error handling, worker transaction rollback, old proxy calls, reload/removal with queued work, and ordinary/child classloaders. No additional Spring-version compatibility is claimed by these tests.

Async service methods added after startup

Add a direct public @Async method to a supported service and compile it together with its caller. The caller can receive a result through Spring's native future:

java
@Async("workers")
public CompletableFuture<String> summarize(int orderId) {
    return CompletableFuture.completedFuture(reports.summarize(orderId));
}

The method can return void, exactly Future or exactly CompletableFuture. Concrete generic arguments are retained in saved metadata, including nested types such as CompletableFuture<List<Order>> and List<Order> parameters. Generic class/method type variables, wildcards, custom Future subclasses, declared CompletionStage/ListenableFuture and reactive returns remain unsupported. These restrictions concern the declared return type; an implementation can return a Future subtype behind a supported Future<T> declaration.

The existing async infrastructure constraints apply. The method is public, concrete and non-final on a non-final, non-generic owner directly extending Object without interfaces. Plain singleton and supported mutable CGLIB receivers, including retained old bean references, use the native executor. Unknown/custom advisors and class/composed @Async remain outside this scope. Class-internal calls retain normal Spring self-invocation semantics and run directly. This does not expose the added method through original-class reflection or promise an unadapted method-reference/lambda call path.

A Future-returning invocation immediately returns a native outer future; Spring obtains the value of the Future returned by the body. A thrown exception or an exceptional returned Future fails that outer future. void failures go to the configured uncaught-exception handler. Executor lookup/submission errors still fail on the submitting thread. Cancellation uses the native returned handle; cancellation before execution is tested. It is not a promise to interrupt running work, cancel an inner future or reverse side effects. Executor capacity must account for Spring's native waiting on the body-returned future.

Supported direct transaction/cache annotations run on the worker. On the tested Spring 5.3.39 stack, a checked exception thrown by a body with a matching rollback rule rolls back its transaction. Returning an exceptional or incomplete Future does not extend the transaction until completion or retroactively roll back its writes. Spring 5 caching stores the body-returned future, not an automatically unwrapped payload; each external async invocation still gets its own outer future. Cache values retain normal cache lifetime across reloads.

Later saves update metadata/executor selection and the callback body. Already admitted jobs retain the body and target captured before the save. Removing only @Async and any transaction/cache advice restores synchronous Future-returning calls. Removing the method makes retained call sites fail; restoring it makes them usable again. The prior queued-target/resource and executor-lifecycle limits still apply. This service invocation support does not add new scheduled/transactional listener shapes. MVC request lifetime protection is a separate opt-in request boundary; WebFlux remains outside it.

Verified with native Spring 5.3.39, H2 and stock JDK 21, including real agent runs on ordinary and child classloaders, old/current bean references, five saves, qualifier changes, worker rollback, cancellation, synchronous annotation removal, method removal/restoration, and self-invocation. No additional Spring-version claim is made.

Transactional event listeners added after startup

On a supported unproxied Spring singleton, add a direct transaction listener and compile the class:

java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void accepted(OrderAccepted event) {
    notifications.record(event.id());
}

The application's standard TransactionalEventListenerFactory must be present and precede the default event factory. Reclazz uses Spring's native transactional adapter and synchronization lifecycle. BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK and AFTER_COMPLETION, direct @Order, event filtering, conditions and fallbackExecution are supported. Default fallback skips events published without a transaction. Preserved argument names work in conditions; use #a0 or #p0 when the compiler did not preserve names.

Callbacks retain Spring's transaction semantics: a BEFORE_COMMIT exception can roll back the transaction, while an after-completion callback cannot undo an already completed commit. This feature does not give callback writes a new transaction or promise to commit them separately.

An added registration is retired before removal or replacement. Events already queued for that registration are skipped when their transaction phase arrives, including condition evaluation. They are not replayed through the new listener. Newly published events use the replacement; a method body already executing is not interrupted. This retirement policy applies to added registrations, not to listeners reflected at application startup. Failed removal keeps the retired registration indexed so a later save can retry cleanup without duplicate delivery. Callbacks also skip an inactive context or an absent singleton; they resolve the current unproxied singleton when invocation begins.

Support requires one reference event parameter, a void return, a direct @TransactionalEventListener and the existing stereotype/singleton restrictions. Do not combine it with a direct @EventListener on the same method. Extra advice such as @Transactional or @Async, result publication, generic signatures, composed annotations, proxies, custom factories and reactive transaction context remain outside this added-method scope. Ordinary added listeners continue to work in contexts with the standard transaction factory.

Verified with Spring 5.3.39, JDBC/H2 and SapMachine 21, including real commit and rollback, native phase/order comparison, pending events during replacement/removal, restoration and ordinary/child classloaders. No production dependency was added. Other Spring versions, reactive transaction managers and a JDK 17 runtime have not been verified for this extension.

Property changes keep the last working values

For non-SAP Spring applications, Reclazz checks a saved .properties change before putting it into the running Environment. It binds affected @ConfigurationProperties beans on separate objects and resolves and converts direct @Value fields and constructor parameters, including the supported computed field expressions. A save containing both a valid service address and an invalid timeout is held as one candidate. The Environment, property beans, direct fields and logger levels keep their old values. Fix the timeout and save again: both pending keys are retried.

The check uses the application's Boot binding code with separate property sources and targets. Boot's validation fallback, a named configurationPropertiesValidator, self-validating records, bind-handler advisors, conversion services, property editors and the annotation's ignore flags apply. In particular, ignoreInvalidFields=true still permits values Boot chooses to ignore. The check does not write Boot's live bound-property records. The current P5 suite uses Boot 2.7.18. Earlier notes described a Boot 3.3.4 probe, but P5 has no fresh native Boot 3 graph and does not certify that probe against the current tree. Record replacement has component evidence; a watched constructor-bound record with held consumers is not currently observed.

No new option is needed. With reloadBoundary=request, successful changes wait for the existing request boundary before application. A save made during that wait becomes another candidate; accepting the earlier candidate never reads the newer file or its logger levels. An interrupted wait accepts nothing.

The log distinguishes these outcomes:

Outcome Running values File baseline
Rejected Kept because binding, conversion or validation failed Pending
Uncheckable Kept because the binding check could not run Pending
Wait interrupted Kept because application never ran Pending
Applied Updated through the existing rebind path Accepted candidate
Partially applied Some values may already have changed; failures are named Pending for another save

Limits: this is a validation step before application, not rollback for arbitrary application code. Constructors, setters, converters, validators and advisors can have side effects; Reclazz cannot isolate state they keep themselves. If a setter or bean rebuild fails after a clean check, changes already made are not rolled back. Request isolation covers the supported MVC scope of the existing boundary; background readers are not paused. Connection pools still have their existing limitations.

A JavaBean target needs a usable no-argument constructor. Constructor-bound targets use Boot's constructor binding. Missing or incompatible Boot internals hold the change as uncheckable. Computed @Value constructors use the restricted check below. SAP Commerce's Config path retains its separate behavior. The legacy path for .properties files without Boot resource origins applies only added or changed keys. For Boot-owned files, see YAML and removal. A syntactically valid truncated file cannot be distinguished from an intentional save; malformed or unreadable files do not advance the baseline.

YAML and removed configuration keys

Already-loaded local Spring Boot .properties, .yml and .yaml files use the application's own loaders. Nested YAML keys and indexed lists reach the same binding path as startup. Reclazz identifies the file through Boot's resource origins and replaces its sources at their existing positions. Command-line and other higher-priority sources continue to win; an edited inactive profile file is not promoted into a new override.

Deleting a key reveals its value from lower-priority sources, or the consumer's default when no source supplies it. Emptying or deleting a previously identified single-document file works, and recreating it restores its values. For mutable @ConfigurationProperties beans, removal copies freshly bound, validated JavaBean properties into the existing object. The bean's identity and existing holders are preserved; nested property objects can be replaced by their setters. Types with read-only or write-only bean properties are held as uncheckable on removal. Constructor-bound beans retain the existing recreation path and its reference limits. @Value fields resolve against the resulting Environment.

Validation covers every affected context before any file source is changed. Malformed input or invalid conversion keeps the previous sources and values. A live setter failure can partially apply values; it is reported as partial and the same file remains pending for an identical-save retry. A waiting request boundary uses the captured file bytes. It does not reread a later save.

Supported YAML documents use the current active profiles and spring.config.activate.on-profile expressions. The set and positions of active documents must stay the same. Changing activation, imports, config locations, profile groups or cloud-platform selection requires a restart. Files containing other spring.config.* or spring.profiles* controls are held. New or initially empty files with no identifiable Boot source, custom source loaders and changed source ownership are outside this path. Applications with Boot file sources do not fall back to a global override for an unowned file. SAP configuration removal is outside this feature. Removing a logging level without a lower configured level still needs a logger reset/restart.

Verified in current P5 with Spring Boot 2.7.18, Spring 5.3.39, SnakeYAML 1.30 and stock SapMachine JDK 17/21 on macOS arm64: real startup-agent HTTP checks for properties and YAML, old bean holders, lists/defaults, invalid conversion/syntax recovery, deletion and restoration; unit regressions cover precedence, active/inactive documents, multiple contexts, immutable candidates, external source replacement and partial-application retry. Other Boot/SnakeYAML versions are not verified. SnakeYAML is a test dependency; the production agent uses the application's parser and packages no YAML library.

Computed @Value fields

An existing singleton field can compute a value from changed properties:

java
@Value("#{${timeout.seconds:5} * 1000L}")
private long timeoutMillis;

@Value("#{${retry.count:2} > 3 ? 'extended' : 'normal'}")
private String retryMode;

Save the .properties file and the field is recalculated without recreating its bean. The expression is checked against the candidate property values before any live property is changed. Syntax errors, integer division by zero, non-finite numeric results and failed type conversions reject the whole candidate, including its other keys and logger levels. Fix the value and save again to retry the pending keys.

The supported subset is one whole #{...} expression on a writable instance field of primitive, boxed primitive, String, collection, map or array type, or a mixed text and #{...} template on a scalar field such as host:#{${port} + 1}, whose every embedded expression is checked the same way. A changed key can be referenced directly or through a property-value chain; placeholder defaults and nested placeholders are resolved. Expressions can use numeric, string, boolean and null literals, +, -, *, /, %, comparisons, boolean operators, ?: conditionals, Elvis defaults, and inline lists {a, b} and inline maps {k: v} whose elements are themselves supported. So a list or map assembled from changed placeholders, such as #{ {${a}, ${b}} }, is re-evaluated on save. Arithmetic follows Spring's operator semantics (including its integer arithmetic); this is not an overflow checker. The bean factory's conversion service is used inside expressions, and its type converter performs the final field conversion, honouring the field's generic element types.

Bean references, property/method access, T(...), new, variables, assignment and regex are unsupported, in a whole expression or inside a template segment. Every AST branch is checked before evaluation, even a branch that would not execute. An affected unsupported field holds the whole candidate as Uncheckable and names the field and reason. It is not evaluated through the application's bean expression resolver. Custom resolver/parser implementations and non-default expression delimiters are also held. Resolved expressions and string results are limited to 2048 characters, with at most 256 AST nodes and depth 32.

Computed constructor parameters have their own rebuild scope. Static/final fields and non-scalar field types are outside the field support. Existing placeholder resolver ordering still applies during live reinjection; the precheck uses the candidate Environment. As with ordinary property rebinding, custom converters may have side effects, and a failure during live application is reported as partial rather than rolled back. Background readers are not paused.

Current P5 observes real watched property saves on standalone Spring 5.3.39 and isolated Spring 6.1.14 with stock SapMachine JDK 17/21 on macOS arm64. The Spring 5 dependency graph also contains Boot 2.7.18; these computed-value watchers use plain Spring configuration. Live SAP runtime compatibility is unverified.

Computed @Value constructor parameters

A constructor argument can use the same scalar arithmetic/conditional subset:

java
@Component
public class Client {
    private final long timeoutMillis;

    public Client(@Value("#{${timeout.seconds:5} * 1000L}") long timeoutMillis) {
        this.timeoutMillis = timeoutMillis;
    }
}

Saving timeout.seconds=8 rebuilds this singleton with timeoutMillis=8000. Before destroying anything, Reclazz resolves and checks every @Value argument on the affected constructor, including unchanged arguments that recreation will re-evaluate. A direct placeholder that resolves to #{...} is checked too. The precheck evaluates values and conversions; it does not invoke the constructor. A syntax, arithmetic or conversion error rejects the whole candidate. An unsupported constructor expression or creation policy holds it as Uncheckable and identifies the bean and constructor parameter. A corrected save retries the pending keys. Unrelated property saves do not recreate the bean.

This support requires an existing singleton that is unproxied or has a supported transaction/security proxy, one declared constructor, and a bean definition that directly constructs that class. Spring's cached constructor and re-resolvable arguments must be readable and agree with that constructor. Factory methods (including @Bean), instance suppliers, manually registered singletons, explicit constructor arguments, method overrides and @ConfigurationProperties beans are outside this new path. Non-singleton beans are not swept. Ordinary injected bean dependencies may accompany the scalar @Value parameters. The expression operators, resolver restrictions and limits are the same as for computed fields.

The instance is replaced: its local state resets, and Spring runs destruction, construction, injection and initialization callbacks. Existing writable reference fields in surviving singletons in the same context are re-pointed to the new instance. Spring may destroy dependent beans too; retrieving them again constructs them with the new dependency. References held by application locals, collections, other contexts or already-destroyed holders are not guaranteed to change.

This is not a dry run of the bean's lifecycle. Ordinary field/method injection and application callbacks still run during successful recreation. Custom converters can run application code during checking too. If construction or initialization fails after the precheck, the outcome is Partial: the Environment may already have changed, the old bean may already be destroyed, and there is no rollback. The file remains pending. Background readers are not paused. Existing placeholder resolver ordering still applies; indirect dependencies are followed within the scope below.

Current P5 watches saves, rejection, recovery and holder replacement on standalone Spring 5.3.39 and isolated Spring 6.1.14 with stock SapMachine JDK 17/21 on macOS arm64. These are directly constructed Spring singletons, not Boot 3 configuration-properties or live SAP tenant acceptance.

@Value factory method parameters

An existing singleton created by a native @Bean method can refresh its factory arguments when their properties change:

java
@Bean
Client client(@Value("#{${client.timeout.seconds:5} * 1000L}") long timeoutMillis) {
    return new Client(timeoutMillis);
}

Saving client.timeout.seconds=8 recreates this client with 8000, even when Client itself has no injection annotations. Direct ${...} scalar arguments, restricted computed values, scalar text templates and indirect property references are supported. Spring resolves ordinary bean dependencies alongside these arguments. Static factories, lite configurations and full @Configuration factories use their native creation path; aliases still identify the recreated singleton.

Before live application, every direct @Value parameter of an affected factory is checked, including unchanged arguments and placeholders that resolve to SpEL. The check does not invoke the factory body. Invalid values hold the entire candidate; unsupported expressions or creation metadata report Uncheckable. Unrelated changes do not recreate the product. The factory's parameter metadata owns this path; annotations on a constructor invoked inside its body do not.

This requires an already instantiated singleton product that is unproxied or has a supported transaction/security proxy, a direct @Bean method, scalar parameters (primitive, boxed primitive or String), the standard expression resolver/parser, and readable Spring metadata proving that native factory arguments will be re-resolved. Instance factory owners must be existing singletons without additional AOP advice; normal full-configuration CGLIB enhancement is supported. Generic factory methods, collection/custom value types, composed annotations, FactoryBean, instance suppliers, explicit factory arguments, method overrides and @ConfigurationProperties binding are outside this native path. Reclazz-owned added factories have the separate support below. Uninstantiated lazy beans and prototype beans are not swept or eagerly created; later lookup uses Spring's ordinary property resolution.

Recreation runs the factory body, injection and lifecycle callbacks. Local state resets, and Spring may destroy dependent beans. Writable references in surviving singletons in the same context are healed using the existing recreation mechanism; locals, collections, other contexts and destroyed holders are not covered. This is not general callback/container retirement. Custom converters may execute during checking. A factory or initialization failure during live application reports Partial, leaves the candidate pending and may leave changed environment values and an already destroyed old bean. There is no rollback or background-reader pause.

Current P5 watches native factory-argument saves on Spring 5.3.39 and isolated Spring 6.1.14, stock SapMachine JDK 17/21, macOS arm64, in ordinary and child application loaders. Boot 3 and live SAP factory-property compatibility remain unverified.

Added factory values on property changes

A supported @Bean method added after startup can also refresh its scalar @Value arguments on a property save. Reclazz keeps the saved parameter metadata with the definition it created, so the method need not appear in ordinary class reflection. Instance and static factories use the same argument resolution as their initial registration. A later configuration save replaces this metadata; future property checks follow the new expressions rather than the old dependencies.

For an already instantiated, unproxied singleton product, a changed input checks every direct @Value parameter before applying any live values, including unchanged parameters that recreation will re-evaluate. Primitive, boxed primitive and String values use the same restricted scalar expressions, templates, conversion and indirect-property tracking as native factories. Ordinary bean parameters may accompany them. Checking does not invoke the factory body, and unsupported method/bean-call expressions hold the candidate as Uncheckable. Invalid arithmetic or conversion rejects the entire candidate; the existing product and environment remain unchanged. Annotations on a constructor called inside the factory body are not treated as factory arguments.

An accepted candidate recreates the product through its original supplier and Spring's lifecycle. Aliases still resolve the new singleton, and writable fields in surviving plain singleton holders in the same context are healed. Locals, collections, cross-context references, destroyed dependents and background readers have the same limitations as native property recreation. Initialization or factory failure during live application is Partial, with no rollback of the environment or the destroyed product. After correcting the candidate, a missing singleton is created on its next lookup; it is not eagerly restored by the property sweep.

Only the original Reclazz-owned definition, bean factory and supplier qualify; copied or changed creation metadata is held as Uncheckable. The configuration must still be its supported singleton, and the expression resolver/parser must remain standard. Arbitrary suppliers, proxy products, collection/custom @Value types and @ConfigurationProperties binding are excluded. Uninstantiated lazy beans and prototypes are not eagerly created or swept; future creation reads current values. Property refresh does not re-evaluate registration conditions or activate a missing conditional bean. It does not perform general callback or listener-container retirement.

Current P5 watches property saves after adding a factory method to lite/default configurations on Spring 5.3.39 and isolated Spring 6.1.14, stock SapMachine JDK 17/21, macOS arm64, in ordinary and child application loaders. Live SAP factory-property compatibility remains unverified.

Transaction and security proxies on property changes

Computed constructor and native scalar @Bean factory arguments also refresh on existing singletons with a supported transaction or method-security JDK/CGLIB proxy, including their combination. For example, changing an immutable timeout supplied by @Value to a protected service recreates the service through Spring, including its proxy. Calls through the replacement retain native authorization and, when configured, transaction commit and rollback behavior.

Eligibility requires an unfrozen, inspectable Spring proxy and a SingletonTargetSource holding one plain target. Supported advice is one standard transaction advisor, standard pre/post method security, or both. Transaction advice must have an initialized standard interceptor and annotation metadata source. Security uses either the standard classic shared pre/post interceptor or distinct modern pre/post advisors with the standard managers, pointcuts and handlers. Each policy required by the target's security metadata must have its advisor. Direct @PreAuthorize/@PostAuthorize and fixed composed markers are supported; filtering, JSR-250/@Secured, parameterized markers and result-proxy policies are excluded.

Candidate validation inspects already initialized security advice; it does not initialize a deferred interceptor or call a service method. Cold, missing, duplicate or custom security infrastructure holds the candidate as Uncheckable. Additional cache/async/custom advice, custom transaction metadata, dynamic/custom target sources and nested proxies remain outside this path and also hold affected candidates. The constructor/factory metadata and expression restrictions above still apply.

Spring destroys and recreates the singleton; Reclazz does not strip its advice or send application calls directly to its target. Writable reference fields in surviving plain singleton holders in the same context are updated from the old proxy to the new one. Raw target references, locals, collections, proxied holders and destroyed dependents are not guaranteed to be healed. Local state resets, background readers are not paused, and live constructor/factory or initialization failures still report Partial without rollback. This does not add cache invalidation or arbitrary advice support. Added supplier-backed factories use the unproxied-product scope above; this proxy support does not extend to them. Existing direct-placeholder constructor and field-discovery behavior is unchanged; in particular, legacy field discovery can consult custom target sources before creation eligibility is checked.

Current P5 observes real H2 commit/rollback, repeated saves, held invalid values, lifecycle and holder replacement on stock SapMachine JDK 17/21, macOS arm64. The watcher configurations use Spring 5.3.39 / Security 5.7.11 with @EnableGlobalMethodSecurity(prePostEnabled=true) and isolated Spring 6.1.14 / Security 6.3.4 with @EnableMethodSecurity, in ordinary and child application loaders. These watcher fixtures use native JDK interface proxies for constructor and factory products. They demonstrate missing authentication, denied pre-authorization and authorized calls, and carry a @PostAuthorize rule; they are not acceptance for every denied post-authorization or CGLIB product variant. Broader component/proxy coverage remains separate from this watcher proof. The Spring 5 graph also contains Boot 2.7.18; the isolated Spring 6 graph uses plain Spring configuration. Live SAP compatibility remains unverified.

Indirect @Value dependencies

Properties can refer to other properties:

properties
shared.timeout=5
client.timeout=${shared.timeout}
java
@Value("#{${client.timeout} * 1000L}")
private long timeoutMillis;

Saving only shared.timeout=8 updates this field to 8000. The same dependency selection applies to ordinary ${...} fields and constructor parameters, and to supported computed constructor and factory parameters. Fields are re-injected and affected beans are rebuilt under their scope and lifecycle rules.

Reclazz follows the placeholder reads through the candidate property sources, using Spring's placeholder helper and source priority. Several alias hops, nested keys such as ${route.${profile}.timeout}, and placeholder defaults are supported. Sources need not enumerate their keys. A higher-priority literal masks a lower-priority alias. Retarget an alias to another key and later saves follow the new chain. Alias values remain ${...} in their source; only the actual saved keys are written to the Environment and accepted into the file baseline.

Tracing stops as soon as it reaches a changed key. The selected target then goes through the existing candidate check: invalid arithmetic, conversion, or a newly introduced circular reference rejects the whole save before live writes. If a trace cannot determine whether a target is affected (for example an already-cyclic source or an unreadable source), the save is Uncheckable. Fixing the candidate allows it to be retried. An unrelated cycle elsewhere is not scanned.

Indirect tracing allows at most 8192 characters per input/raw value, 128 placeholder occurrences across the traversal, and 256 lookups per expression. Exceeding a trace limit holds the candidate as uncheckable. These are dependency-selection limits; the existing expression checks still apply after a target is selected. Results are cached only within a selection pass, up to 1024 expressions, and are discarded after that pass. Direct references retain their conservative matching: a mentioned fallback key may select a target even when the resulting value stays the same.

This tracks @Value dependencies through the candidate Environment's default ${...}/colon syntax. It does not add indirect @ConfigurationProperties binding, custom placeholder-language support, arbitrary SpEL, YAML/key removal or SAP Commerce Config support. Custom embedded resolvers may use different sources or ordering during live injection; that existing precheck/live limitation remains. Application property-source lookups and conversions can have side effects. Mutable sources changed independently of Reclazz are not frozen across the request boundary. Successful constructor recreation retains its existing state-reset and reference limits; failures during live application remain partial without rollback.

Current P5 watches indirect-value saves, rejection and recovery on standalone Spring 5.3.39 and isolated Spring 6.1.14 with stock SapMachine JDK 17/21 on macOS arm64. Boot 3 binding, indirect Boot configuration-properties and live SAP runtime compatibility remain unverified.

Cache dependencies after reload

Spring Cache observation is automatic when the companion engine instruments application methods. No agent argument is needed. If PriceService caches its result in prices and calls an unannotated DiscountRules helper, reloading the helper invalidates prices. An unrelated descriptions cache stays populated. This works for externally compiled classes and AutoCompile.

The agent records the watched classes executed inside Spring's synchronous cache interceptor and the actual cache instances involved. If a cached quotes calculation reads an already cached prices result, their dependency is recorded too: changing the price helper invalidates both regions. Equal cache names in separate managers or application contexts are distinct, as are classes with the same name in different classloaders. Caches supplied directly by a CacheResolver are included. This is region-level eviction, not per-key invalidation. Dependencies are conservative unions: a dependency that stops being used can still cause eviction until its class or cache is collected.

A cache operation overlapping a class mutation is conservatively invalidated when its outermost synchronous interceptor returns. Its caller can still get the result calculated with old code, but that result is not retained for the next call after the invalidation succeeds. The interval between Spring storing a value and the interceptor exiting is not isolated: another thread can read that value, and thread scheduling can extend this interval. Nested operations defer their invalidation until the outermost exit so an inner exit does not clear caches while an outer synchronous cache loader still holds its lock. Normal and exceptional exits release observation. Failed clears are reported, do not replace the application's result or exception, and retain dependencies for another reload to retry. Operations overlapping unrelated reloads may also be cleared conservatively.

Attach mode and any cache-hook failure mark observation incomplete. For an annotated class the agent then retains its existing fallback of clearing all manager caches; known resolver-only caches are also cleared. An unannotated helper invalidates only its observed dependents, even with incomplete history. Without observations, an annotated class keeps the same fallback. Tracking stops growing at 512 cache instances or 8192 class/dependency links and marks coverage incomplete for the session; existing dependency records remain usable. Persistent class and cache identities are weakly held.

Limits: native JBR/DCEVM redefinition does not use the method instrumentation and therefore keeps the annotation fallback. Constructors, class initializers, excluded methods/classes, unwatched libraries, cache access outside Spring's interceptor, pre-attach entries and asynchronous/reactive computations do not have complete dependency coverage. The synchronous paths, including @Cacheable(sync=true), are tested against Spring 5.3.39. The hook matches the Spring 6 signatures as well, but this change does not add a real Spring 6 E2E matrix. A watched method pays a volatile activity check even when no cache operation is open; during observation it also records a class identity. verbose=true names targeted evictions and computations crossing reloads.

New JPA entities with a prepared schema

With jpaRefresh=true, a new @Entity class can be mapped after startup when its table is already present and hibernate.hbm2ddl.auto=validate. This supports keeping schema changes in your own migration process: apply the schema change, then add and compile the entity. Reclazz asks Hibernate to validate the candidate mapping, verifies that its metamodel contains the new entity, then replaces the native factory behind Spring's existing client proxy. Existing injected factory and shared EntityManager references follow the new mapping. No schema action is changed to update or create by this path.

Missing tables, missing columns and incompatible column types leave the previous factory open and its data accessible. The failed candidate's managed-class name is removed again. After correcting the schema, compile another body-only edit to the pending entity to retry; AutoCompile follows the same rule. An identical class file is normally ignored by change detection. Changing its declarations or mapping annotations while pending is refused: restore the original mapping and compile a body edit, or restart for the changed mapping. A normal save of an already mapped entity does not trigger this new-entity rebuild again.

Verified with standard LocalContainerEntityManagerFactoryBean, the standard Hibernate provider, Spring ORM 6.1.14, Hibernate 6.5.3.Final and H2 2.2.224 on stock JDK 21, including real agent runs on ordinary and child application classloaders. Only one persistence unit is supported for new-entity selection. The factory or unit must explicitly configure Hibernate validate. JPA database/script action overrides (including none), contributor-specific actions, custom schema tools, schema filters, factory subclasses and custom providers are refused on this path. none/unset modes, automatic migration execution and multi-unit selection are not added here. Hibernate's own schema validation determines what is checked; this is not a check of every index, constraint or application data invariant.

Successful rebuilds retire the old native factory. Finish active database work before triggering one; retaining old persistence contexts or active transactions across a successful rebuild is not supported. The failure guarantee applies before installing the candidate, not to rolling back arbitrary provider callback side effects. A failure closing the old factory after installation is reported separately; the new mapping remains installed. Existing entity field additions on stock JVMs still require enhanced redefinition or a restart. Existing DDL-enabled mapping refresh keeps its previous behavior.

Held repositories after a prepared mapping refresh

Separate watcher acceptance verifies an already-injected Spring Data repository across this prepared-schema refresh on Spring ORM 6.1.14, Spring Data JPA 3.3.4, Hibernate 6.5.3.Final and H2 2.5.250, on stock SapMachine 17/21 with ordinary and child application loaders. The original repository keeps an existing entity as its domain root. After the native factory changes, it writes and reads that entity through saveAndFlush, findById, a warmed JPQL query and count. A custom repository fragment holding the original shared EntityManager queries the newly mapped entity. Repository, holder, shared EntityManager and injected factory-client identities remain unchanged; the old native factory closes and an unrelated context remains accessible. A body-only save of the mapped entity does not rebuild it again. Each mapping state is compared with a fresh agentless native application and exact expected values.

This observer also verifies missing-table refusal followed by explicit synthetic schema preparation and a body-only retry, opt-out refusal and two-unit ambiguity refusal. It does not add automatic schema migration or multi-unit selection. These are scoped observations of an existing domain-root repository and a custom fragment, not a repository whose generic domain root becomes a new type or a startup-declared query that initially names an unmapped entity. Spring5/Boot2 javax persistence, enhanced existing-field edits, cold/lazy products, active transactions or retained open persistence contexts, other databases/providers, real-provider close failures and simultaneous swap/publication need separate proof.

Reload between requests

Add reloadBoundary=request to the agent arguments to keep a supported Spring MVC request from crossing a class reload. For example:

bash
java -javaagent:/path/to/reclazz-agent.jar=reloadBoundary=request -jar app.jar

If a request calls a pricing method, pauses, and calls a discount method, an ordinary reload can put the first call on the old code and the second on the new code. With this option, the request finishes before the reload starts. New requests wait while the class batch and its framework follow-up run, then enter against the updated code. The default, immediate, keeps the existing reload behavior and installs no request hook.

The agent waits up to one second for active dispatches, supported async requests and Callable workers to drain. If a long request or a breakpoint outlasts that deadline, it prints Request boundary deferred, reopens admission, and retains the queued edit. It then waits for a natural idle boundary instead of repeatedly stopping new requests. The next safe opportunity retries automatically, without another save. New edits remain queued on the same reload thread. The one-second limit covers draining requests, not applying the reload; a slow framework follow-up can keep new requests waiting longer. After thirty seconds, HEALTH reports the waiting work through the existing reload status; sessionLog records deferrals immediately.

The boundary is the execution of Spring MVC's FrameworkServlet.processRequest in any application classloader in this JVM. It supports the javax.servlet and jakarta.servlet signatures. Nested forwards/includes on the same thread stay within the outer boundary. Normal returns and exceptions both release it. AutoCompile still compiles outside the boundary and applies its compiled batch inside it.

For Spring 5 javax.servlet and Spring 6 jakarta.servlet MVC, Callable (including WebAsyncTask) and DeferredResult also retain the boundary after the initial dispatch returns. The hook uses the actual FrameworkServlet method's servlet namespace and classloader, including when both servlet APIs are visible. Producing a result or starting an async redispatch does not end protection: the native servlet completion event does. A redispatch belonging to an admitted request can proceed while reload is draining, so it can finish without waiting on its own reload. Timeout and error handling retain Spring's native results. A Callable worker that ignores cancellation stays counted until its body exits, even if the HTTP response has already timed out. An uncompleted request or worker can therefore defer a reload indefinitely; new traffic is admitted again after the one-second drain deadline.

Verified with Spring 5.3.39 / Tomcat 9.0.121 and Spring 6.1.14 / Tomcat 10.1.60 on stock SapMachine JDK 17 and 21 in current P3b evidence, using real HTTP requests on ordinary and child application classloaders. Both graphs exercise Callable/DeferredResult requests, a timed-out WebAsyncTask whose worker ignores cancellation, and the unchanged immediate-mode control. Focused native lifecycle tests cover immediate results, failure before async startup, executor rejection, cancellation before execution, interceptor errors, redispatch and native completion. This does not claim a Boot auto-configuration matrix or compatibility with other Spring/container versions.

The guarantee covers successful method-body changes during these supported lifetimes. Servlet filters outside the MVC dispatch, WebFlux, streaming/SSE, scheduled jobs and other background threads are outside it. A DeferredResult producer still running after its request completes is also outside protection. Custom async lifecycle implementations are not covered. Resource/configuration reloads retain their own documented boundaries (validated property updates use this gate). This is not a database snapshot or an all-or-nothing deployment: structural changes and a partially failed batch retain existing limitations.

Use this option at JVM startup. Attach mode, missing bootstrap support, or Spring MVC already loaded by an earlier agent refuses initialization rather than silently running without protection. A missing or failed MVC hook keeps class edits deferred. A non-MVC application therefore should use immediate. An invalid reloadBoundary value refuses initialization. Application startup itself is not aborted.

Workflow

text
1. Start the SAP Commerce server (with agent in JVM args)
   └── Reclazz banner appears in the console output

2. Edit your Java source files in IntelliJ or any editor

3. Compile with ant:
   cd hybris/bin/platform
   ant build

4. Reclazz detects the new .class files and hot-swaps them instantly
   └── "Hot-swapped: com.example.MyService (12ms)" appears in the console

5. Test your changes, no server restart needed

Use this option when you run the SAP Commerce server from IntelliJ IDEA using a Run/Debug configuration. The plugin handles everything automatically.

How It Works

When you launch a Run/Debug configuration in IntelliJ IDEA, the Reclazz plugin:

  1. Detects that the project is an SAP Commerce (Hybris) project
  2. Injects the -javaagent flag into the JVM arguments automatically
  3. Detects your JDK version (17 or 21) and whether you are using JetBrains Runtime
  4. Adds recommended JVM flags (e.g., -XX:+AllowEnhancedClassRedefinition for JBR)
  5. Connects to the agent after the server starts
  6. Watches for file changes and shows reload status in the IDE

You do not need to edit any properties files or manage the agent JAR path.

Setup

  1. Enable the plugin: Settings > Tools > Reclazz > check Enable Reclazz
  2. That's it. Run your Hybris server from IntelliJ as usual.

Workflow with ant build

This is the default and safest workflow. The IntelliJ plugin injects the agent, but compilation is still handled by ant:

text
1. Start the SAP Commerce server from IntelliJ (Run/Debug)
   └── Reclazz agent is automatically injected
   └── Plugin connects to the agent
   └── Status bar shows "Reclazz: Connected"

2. Edit your Java source files in IntelliJ

3. Compile with ant (from terminal or IntelliJ terminal):
   cd hybris/bin/platform
   ant build

4. Reclazz detects the new .class files and hot-swaps them
   └── Reclazz tool window shows reload details
   └── Status bar updates: "Reclazz: 1 reloads"

5. Test your changes immediately

Workflow with IntelliJ Build (AutoCompile mode)

If you prefer to skip ant build entirely, enable AutoCompile. In this mode, Reclazz watches .java source files and compiles them internally using the JDK compiler API (javax.tools):

  1. Go to Settings > Tools > Reclazz
  2. Check AutoCompile (compile .java files internally instead of using ant)
  3. Start the server from IntelliJ
text
1. Start the SAP Commerce server from IntelliJ (Run/Debug)
   └── Reclazz agent is injected with autoCompile=true

2. Edit a Java source file in IntelliJ

3. Save the file (Ctrl+S / Cmd+S)
   └── Reclazz detects the .java change
   └── Compiles it internally using javax.tools
   └── Hot-swaps the resulting bytecode
   └── "Compiled: MyService.java (45ms) → Hot-swapped: com.example.MyService (8ms)"

4. Test your changes immediately, no ant build, no server restart

AutoCompile treats the sources collected in one attempt as a package. All modules compile into a temporary directory, in dependency order, before any output is published. If compilation fails, the output directories stay unchanged and the running classes keep their previous version. The log names the compiler error and the number of held source files. Fixing any source retries the held sources together with the new save. Deleting a blocking source removes it and retries the remaining sources.

While one source is broken, other saves join its held package. Two saves collected in separate attempts are separate packages. This is not an editor-wide refactoring transaction. Use the normal build tool for generated code and resources that depend on build tasks or annotation-processor setup. Do not run AutoCompile and an external compiler against the same output tree at the same time.

Each successful output file is replaced through a temporary file and rename. If publication fails partway through, no agent reload starts, but earlier replacements remain on disk. The error names the failed file; fixing the I/O problem and saving again retries the package. Class loaders can observe mixed old and new files during publication. Reload and framework operations still have their existing per-class failure behavior.

For external builds, the IntelliJ plugin holds class changes from build start until successful compilation. Errors or cancellation keep them held until the next successful build. A lost connection never implies success. HEALTH reports the held count and time; a missing result produces a warning after five minutes. The BUILD protocol explains recovery and a Gradle/Maven wrapper. Without BUILD signals, the existing file-watching behavior continues.

What the Plugin Shows

  • Status bar widget: Bottom-right corner shows connection status and reload count

    • Reclazz: Idle, Plugin enabled, server not running
    • Reclazz: Connected, Agent connected, watching for changes
    • Reclazz: 5 reloads, Number of successful hot-swaps in this session
    • Reclazz: Error, Last reload failed (check tool window for details)
  • Tool window: Bottom panel labeled "Reclazz" shows a detailed log of all events:

    • Compilation results (autoCompile mode)
    • One line per reload, with the timing and what the save touched: Reloaded com.acme.OrderService (12ms): bean orderService re-created, mappings re-scanned, caches evicted. When the save added something that only a restart completes, the line ends with 1 thing now waits on a restart, ask PENDING. The same line is the status bar widget's tooltip.
    • Each framework step's own sentence, under verbose=true
    • Errors and warnings

    Export Log saves the latest 500 events received by this panel, oldest first in arrival order. This includes warnings, information, compilation and connection/disconnection events as well as reloads and errors. Each new event beyond the limit evicts the oldest, regardless of level; the export header states the retained count and limit. Export takes a consistent snapshot while new events arrive. This is in-memory history for the panel's lifetime, not a durable record of the whole session: it does not replay events from before the panel opened, the initial waiting/connection banner, or filtered heartbeats.

  • Notifications: Balloon notifications for important events (JDK detection, connection status)


Option 3: Auto-Build on Idle (Optional)

This feature is off by default and must be explicitly enabled. It can increase CPU usage because it triggers a build every time you stop typing, even for incomplete changes.

How It Works

When enabled, Reclazz monitors your editing activity. After you stop typing for a configurable threshold, it automatically triggers a build. Combined with the hot-swap pipeline, this means your changes are compiled and reloaded without any manual action.

Setup

  1. Go to Settings > Tools > Reclazz
  2. Check AutoCompile, this is required for auto-build to work
  3. The debounce timer controls how long Reclazz waits after your last edit before triggering compilation

How the Debounce Timer Works

The Debounce (ms) setting (default: 500ms) controls the delay:

text
You type... type... type... [stop]
                                  |
                                  ├── 500ms timer starts
                                  |
                                  └── No more edits detected
                                      └── Reclazz compiles the changed file
                                          └── Hot-swaps the result

If you resume typing before the timer expires, it resets. This prevents unnecessary compilations while you are still editing.

Adjusting the Timer

Setting Effect
100ms Very aggressive, compiles almost instantly after every pause. High CPU usage.
500ms (default) Good balance, compiles after a brief pause. Suitable for most workflows.
1000-2000ms Conservative, waits for a longer pause. Lower CPU usage, but slower feedback.
3000-5000ms Very conservative, only compiles after you clearly stop editing.

Adjust the timer in Settings > Tools > Reclazz > Debounce (ms).

Performance Considerations

  • CPU: Each compilation invokes the JDK compiler. Frequent compilations can increase CPU usage, especially on large files or slow machines.
  • File descriptors: The file watcher opens file descriptors for every watched directory. On large Hybris installations with many extensions, this can approach OS limits. The Startup delay setting (default: 30s) helps by delaying the watcher until the server has finished starting.
  • Incomplete code: If you pause mid-edit, Reclazz may try to compile syntactically invalid code. Compilation errors are shown in the tool window but do not affect the running server.

Recommendation

Use auto-build for rapid iteration on small changes (fixing a service method, tweaking business logic). For larger changes involving multiple files, use ant build or IntelliJ's Build Project (Ctrl+F9 / Cmd+F9) instead.


Option 4: Attach to Running Server

Use this option when the SAP Commerce server is already running without the -javaagent flag, for example, started from a terminal or by another team member. Reclazz uses the JVM Attach API to load the agent into the running process without restarting it.

How It Works

The IntelliJ plugin scans for running JVMs, identifies SAP Commerce processes (by detecting the Tanuki wrapper or Hybris bootstrap loader), and dynamically loads reclazz-agent.jar into the selected process. The agent initializes inside the running JVM and begins watching for class changes.

Requirements

  • IntelliJ IDEA and the Hybris server must run as the same OS user (JVM Attach API restriction)
  • JDK 17+ (the jdk.attach module must be available)

Usage

From the menu:

  1. Go to Tools > Attach Reclazz to Running Server
  2. If multiple SAP Commerce processes are detected, pick the one you want
  3. The agent loads and the plugin connects automatically
  4. Status bar shows "Reclazz: Connected"

From settings:

  1. Go to Settings > Tools > Reclazz
  2. In the Connection group, click Attach to Running Server...

Workflow

text
1. Start SAP Commerce from terminal (without -javaagent)
   ./hybrisserver.sh

2. In IntelliJ: Tools > Attach Reclazz to Running Server
   └── Plugin detects the running JVM
   └── Agent is loaded dynamically
   └── Reclazz banner appears in the server console
   └── Status bar shows "Reclazz: Connected"

3. Edit Java source files

4. Compile with ant:
   cd hybris/bin/platform
   ant build

5. Reclazz detects the new .class files and hot-swaps them
   └── No server restart needed

Limitations

  • JBR flags cannot be added retroactively: If the server was started without -XX:+AllowEnhancedClassRedefinition, the JVM cannot switch to enhanced redefinition mid-run. The companion-class reloader still works on the attached JVM, so structural changes are still applied, but reflective access to the new members carries the standard-JVM caveat (see below).
  • Startup delay is skipped: Since the server is already running, the agent starts watching immediately (no startupDelaySec wait).

Troubleshooting

  • "No running SAP Commerce JVM found": The server must be running before you attach. Reclazz looks for processes containing org.tanukisoftware.wrapper.WrapperSimpleApp, -Dplatform.home=, -DHYBRIS_BIN_DIR=, or de.hybris.bootstrap.loader.Loader.
  • "Permission denied": IntelliJ and the server must run as the same OS user. If the server runs as hybris and IntelliJ runs as your user, the attach will be refused.
  • "Agent is already loaded": The agent was already attached. The plugin will reconnect to the existing agent instance.

When a Reload Goes Wrong

A reload is a change to a running program, and the way back is the same as the way in: change the file back and save. The old bytes are reloaded like any other change, so an edit that throws on every request is undone in the editor, without a restart. What the agent offers on top:

Ask Answer
DIAGNOSE <class> (Tools menu, or a line on the status socket) what happened to the class the last time it was saved, and the file the bytes came from
PENDING what this session has done that only a restart completes; when the list is empty, nothing is
HEALTH reloads, failures, latency, what is watched, and a reload that is still running
sessionLog=<path> the whole session, timestamped, for reading back after the fact

When the trouble is the agent rather than the edit: excludeClasses=<pattern> takes a class out of instrumentation (method-body reloads still work for it), structuralReload=false turns the companion engine off for the session, and removing the -javaagent line runs the application with no agent at all. None of these loses application state that a restart would have kept; a restart is the last step, and PENDING says when it is the only one.

State during a reload

Keeping the JVM alive does not mean every bean keeps its state. In-place method-body redefinition retains the fields of existing objects. Spring bean refresh instead destroys and recreates a singleton; its mutable instance state resets and normal creation/destruction callbacks may run. Reclazz does not copy or restore that state. Unrelated singleton objects are not globally recreated. Recovery by saving old code does not restore resources or state lost during bean destruction, and a failed recreation is not a transaction with rollback.

Spring Data repository refresh

The Data refresh step recognizes assignability to Spring Data's actual Repository marker, including deep base-interface hierarchies. A Spring @Repository stereotype alone is an ordinary DAO, not proof of Spring Data. Repositories have a single recreation owner even when they carry that stereotype.

Warm singleton repository products are recreated once per matching local bean name/context; already-live ordinary singleton fields holding the old product are re-pointed by identity. This is not an arbitrary object-graph, collection, static/final-field or externally-held-reference rewrite. Spring-recorded dependents use the container's normal recreation behavior. Missing products or failed recreation do not produce a repository-success effect. Failures do not undo earlier destruction or independently successful sibling refreshes.

Lazy factories are not initialized for type discovery. An existing factory's cached product is read without creating it. A factory whose product is still cold can be invalidated without creating a product; the next normal lookup uses the updated factory definition. Non-singleton products are outside this refresh.

Native query edits and repeated saves are tested with Spring Data JPA 3.3.4, Spring 6.1.14, Hibernate 6.5.3.Final and H2 2.5.250 on SapMachine 17/21. Existing JPQL watcher coverage and a separate SQL @Query(nativeQuery=true) observer use repository interfaces with and without Spring's @Repository, in normal and child application loaders.

The SQL observer seeds three H2 rows, edits a predicate and scalar expressions, observes a missing-column error, recovers on the next valid save and restores the original query. Each saved version is compared with a fresh agentless Spring Data application and an exact expected result. A retained ordinary managed singleton holder follows the query edits without replacement. The current Spring-recorded dependent is recreated with the current injected repository; its old externally held instance is outside this support. Unrelated repository instances in the same and another context retain their identities, both entity-manager factories stay open and unchanged, and the row count remains three.

Successful shape-preserving JVM redefinition reaches repository refresh even though an interface has no companion metadata. New interface methods and interface default/static method hot reload are outside this acceptance. The SQL evidence is limited to scalar H2 queries, not other database dialects, native pagination, projection interfaces or parameter-binding variants. It does not certify an entity-manager-factory swap, concurrent publication, all Spring Data modules, versions, proxy shapes or custom factory behavior.

Backoffice socket actions

Existing widget action bodies use ordinary class reload. Adding a method with the native com.hybris.cockpitng.annotations.SocketEvent annotation does not register that action in already-running widgets. Reclazz records a widget-specific restart finding in PENDING; it does not claim that a definition-cache refresh registered the action. A body edit or an unrelated added helper creates no such registration finding. Backoffice label/config cache support remains separate.

Debugging Reloaded Code

Breakpoints keep working in code Reclazz has reloaded. On a stock JDK an edited method body runs in a companion class named after yours (Greeter$$Reclazz$v1), and the debugger finds it the way it finds anonymous classes and lambdas: it carries the original source file name and the new body's line numbers, so a breakpoint on a line of the edited method binds and hits, the frame shows Greeter.greet in Greeter.java, and stepping into the method is not filtered out. Set or move the breakpoint after the edit if the line you want did not exist before; a breakpoint on a line that the old body had and the new one does not has nothing to bind to, as in any rebuild.

Other Clients and Build Tools

Everything the IntelliJ plugin sees comes over a small loopback socket, and anything else can read it or send the four commands, DIAGNOSE, PENDING, HEALTH and SCAN: another IDE's extension, a Gradle or Maven build that nudges the agent when it has finished writing class files, a log shipper. The port file, the JSON lines and the commands are in protocol.md, with a Gradle and a shell recipe for SCAN.

Seeing Reloads in JDK Flight Recorder

Every reload is also a Flight Recorder event, so it sits on the same timeline as the garbage collections, safepoints and JIT compilations around it. Nothing to configure: the events are emitted whenever a recording is running.

bash
jcmd <pid> JFR.start name=reclazz settings=default
# ... save a few files ...
jcmd <pid> JFR.dump name=reclazz filename=reclazz.jfr
jfr print --events reclazz.Reload,reclazz.ReloadFailed reclazz.jfr
Event Fields
reclazz.Reload className (the JVM's name, Outer$Inner included), structural (members added or removed, as opposed to bodies changing), measured (the measured time, or -1 for one of a batch timed as a whole), shape (what changed, as v2, +1 method, when known), source (the class file the bytes came from, or the .java file that was compiled in AutoCompile mode)
reclazz.ReloadFailed className, reason, source

In JDK Mission Control they appear under the Reclazz category in the event browser. A recording of a slow session, sent along with a report, shows what the agent did and when without the console log.

Choosing the Right Mode

Scenario Recommended Mode
Server runs from terminal (./hybrisserver.sh) Manual Setup
Server runs from IntelliJ, compile with ant build IntelliJ Auto + ant build
Server runs from IntelliJ, want instant feedback IntelliJ Auto + AutoCompile
Rapid iteration on single files Auto-Build on Idle
Server already running without agent Attach to Running Server
CI/CD or headless environments Manual Setup with standalone agent

Supported JDK Providers

Reclazz works with any JDK 17+ that supports the standard Java Instrumentation API (java.lang.instrument). Structural hot-reload (adding/removing methods and fields) works on every supported vendor, the difference between enhanced-redefinition VMs (JBR/DCEVM) and standard JVMs is reflective visibility, not whether the change takes effect.

JDK Provider Method Body Structural Reload Reflective Visibility of New Members
JetBrains Runtime (JBR) Yes Yes (enhanced redefinition) Full, reflection sees new members
DCEVM Yes Yes (enhanced redefinition) Full, reflection sees new members
Oracle JDK Yes Yes (companion-class mode) Hot-compiled callers only
SapMachine Yes Yes (companion-class mode) Hot-compiled callers only
OpenJDK (generic) Yes Yes (companion-class mode) Hot-compiled callers only
Amazon Corretto Yes Yes (companion-class mode) Hot-compiled callers only
Eclipse Temurin (Adoptium) Yes Yes (companion-class mode) Hot-compiled callers only
Azul Zulu Yes Yes (companion-class mode) Hot-compiled callers only
BellSoft Liberica Yes Yes (companion-class mode) Hot-compiled callers only
GraalVM Limited Limited No (not recommended)

Important: The -XX:+AllowEnhancedClassRedefinition flag is JBR/DCEVM-only. Do not add this flag manually when using Oracle JDK, SapMachine, OpenJDK, or any other standard JVM, it will cause a startup crash. The plugin only adds this flag when it detects a compatible JDK.

GraalVM note: GraalVM has known limitations with Instrumentation.redefineClasses(). Some method body changes may fail. If you experience issues, consider switching to a standard OpenJDK or JBR.

Companion-class reflective visibility caveat

On standard JVMs, structural reloads use a hidden companion nestmate instead of modifying the original Class object (which the JVM doesn't allow). This has one consequence worth knowing:

Works after a structural reload (any JDK 17+):

  • Hot-compiled Java code that calls a new method directly, the invocation is rewritten through the companion via invokedynamic
  • Hot-compiled Java code within the owner or another reloaded watched class that reads/writes a new instance field; see the cross-class scope
  • Jackson JSON input/output through the added property adapter
  • Hybris Jalo-layer property access (jaloItem.setProperty(...))
  • Flexible search with new attribute columns (after HAC updatesystem for items.xml changes)

Needs server restart on standard JVMs (works immediately on JBR/DCEVM):

  • Class.getMethod("setNewThing", ...) on the original class
  • Reflective consumers without a supported adapter (Hybris ModelService's attribute dispatch, Gson, and Jackson use outside the property adapter's scope)
  • Groovy console reflection that reaches through ModelService.setAttributeValue / getAttributeValue

If your workflow relies heavily on reflective access to newly-added members (e.g. scripted type-system exploration), use JBR or DCEVM for full visibility. For the typical edit-compile-run loop on hand-written Java, companion-class mode is transparent.

Cross-class access to added instance fields

A separately reloaded watched caller can directly read and write instance fields added to a watched owning class on the recorded SapMachine 17/21 macOS arm64 lanes. Caller and owner-side accesses share the same field store: existing objects receive isolated first-read initializers, writes (including null values) survive later owner/caller saves, and separate objects retain separate values. The portable acceptance observes int, long, double and String fields, original fields and an unwatched JDK-field control. A null receiver throws NullPointerException, as an ordinary instance-field access does.

This repairs the earlier NoSuchFieldError case for direct access through the watched declaring type. These fixtures keep field types stable and do not certify field-type changes or every inherited-field, classloader, enhanced-JVM or value shape. Added fields still do not become native members visible on the original class. See the versioned core evidence.

Setting Up JBR

  1. Download JBR 17 or 21 from JetBrains releases
  2. Set JAVA_HOME to the JBR installation
  3. If using Manual Setup, add -XX:+AllowEnhancedClassRedefinition to your JVM args
  4. If using IntelliJ Auto mode, the plugin detects JBR automatically and adds the flag for you

Known Limitations

Added Static Field Values

On a stock JDK, a static field added after startup gets its compile-time constant or the value computed by an isolated initializer. Conditional expressions work too, for example:

java
private static final String MODE =
    System.getProperty("app.mode") != null ? "on" : "off";

Reclazz runs only the new field's initializer, including its condition and the selected branch. Nested conditions, short-circuit boolean expressions, object construction and primitive/reference results are supported when the expression can be separated. Other static blocks and existing static field values are not reset. Initializers for newly added fields run in source order. Later saves keep an initialized field's current value, including null or a value the application wrote; changing the initializer does not reinitialize that field.

The conditional path accepts forward branches that finish at one field assignment, including isolated switch expressions compiled to forward-only tableswitch or lookupswitch branches. Only the selected arm runs, including the default arm; null and primitive values are supported. An outside condition that can skip the assignment, loops, multiple assignments to the field, shared field/array writes, local variables, locking and separate void/discarded-result calls are refused. An unrelated try/catch elsewhere is allowed when its exception edges cannot cover, enter, skip or repeat the initializer. A preceding handler region must be forward-only and must not explicitly return or throw before the initializer. Catch/finally bodies are never extracted or replayed. An unsupported initializer leaves the field at its type default and the reload names the reason. Calls that compute the condition or selected value retain their usual application side effects; this does not make initialization transactional or add rollback on failure.

New Field Values on Objects That Already Existed

When a structural reload adds an instance field, objects created after the reload run the new constructor and get the field's initialiser value. An object that already existed did not, so Reclazz lifts the initialiser's own instructions out of the constructor and runs them for that object on the field's first read. Adding private final List<String> cache = new ArrayList<>(); to a live Spring singleton therefore reads a list, and private int retries = 3; reads 3. The initialiser may read the object's other fields and call its methods, which the live object has; each object computes its own value; a value the application wrote before the first read, null included, is kept; and subsequent reads reuse the stored value.

Isolated conditional expressions also work across equivalent constructor routes. For example, add this field when enabled is already part of the object:

java
private String mode = enabled ? "on" : "off";

The condition uses that object's state at the field's first read. Only the selected branch runs; nested conditions, short-circuit booleans, object construction and primitive/reference values are supported. Isolated switch expressions compiled to forward-only tableswitch or lookupswitch branches follow the same rules, including default arms and null results. Existing objects can therefore receive different values from the same added initializer. A later save preserves values already stored; an object that has not read the field yet uses the current initializer. The constructor and preceding initializer blocks are not replayed.

For this conditional path, every constructor route must reach a constructor that calls super(...) and assigns the field exactly once after that call. The extracted value instructions must match across those constructors, including operands and branch targets; debug information and label identities do not matter. A this(...) chain is supported when it reaches such a constructor and none of the delegating constructors writes the field. Missing or different assignments, unknown or cyclic delegation, reassignment of this, and control flow before the this(...)/super(...) call are refused. This is a bounded bytecode comparison, not a proof that differently written expressions are semantically equivalent.

Constructor arguments and other local variables, outside branches, loops, shared field/array writes, locking and separate void/discarded-result calls remain unsupported. An unrelated try/catch on a constructor route is allowed only when its exception edges cannot cover, enter, skip or repeat the extracted initializer or bypass the constructor initialization call. Preceding handler regions must be forward-only without explicit early return or throw; handlers are never replayed. Switch lowerings that need temporary locals or other unsupported instructions are still refused: this is not support for every compiler, string switch or pattern switch. Historical constructor arguments are not reconstructed. Newly created objects keep normal Java initialization order: field initializers run before the constructor body, so a field set by that body can still have its default value during initialization.

Concurrent first-read computations can overlap; the first stored outcome wins, including failure. Application code runs outside the store's locks; there is no rollback of initializer side effects.

A throwing initializer is isolated to that object and field. Reads return the type default (null, 0, false), and later reads do not retry in the same initializer generation. Other objects still compute their own values. Each subsequent initializer registration during structural reload creates a new generation: a failed object's next read may try the current initializer again. Successful values and application writes, including explicit null, are preserved. Removing the initializer leaves failed/unset objects at the default until a later registration supplies one again. There is no configurable retry policy or replay.

A computation still running when its generation is replaced cannot publish its old value or failure. That read returns a value already stored by the application or another reader, or the type default if none exists; a later read can initialize with the current generation. Already-running computations may overlap before a failure is recorded, but repeated reads after that failure do not start more work in the same generation.

What cannot be lifted keeps the type default (null, 0, false) on pre-existing objects, and the reload names the field and the reason:

  • an assignment that reads a constructor argument (this.upper = name.toUpperCase() in the constructor body), because the object no longer has the argument;
  • an initialiser whose control flow leaves the assignment, loops, overlaps an exception handler, has unsafe incoming or bypassing control flow, uses local variables, or shares a computation with another field.

A supported initializer can still throw when it runs on first read. That runtime failure follows the per-object behavior above; it is not an extraction refusal diagnosed during reload.

On JetBrains Runtime or DCEVM the field is a real field on the redefined class, so reflection sees it, and objects from before the reload keep the type default there.


What Cannot Be Hot-Reloaded

The action depends on which part changed. Regeneration and reload do not update the database schema or recreate the platform:

Change Why What To Do
*-items.xml changes Reclazz runs platform ant build and reloads generated bytecode; persistence changes also need a type-system/schema update Run Update Running System in HAC for new persistent attributes; original-class reflection on added members needs enhanced redefinition or restart
Generated model/DTO classes Method bodies and supported structural changes reload after code generation Use the same companion/enhanced reflection limits as handwritten classes
New extensions Extension list is fixed at startup ant all + restart
Superclass changes Reclazz keeps the loaded superclass; eligible bodies may reload, dependent methods remain pinned, and class-level blockers refuse the save Restart for the hierarchy change; check partial/refused outcomes
New JAR dependencies Classpath is fixed at JVM startup Restart
*-spring.xml and captured local Spring XML bean definitions Supported bean-definition changes are diffed and applied Unsupported changes are reported; follow that diagnostic instead of assuming every XML edit needs a restart

SAP reload outcomes and verification

An identical property save retries keys whose runtime write failed. Successful keys are acknowledged individually, so a later HAC edit to one of those keys is not overwritten while another key retries. Unresolved placeholders remain pending; applying a value does not recreate startup-only consumers.

Code generation distinguishes the watcher's own timestamp echo from changed XML contents. A second edit during ant build queues another build, even within the three-second echo window.

ImpEx auto-import refuses literal and macro-expanded REMOVE headers unless impexAllowRemove=true. Unresolved/recursive header macros are also refused; use HAC for inputs whose safety the bounded header scan cannot establish. Macros in ordinary value rows remain usable. This scan is not a full ImpEx parser or a sandbox for executable import scripts.

SAP interceptor refresh captures Spring mapping identities before bean refresh and explicitly unregisters/registers them in the tenant's registry afterwards. Missing registry/mapping or a failed update is not reported as re-registration. The registry contract is checked against the owner's SAP 2211-jdk21.8 SDK; model-save behavior additionally needs the live test extension described in the integration tests.

Native HTTP validation and overlapping-request evidence

The P3a evidence slice uses actual Tomcat/DispatcherServlet requests with native Hibernate Validator. Its constraint cases warm MVC and an already-held validator before adding, changing, removing and restoring annotations on fields that already exist in the loaded DTO. Invalid validation and malformed JSON are checked before controller side effects; an unedited DTO and context identity are controls. This is not a claim about newly added constrained fields, custom providers or every validation group/message policy.

With reloadBoundary=request, the native servlet cases hold two Callable workers across two saves. Finishing one request does not release the other worker's old code. A 503 timeout also does not release an interrupt-ignoring worker, including when the other request has completed. Once all owners finish, the latest saved version is observed with the same context identity. These controlled schedules do not establish throughput, arbitrary asynchronous work or every possible race.

Measured versions, source/artifact hashes and actual commands are in the record; other P3 MVC/data/callback families and unlisted environments remain separate.

Canonical source

This full page is generated from docs/usage.md. Edit the repository source and rebuild; do not edit generated HTML.

Search guides, changes and limits. Your query stays in this browser.

↑ ↓ to select · Enter to open · Esc to close