This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 469fa09c41 docs: document juneau-commons inject package and migration
updates
469fa09c41 is described below
commit 469fa09c4111de8bb663197c8c1fe23629cff2c7
Author: James Bognar <[email protected]>
AuthorDate: Thu May 14 18:02:45 2026 -0400
docs: document juneau-commons inject package and migration updates
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 67 ++++++++++
pages/topics/06.01.JuneauCommonsBasics.md | 1 +
pages/topics/06.02.07.JuneauCommonsInject.md | 185 +++++++++++++++++++++++++++
pages/topics/23.01.V9.5-migration-guide.md | 14 ++
4 files changed, 267 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 5c4754f4e2..d33162ed5d 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -1277,6 +1277,73 @@ The following builder-class constructors and fields have
been narrowed from `Wri
Callers that previously passed a `WritableBeanStore` still compile without
changes, since `WritableBeanStore` extends `BeanStore`.
+#### JSR-330 Alignment + Spring-Lite Additions in `commons.inject` (TODO-24)
+
+The `org.apache.juneau.commons.inject` package now presents a coherent
injection story:
+
+> *Juneau supports JSR-330 (`jakarta.inject` / `javax.inject`) annotations as
closely as our lightweight model allows, without taking a hard dependency on
`jakarta.inject-api`. Where Spring conventions add value to the lightweight
model, we adopt them under our own annotations.*
+
+All JSR/Spring annotation recognition is **by fully qualified name** (FQN), so
applications can use `jakarta.inject.Inject`, `javax.inject.Inject`,
`org.springframework.beans.factory.annotation.Autowired`, or
`org.apache.juneau.commons.inject.Inject` interchangeably — Juneau picks them
up transparently without a compile-time dependency.
+
+##### New annotations and types
+
+| Type | Purpose |
+|------|---------|
+| `@Inject` (Juneau-owned) | Marks constructor parameters, methods, or fields
as injection points. JSR-330 / Spring equivalents (`jakarta.inject.Inject`,
`javax.inject.Inject`, `@Autowired`) are recognized by FQN. |
+| `@Named` | Bean qualifier (moved from `org.apache.juneau.annotation.Named` —
see migration row below). |
+| `@Qualifier` | Meta-annotation marker used to declare custom qualifier
annotations. |
+| `@Singleton` | Scope marker. Semantics: a singleton is registered once in a
`BeanStore` and reused; no JVM-wide singleton, no proxying. |
+| `@Primary` | Disambiguates when multiple candidates of the same type exist.
Unqualified `BeanStore.getBean(type)` prefers a single `@Primary` candidate;
multiple primaries throw `BeanCreationException`. |
+| `@Order(int)` and `@Bean#priority()` | Ordering for
`BeanStore.getBeansOfType(Class)`. Lower values are higher precedence. When
both are present, `@Order` wins. |
+| `@Configuration` | Marker for types that host `@Bean` declarations. Supports
`imports = { ... }` for chaining configuration classes (deduped transitively).
`@Bean` members declared on superclasses are inherited. |
+| `@PostConstruct` / `@PreDestroy` | Lifecycle hooks. JSR-250
`jakarta.annotation` / `javax.annotation` equivalents are recognized by FQN. |
+| `@Conditional(Class<? extends Condition>)` | Predicate-driven registration
guard. |
+| `@ConditionalOnClass(String)` | Skip the annotated configuration / `@Bean`
when a class is absent from the classpath. |
+| `@ConditionalOnMissingBean(Class, name)` | Skip when a matching bean is
already registered. |
+| `@ConditionalOnProperty(name, havingValue, matchIfMissing)` | Skip /
register based on a `Settings` property. |
+| `Provider<T>` | Single-method `T get()` interface. Parameters and fields
typed as `Provider<T>` (Juneau / `jakarta.inject` / `javax.inject`) are
resolved via a `java.lang.reflect.Proxy` over a `Supplier<T>` from the bean
store. |
+| `Condition` / `ConditionContext` | SPI for custom `@Conditional` predicates
with access to the bean store, settings facade, classloader, and annotated
element. |
+| `JsrSupport` | Single utility class centralizing recognized FQN constants
(`JAKARTA_INJECT`, `SPRING_AUTOWIRED`, etc.) and matcher helpers. |
+
+##### `@Configuration` registration
+
+`WritableBeanStore.registerConfiguration(Class<?>)` and
`registerConfigurations(Class<?>...)` register a `@Configuration` class with
the store:
+
+- Static and non-static `@Bean` fields and methods are discovered.
+- `imports` are processed recursively and deduplicated; the same class
registered twice (directly or transitively) is processed only once per bean
store.
+- `@Bean` members declared on superclasses of the configuration type are
inherited; the superclass is processed before the subclass so subclass `@Bean`
methods can depend on superclass beans.
+- Duplicate `(type, name)` registrations within the same store throw
`BeanCreationException`.
+- `@Conditional` annotations on the configuration class skip the entire class
(and any imports it would have pulled in if the configuration class itself is
conditional); on individual `@Bean` members they skip only that member.
+
+##### Bean lifecycle: `AutoCloseable`
+
+`WritableBeanStore` now extends `AutoCloseable`. Calling `close()`:
+
+- Invokes `@PreDestroy` methods on all beans that were resolved through
`getBean(...)` / `getBeansOfType(...)`, in LIFO order (most-recently resolved
first).
+- Aggregates per-bean destroy failures as suppressed exceptions on a single
thrown `BeanCreationException`.
+- Marks the store as closed; subsequent mutating operations throw
`IllegalStateException`.
+- Is idempotent; a second `close()` is a no-op.
+
+`RestContext.destroy()` now invokes `beanStore.close()` after the existing
`@RestDestroy` hooks fire, so REST resources can rely on `@PreDestroy` for
cleanup of bean-store-managed dependencies.
+
+##### `@Inject` on `@Rest` resource fields and methods
+
+`@Rest` resources now have `@Inject`-annotated fields and methods populated
from the resource's bean store during context initialization, immediately
before `@PostConstruct` callbacks fire. This applies to all four recognized
FQNs (Juneau, `jakarta.inject.Inject`, `javax.inject.Inject`, Spring
`@Autowired`). `Optional<T>`, collection types, and `Provider<T>` are all
supported as field / parameter types.
+
+##### Constructor visibility for `BeanInstantiator`
+
+`BeanInstantiator` now considers **package-private** constructors as a final
fallback after public and protected, both for the target bean type and for
builder discovery. `private` constructors remain excluded (they signal "do not
instantiate"). This means `@Configuration` classes and other
framework-instantiated types no longer need to expose a `public` (or even
`protected`) constructor purely to satisfy the bean store, as long as the class
is reachable in the declaring package.
+
+##### Migration guide
+
+| Topic | Action |
+|-------|--------|
+| `org.apache.juneau.annotation.Named` | **Moved** to
`org.apache.juneau.commons.inject.Named`. Update imports — no alias is kept at
the old location. |
+| Simple-name annotation matching | Recognition is now strictly **FQN-based**.
Local `@interface Inject` / `@interface Autowired` / `@interface PostConstruct`
mocks in tests no longer trigger injection; use the real Juneau-owned types or
the canonical JSR-330 / Spring FQNs. |
+| `@PostConstruct` discovery | Now matches by FQN
(`org.apache.juneau.commons.inject.PostConstruct`,
`jakarta.annotation.PostConstruct`, `javax.annotation.PostConstruct`). Custom
annotations named `PostConstruct` are no longer picked up. |
+| `WritableBeanStore` implementations | Must now implement `void close()
throws BeanCreationException` (typically by delegating to a wrapped
`BasicBeanStore`). |
+| `@Bean` precedence chain | Unchanged from earlier in 9.5.0: Spring /
overriding-parent → `@Bean` / local entry → memoizer-backed framework default. |
+
### juneau-marshall-rdf
#### Upgraded Apache Jena to 5.6.0
diff --git a/pages/topics/06.01.JuneauCommonsBasics.md
b/pages/topics/06.01.JuneauCommonsBasics.md
index e6c422a4f3..21df3dec99 100644
--- a/pages/topics/06.01.JuneauCommonsBasics.md
+++ b/pages/topics/06.01.JuneauCommonsBasics.md
@@ -34,6 +34,7 @@ The `juneau-commons` module provides common utilities and
APIs used across the J
- **Collections** (`org.apache.juneau.commons.collections`): Enhanced
collection utilities including fluent collections, multi-maps, filtered
collections, and caching utilities
- **I/O** (`org.apache.juneau.commons.io`): File and stream utilities, console
support, MIME type detection, and path builders
+- **Inject** (`org.apache.juneau.commons.inject`): JSR-330–aligned bean store
+ Spring-lite injection layer (`@Inject`, `@Named`, `@Qualifier`, `@Singleton`,
`@Primary`, `@Order`, `@Bean`, `@Configuration`, `@Conditional`,
`@PostConstruct`, `@PreDestroy`, and `Provider<T>`) without compile-time
dependencies on `jakarta.inject-api` or `jakarta.annotation-api`
- **Reflection** (`org.apache.juneau.commons.reflect`): Comprehensive
reflection utilities for working with classes, methods, fields, and annotations
- **Settings** (`org.apache.juneau.commons.settings`): Thread-local and global
settings management with support for functional sources and stores
- **Utils** (`org.apache.juneau.commons.utils`): General utility classes for
strings, collections, classes, dates, files, and more
diff --git a/pages/topics/06.02.07.JuneauCommonsInject.md
b/pages/topics/06.02.07.JuneauCommonsInject.md
new file mode 100644
index 0000000000..ec7ec31d79
--- /dev/null
+++ b/pages/topics/06.02.07.JuneauCommonsInject.md
@@ -0,0 +1,185 @@
+---
+title: "Inject Package"
+slug: JuneauCommonsInject
+---
+
+The `org.apache.juneau.commons.inject` package provides Juneau's lightweight
injection model. The framework is **JSR-330–aligned** (`jakarta.inject` /
`javax.inject`) and layers a small set of Spring-flavored conveniences
(`@Bean`, `@Configuration`, `@Primary`, `@Order`, `@Conditional`) on top — all
under Juneau-owned annotations, and **without a compile-time dependency on
`jakarta.inject-api` or `jakarta.annotation-api`**.
+
+> Juneau supports JSR-330 (`jakarta.inject` / `javax.inject`) annotations as
closely as our lightweight model allows. Where Spring conventions add value to
the lightweight model, we adopt them under our own annotations.
+
+## What this package is — and is not
+
+| Goal | Non-goal |
+|------|----------|
+| Lightweight, predictable bean lookup with constructor / field / method
injection. | Become a full JSR-330 / CDI **provider**. |
+| FQN-based recognition of `jakarta.inject` / `javax.inject` / Spring
`@Autowired` so user code that already uses those annotations Just Works. |
Become a Spring replacement. |
+| Spring-lite conveniences (`@Bean`, `@Configuration`, `@Primary`, `@Order`,
`@Conditional`) under Juneau annotations. | Pull `jakarta.inject-api` onto the
compile classpath of `juneau-commons` or downstream modules. |
+| JSR-250 lifecycle (`@PostConstruct` / `@PreDestroy`) honored on
framework-instantiated beans. | Provide a JVM-wide singleton scope, AOP,
proxying, or scopes beyond `@Singleton`. |
+
+## Recognized annotations and types
+
+All FQN-based — Juneau picks them up transparently whether the user wrote the
Juneau-owned annotation, the JSR-330 equivalent, or the Spring equivalent.
+
+| Concern | Juneau type | JSR-330 / Spring equivalents recognized |
+|---------|-------------|----------------------------------------|
+| Injection point | `org.apache.juneau.commons.inject.Inject` |
`jakarta.inject.Inject`, `javax.inject.Inject`,
`org.springframework.beans.factory.annotation.Autowired` |
+| Named qualifier | `org.apache.juneau.commons.inject.Named` |
`jakarta.inject.Named`, `javax.inject.Named` |
+| Qualifier marker | `org.apache.juneau.commons.inject.Qualifier` |
`jakarta.inject.Qualifier`, `javax.inject.Qualifier` |
+| Singleton scope | `org.apache.juneau.commons.inject.Singleton` |
`jakarta.inject.Singleton`, `javax.inject.Singleton` |
+| Provider | `org.apache.juneau.commons.inject.Provider<T>` (single `T get()`)
| `jakarta.inject.Provider`, `javax.inject.Provider` (resolved via
`java.lang.reflect.Proxy`) |
+| Post-construct hook | `org.apache.juneau.commons.inject.PostConstruct` |
`jakarta.annotation.PostConstruct`, `javax.annotation.PostConstruct` |
+| Pre-destroy hook | `org.apache.juneau.commons.inject.PreDestroy` |
`jakarta.annotation.PreDestroy`, `javax.annotation.PreDestroy` |
+
+Juneau-only extensions:
+
+| Annotation | Purpose |
+|------------|---------|
+| `@Bean(name, value, priority)` | Marks a method or field on a
framework-instantiated host as a provider of a bean. |
+| `@Configuration(imports={...})` | Marks a type that hosts `@Bean`
declarations. `imports` chains additional configuration classes. Members
declared on superclasses are inherited. |
+| `@Primary` | Disambiguates when multiple candidates of the same type exist.
Unqualified `getBean(type)` prefers a `@Primary` candidate; multiple primaries
throw `BeanCreationException`. |
+| `@Order(int)` | Bean ordering for `getBeansOfType(Class)` — lower is higher
precedence. When both `@Order` and `@Bean#priority()` are present, `@Order`
wins. |
+| `@Conditional(Class<? extends Condition>)` | Generic predicate-driven
registration guard. |
+| `@ConditionalOnClass("fqn")` | Skip registration when a class is not on the
classpath. |
+| `@ConditionalOnMissingBean(Class, name)` | Skip when a matching bean is
already in the store. |
+| `@ConditionalOnProperty(name, havingValue, matchIfMissing)` | Skip /
register based on a `Settings` property. |
+
+`Condition` and `ConditionContext` are the SPI for custom predicates.
+
+## Core API
+
+### `BeanStore` — read-only lookup
+
+```java
+beanStore.getBean(MyService.class); // unnamed (or
@Primary among candidates)
+beanStore.getBean(MyService.class, "name"); // qualified by name
+beanStore.getBeansOfType(MyService.class); // all candidates,
ordered by @Order / priority
+beanStore.hasBean(MyService.class);
+```
+
+### `WritableBeanStore` — registration
+
+```java
+WritableBeanStore store = new BasicBeanStore(null);
+store.addBean(MyService.class, new MyService()); // unnamed
+store.addBean(MyService.class, new MyService(), "name"); // named
+store.addSupplier(MyService.class, MyService::new);
+store.addDefaultSupplier(MyService.class, () -> defaultInstance); //
framework default, lowest priority
+store.registerConfiguration(MyConfig.class); // @Configuration
with @Bean members
+```
+
+`registerConfiguration` is **idempotent at the store level** — registering the
same class twice (directly or transitively via `imports`) is processed only
once per store.
+
+### Lifecycle: `AutoCloseable`
+
+`WritableBeanStore extends AutoCloseable`. Calling `close()`:
+
+1. Invokes `@PreDestroy` on all beans that were resolved through
`getBean(...)` / `getBeansOfType(...)`, in **LIFO** order (most-recently
resolved first).
+2. Aggregates per-bean destroyer failures as suppressed exceptions on a single
`BeanCreationException`.
+3. Marks the store closed; subsequent mutating operations throw
`IllegalStateException`.
+4. Is idempotent — a second `close()` is a no-op.
+
+`RestContext.destroy()` calls `beanStore.close()` after the existing
`@RestDestroy` hooks fire.
+
+## Examples
+
+### Constructor injection (works with any recognized `@Inject`)
+
+```java
+public class MyService {
+
+ private final Repo repo;
+ private final Notifier notifier;
+
+ @Inject // org.apache.juneau.commons.inject.Inject — or jakarta / javax /
Spring @Autowired
+ public MyService(Repo repo, @Named("email") Notifier notifier) {
+ this.repo = repo;
+ this.notifier = notifier;
+ }
+}
+```
+
+### `@Configuration` with imports, superclass inheritance, and `@Conditional`
+
+```java
+@Configuration(imports = { CommonConfig.class })
+public class AppConfig extends BaseConfig {
+
+ @Bean public Repo repo() { return new JdbcRepo(); }
+
+ @Bean @Primary public Notifier emailNotifier() { return new
EmailNotifier(); }
+
+ @Bean @ConditionalOnClass("redis.clients.jedis.Jedis")
+ public Cache cache() { return new RedisCache(); }
+}
+```
+
+`AppConfig` inherits `@Bean` members declared on `BaseConfig`. The `Cache`
bean only registers if `Jedis` is on the classpath.
+
+### `Provider<T>` injection
+
+```java
+public class LazyConsumer {
+ @Inject jakarta.inject.Provider<HeavyService> provider;
+
+ void onRequest() {
+ var svc = provider.get(); // resolved from the bean store per-call
+ }
+}
+```
+
+Juneau resolves the `jakarta.inject.Provider<T>` parameter via a
`java.lang.reflect.Proxy` over a `Supplier<T>` from the bean store — no Spring
or `jakarta.inject-api` jar required at compile time.
+
+### `@PostConstruct` / `@PreDestroy`
+
+```java
+public class CacheService implements AutoCloseable {
+
+ @Inject Settings settings;
+
+ @PostConstruct
+ public void start() { /* warm up */ }
+
+ @PreDestroy
+ public void shutdown() { /* flush + close */ }
+}
+```
+
+`@PreDestroy` fires only on beans that were resolved from the store, in LIFO
order, when `WritableBeanStore.close()` is called.
+
+## `@Inject` on `@Rest` resource fields
+
+REST resources have `@Inject`-annotated fields and methods populated from the
resource's bean store during context initialization, immediately before
`@PostConstruct` callbacks fire.
+
+```java
+@Rest
+public class MyResource {
+
+ @Bean public Repo repo() { return new JdbcRepo(); }
+
+ @Inject Repo repo; // populated by RestContext
+
+ @jakarta.inject.Inject Notifier notifier; // also populated (FQN match)
+
+ @PostConstruct void init() { /* fields are ready here */ }
+}
+```
+
+## Constructor visibility
+
+When `BeanInstantiator` constructs a bean (including a `@Configuration`
class), it searches declared constructors in this order, taking the
most-specific resolvable constructor at each level:
+
+1. `public`
+2. `protected`
+3. **package-private** (default access)
+
+`private` constructors are deliberately excluded — they signal "do not
instantiate". The same ladder is used for builder discovery on the bean's
builder type. In practice this means a `@Configuration` class (or any other
bean type instantiated through the framework) does not need to expose a
`public` constructor purely to satisfy the bean store, as long as the class is
reachable in the declaring package.
+
+## Deviations from full JSR-330
+
+These are intentional simplifications:
+
+- No circular-dependency resolver — cycles are caller-visible.
+- No Scope SPI beyond `@Singleton`. Custom scopes are not supported in v1.
+- No qualifier-attribute equality (Spring-style); qualifiers compare by
**annotation type** and by `@Named` value.
+- No AOP / proxying except the `Provider<T>` adapter described above.
+- Field / method injection of `@Inject` is honored on
**framework-instantiated** types (e.g. `@Rest` resources). The framework does
not scan arbitrary user objects.
diff --git a/pages/topics/23.01.V9.5-migration-guide.md
b/pages/topics/23.01.V9.5-migration-guide.md
index f10d3eb007..5e57bb04d8 100644
--- a/pages/topics/23.01.V9.5-migration-guide.md
+++ b/pages/topics/23.01.V9.5-migration-guide.md
@@ -222,5 +222,19 @@ Several previously `protected` or package-private members
were widened to `publi
- `BeanPropertyMeta.Builder` fields `innerField`, `getter`, `setter`,
`rawTypeMeta`, `swap`, `readTransform`, `writeTransform`, `dictionaryClasses`,
`isUri`, `typeMeta` widened package-private → `public`.
- `BeanPropertyValue#properties()` widened `protected` → `public`.
+## JSR-330 Alignment + Spring-Lite Additions in `commons.inject` (TODO-24)
+
+The `org.apache.juneau.commons.inject` package now exposes a JSR-330–aligned
injection surface plus a Spring-lite configuration / conditional / lifecycle
layer. Recognition of injection-related annotations is by **fully qualified
name** — applications can drop in `jakarta.inject.Inject`, Spring `@Autowired`,
etc., without Juneau pulling either API onto the compile classpath.
+
+| Old | New | Notes |
+|-----|-----|-------|
+| `org.apache.juneau.annotation.Named` |
`org.apache.juneau.commons.inject.Named` | Moved; no deprecated alias is kept
at the old location. Update imports. |
+| Local test `@interface Inject` / `@interface Autowired` / `@interface
PostConstruct` matched the framework by simple name. | Recognition is now
strictly **FQN-based** via `JsrSupport`. Replace test mocks with the real
Juneau-owned types (`org.apache.juneau.commons.inject.Inject` /
`PostConstruct`), or declare stand-in types at the canonical FQN
(`jakarta.inject.Inject`,
`org.springframework.beans.factory.annotation.Autowired`) on the test
classpath. | Affects code that relied on simple [...]
+| `@PostConstruct` discovered by simple class name. | Discovered by FQN
(`org.apache.juneau.commons.inject.PostConstruct`,
`jakarta.annotation.PostConstruct`, `javax.annotation.PostConstruct`). | Custom
annotations named `PostConstruct` no longer trigger lifecycle invocation. |
+| `WritableBeanStore` did not implement `AutoCloseable`. | `WritableBeanStore
extends AutoCloseable`. `close()` invokes `@PreDestroy` on resolved beans in
LIFO order, aggregates failures as suppressed exceptions, and marks the store
closed. | Custom implementations must add `void close() throws
BeanCreationException` (usually by delegating to a wrapped `BasicBeanStore`).
`RestContext.destroy()` now wires this in automatically. |
+| n/a | New annotations: `@Inject`, `@Qualifier` (meta), `@Singleton`,
`@Primary`, `@Order(int)`, `@Configuration`, `@PostConstruct`, `@PreDestroy`,
`@Conditional`, `@ConditionalOnClass`, `@ConditionalOnMissingBean`,
`@ConditionalOnProperty`. New types: `Provider<T>`, `Condition`,
`ConditionContext`, `JsrSupport`. | All live in
`org.apache.juneau.commons.inject`. See the 9.5 release notes for full
semantics. |
+| `@Bean` had only `name()` / `value()` / `methodScope()` / `description()`. |
New `int priority() default Integer.MAX_VALUE/2;` attribute provides bean
ordering when `@Order` is absent. | Existing usages unchanged; the attribute
defaults to mid-range so previously-unordered collections retain stable
behavior. |
+| n/a | `WritableBeanStore.registerConfiguration(Class)` /
`registerConfigurations(Class...)` register a `@Configuration` class,
recursively processing `imports`, deduplicating across calls, inheriting
`@Bean` members from superclasses (parent-first), and honoring `@Conditional`
annotations at both class and member level. | Duplicate `(type, name)`
registrations throw `BeanCreationException`. Class-level conditional failures
cascade — the configuration and its imports are silently skipped. |
+
<!-- Additional rows will be populated as 9.5 breaking changes land. See
todo/TODO-17 for the
ongoing 9.5.0 audit. -->