Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions api/src/main/java/jakarta/enterprise/context/Eager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2025, Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* Apache Software License 2.0 which is available at:
* https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: Apache-2.0
*/

package jakarta.enterprise.context;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.enterprise.util.AnnotationLiteral;

/**
* Marks the annotated bean as eagerly initialized. May be applied to a bean class,
* producer method or field or {@linkplain jakarta.enterprise.inject.Stereotype stereotype}.
* <p>
* Only {@link ApplicationScoped @ApplicationScoped} beans may be eagerly initialized.
* <p>
* Contextual instance of an eagerly initialized bean is created no later than at the moment
* the container fires the {@link jakarta.enterprise.event.Startup Startup} event.
*
* @since 5.0
*/
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Eager {
Comment on lines 21 to 35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feeling is that many more people read the javadoc than read the spec so I think we should include the following information here:

  • Eager is equivalent to observing Startup
  • Eager may only be used on ApplicationScoped beans
  • Eager can be applied to a managed bean class, a producer method or a producer field

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I had a similar text to the spec in the javadoc, but decided against it, because it's relatively dense and thick. I'll add something lighter back.

Also, this reminds me I have to add support for synthetic beans -- I had it in mind originally, but forgot. I'll fix that too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I addressed this particular comment.

Locally, I have code that allows synthetic beans to be eagerly initialized, but I think I need to prototype an implementation at least to some extent to see whether the API I've got makes sense.

Copy link
Contributor

@hantsy hantsy Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feeling is that many more people read the javadoc than read the spec so I think we should include the following information here:

* `Eager` is equivalent to observing `Startup`

* `Eager` may only be used on `ApplicationScoped` beans

* `Eager` can be applied to a managed bean class, a producer method or a producer field

And the JavaDoc here confused me.

Contextual instance of an eagerly initialized bean is created at the same time
the {@link jakarta.enterprise.event.Startup Startup} event is fired.

If the eager is inverting the lazy proxy creation. I do not think it is equivalent to @Observes StarupEvent.

It should be related to how to handle the bean instantiation. Firstly creating an empty proxy like normal, then immediately execute more steps:

  1. Inject the dependencies
  2. Call the managed bean lifecycle @PostConstruct method.

In theory, any scoped beans could be operated like this. Even applicationScoped bean looks more reasonable.

In contrast to Spring Boot ApplicationReady event, the bean initialization does not depend on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the eager is inverting the lazy proxy creation. I do not think it is equivalent to @observes StarupEvent.

I am not sure I understand your comment but it is equivalent.
In order for CDI container to notify a bean of such an event, it first creates its instance which includes injecting into it and calling its post construct method. This is a shorthand for the same.

In theory, any scoped beans could be operated like this. Even applicationScoped bean looks more reasonable.

See the original CDI issue, it mentioned several variants but in the end we decided to start smaller and only define it for app scoped beans which are the most common usage we know/heard of.
FTR, you can achieve the same for any scope and with the addition of BeanContainer#unwrapClientProxy it should be fairly simple.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very much not true. As mentioned above, the only thing that @Eager does is forcing the contextual instance of the bean into existence early on. If there's another bean calling into the @Eager bean before that eager bean was initialized (most likely because the other bean is also @Eager and calls into the first bean from a @PostConstruct callback or something like that), the instance will be created during that call.

Copy link
Member

@Azquelt Azquelt Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is the nice thing about making an @Eager bean with a @PostConstruct method, vs observing Startup and doing any setup logic in the observer method; @PostConstruct is guaranteed to run before the bean is used, whereas observer methods are called in priority order.

All that @Eager does is make sure that an instance of the bean is created when the Startup event is fired.

Would it be clearer to say this?

A contextual instance of an eagerly initialized bean is created at the same time
the {@link jakarta.enterprise.event.Startup Startup} event is fired, if one does
not exist.

I can't decide if that detail is helpful to have in the Javadoc or just makes the sentence more confusing.

Edit: this might be better:

The contextual instance of an eagerly initialized bean is created at the same time
the {@link jakarta.enterprise.event.Startup Startup} event is fired, if it has not
already been created.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather say

Contextual instance of an eagerly initialized bean is created at the same time no later than the {@link jakarta.enterprise.event.Startup Startup} event is fired.

Or maybe reword it more:

Eagerly initialized bean is guaranteed to be created at latest when the Startup event is fired.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, I think the CDI implementor has a mechanism that can track all @Eager beans and make sure all are initialized successfully, then fire the StartUpEvent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand the difference between "immediately before Startup" and "during Startup at priority Integer.MIN_VALUE", but sure, we can say "immediately before firing Startup".

Aside:

fire the StartUpEvent

Please stop calling it StartupEvent or StartUpEvent, it's called Startup.

/**
* Supports inline instantiation of the {@link Eager} annotation.
*/
final class Literal extends AnnotationLiteral<Eager> implements Eager {
public static final Literal INSTANCE = new Literal();

private static final long serialVersionUID = 1L;

private Literal() {
}
}
}
5 changes: 5 additions & 0 deletions api/src/main/java/jakarta/enterprise/inject/Reserve.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
* <p>
* Unlike {@linkplain Alternative alternatives}, reserves cannot be selected for a bean archive in {@code beans.xml}.
* </p>
*
* @since 5.0
*/
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
Expand All @@ -59,5 +61,8 @@ final class Literal extends AnnotationLiteral<Reserve> implements Reserve {
public static final Literal INSTANCE = new Literal();

private static final long serialVersionUID = 1L;

private Literal() {
}
}
}
18 changes: 17 additions & 1 deletion api/src/main/java/jakarta/enterprise/inject/Stereotype.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
* <li>all beans with the stereotype have defaulted bean names, or that</li>
* <li>all beans with the stereotype are alternatives, or that</li>
* <li>all beans with the stereotype have predefined {@code @Priority}, or that</li>
* <li>all beans with the stereotype are reserves.</li>
* <li>all beans with the stereotype are reserves, or that</li>
* <li>all beans with the stereotype are eagerly initialized.</li>
* </ul>
*
* <p>
Expand Down Expand Up @@ -149,6 +150,21 @@
* </pre>
*
* <p>
* A stereotype may declare an {@link jakarta.enterprise.context.Eager &#064;Eager} annotation, which specifies
* that every bean with the stereotype is eagerly initialized.
* </p>
*
* <pre>
* &#064;ApplicationScoped
* &#064;Eager
* &#064;Stereotype
* &#064;Target(TYPE)
* &#064;Retention(RUNTIME)
* public @interface InitOnStartup {
* }
* </pre>
*
* <p>
* A stereotype may declare other stereotypes. Stereotype declarations are transitive. A stereotype declared by a second
* stereotype is inherited by all beans and other stereotypes that declare the second stereotype.
* </p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public interface BeanInfo {
* Returns whether this bean is a {@linkplain jakarta.enterprise.inject.Reserve reserve}.
*
* @return whether this bean is a {@linkplain jakarta.enterprise.inject.Reserve reserve}
* @since 5.0
*/
boolean isReserve();

Expand All @@ -138,6 +139,14 @@ public interface BeanInfo {
*/
Integer priority();

/**
* Returns whether this bean is {@linkplain jakarta.enterprise.context.Eager eagerly initialized}.
*
* @return whether this bean is {@linkplain jakarta.enterprise.context.Eager eagerly initialized}
* @since 5.0
*/
boolean isEager();

/**
* Returns the bean name of this bean. A bean name is usually defined
* using the {@link jakarta.inject.Named @Named} annotation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
* a set of {@linkplain #interceptorBindings() interceptor bindings},
* default {@linkplain #priority() priority}, whether all beans with
* the stereotype are {@linkplain #isAlternative() alternatives}
* or {@linkplain #isReserve() reserves}, or have {@linkplain #isNamed() default names}.
* or {@linkplain #isReserve() reserves}, are {@linkplain #isEager() eagerly initialized},
* or have {@linkplain #isNamed() default names}.
*
* @since 4.0
*/
Expand Down Expand Up @@ -53,6 +54,7 @@ public interface StereotypeInfo {
* This means that all beans with this stereotype are reserves.
*
* @return whether this stereotype is meta-annotated {@link jakarta.enterprise.inject.Reserve @Reserve}
* @since 5.0
*/
boolean isReserve();

Expand All @@ -68,6 +70,15 @@ public interface StereotypeInfo {
*/
Integer priority();

/**
* Returns whether this stereotype is meta-annotated {@link jakarta.enterprise.context.Eager @Eager}.
* This means that all beans with this stereotype are eagerly initialized.
*
* @return whether this stereotype is meta-annotated {@link jakarta.enterprise.context.Eager @Eager}
* @since 5.0
*/
boolean isEager();

/**
* Returns whether this stereotype is meta-annotated {@link jakarta.inject.Named @Named}.
* This means that all beans with this stereotype have default bean names.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public interface SyntheticBeanBuilder<T> {
* @return this {@code SyntheticBeanBuilder}
* @throws IllegalStateException if this method is called multiple times
* or if the bean was previously marked as {@linkplain #alternative(boolean) alternative}.
* @since 5.0
*/
SyntheticBeanBuilder<T> reserve(boolean isReserve);

Expand All @@ -155,6 +156,21 @@ public interface SyntheticBeanBuilder<T> {
*/
SyntheticBeanBuilder<T> priority(int priority);

/**
* Marks this synthetic bean as a {@linkplain jakarta.enterprise.context.Eager eagerly initialized} if desired.
* <p>
* If not called, this synthetic bean will not be eagerly initialized.
* <p>
* If this synthetic bean does not have a scope that permits eager initialization,
* the synthetic bean registration will fail.
*
* @param isEager whether this synthetic bean should be eagerly initialized
* @return this {@code SyntheticBeanBuilder}
* @throws IllegalStateException if this method is called multiple times
* @since 5.0
*/
SyntheticBeanBuilder<T> eager(boolean isEager);

/**
* Sets the bean name of this synthetic bean. If {@code beanName} is {@code null},
* this synthetic bean will not have a name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public interface AfterTypeDiscovery {
/**
* @return the list of enabled reserves for the application, sorted by priority in ascending order.
* @throws IllegalStateException if called outside of the observer method invocation
* @since 5.0
*/
public List<Class<?>> getReserves();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,23 @@ public interface BeanAttributes<T> {
*
* @return <code>true</code> if the bean is a {@linkplain Reserve reserve}, and <code>false</code>
* otherwise.
* @since 5.0
*/
public default boolean isReserve() {
// `default` to avoid breaking older `Bean` implementations
return false;
}

/**
* Determines if the bean is {@linkplain jakarta.enterprise.context.Eager eagerly initialized}.
*
* @return <code>true</code> if the bean is {@linkplain jakarta.enterprise.context.Eager eagerly initialized},
* and <code>false</code> otherwise.
* @since 5.0
*/
public default boolean isEager() {
// `default` to avoid breaking older `Bean` implementations
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ public interface BeanManager extends BeanContainer {
*
* @param <T> the type
* @param attributes a {@link BeanAttributes} which determines the bean types, qualifiers, scope, name and stereotypes of
* the returned {@link Bean}, and the return value of {@link Bean#isAlternative()} and {@link Bean#isReserve()}
* the returned {@link Bean}, and the return value of {@link Bean#isAlternative()}, {@link Bean#isReserve()},
* and {@link Bean#isEager()}
* @param beanClass a class, which determines the return value of {@link Bean#getBeanClass()}
* @param injectionTargetFactory an {@link InjectionTargetFactory}, used to obtain an {@link InjectionTarget}
* @return a container provided implementation of {@link Bean}
Expand All @@ -343,7 +344,8 @@ public <T> Bean<T> createBean(BeanAttributes<T> attributes, Class<T> beanClass,
* @param <T> the type
* @param <X> the type of the declaring bean
* @param attributes a {@link BeanAttributes} which determines the bean types, qualifiers, scope, name and stereotypes of
* the returned {@link Bean}, and the return value of {@link Bean#isAlternative()} and {@link Bean#isReserve()}
* the returned {@link Bean}, and the return value of {@link Bean#isAlternative()}, {@link Bean#isReserve()},
* and {@link Bean#isEager()}
* @param beanClass a class, which determines the return value of <code>Bean.getClass()</code>
* @param producerFactory a {@link ProducerFactory}, used to obtain a {@link Producer}
* @return a container provided implementation of {@link Bean}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,18 @@ public interface BeanAttributesConfigurator<T> {
*
* @param value value for reserve property
* @return self
* @since 5.0
*/
BeanAttributesConfigurator<T> reserve(boolean value);

/**
* Change the eagerly initialized status of the configured bean.
* By default, the configured bean is not eagerly initialized.
*
* @param value value for eagerly initialized property
* @return self
* @since 5.0
*/
BeanAttributesConfigurator<T> eager(boolean value);

}
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public interface BeanConfigurator<T> {

/**
* Change the alternative status of the configured bean.
* By default the configured bean is not an alternative.
* By default, the configured bean is not an alternative.
*
* @param value value for alternative property
* @return self
Expand All @@ -319,10 +319,11 @@ public interface BeanConfigurator<T> {

/**
* Change the reserve status of the configured bean.
* By default the configured bean is not a reserve.
* By default, the configured bean is not a reserve.
*
* @param value value for reserve property
* @return self
* @since 5.0
*/
BeanConfigurator<T> reserve(boolean value);

Expand All @@ -342,4 +343,14 @@ public interface BeanConfigurator<T> {
*/
BeanConfigurator<T> priority(int priority);

/**
* Change the eagerly initialized status of the configured bean.
* By default, the configured bean is not eagerly initialized.
*
* @param value value for eagerly initialized property
* @return self
* @since 5.0
*/
BeanConfigurator<T> eager(boolean value);

}
37 changes: 36 additions & 1 deletion spec/src/main/asciidoc/core/definition.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,20 @@ If a reserve managed bean declares an alternative producer, the container automa

If an interceptor is a reserve, non-portable behavior results.

[[eager_initialization]]

=== Eager initialization

A bean class of a managed bean, a producer method and a producer field may be annotated `@Eager`.
This annotation marks the bean as _eagerly initialized_.

Contextual instance of an eagerly initialized bean is created no later than at the moment the container fires <<startup_event, the `Startup` event>>.
For example, it is as if an enabled bean existed that declared a synchronous observer with observed event type of `Startup`, no observed event qualifier, priority of `Integer.MIN_VALUE`, and this observer method obtained a contextual instance of the eagerly initialized bean.

Only `@ApplicationScoped` beans may be eagerly initialized.
If a bean of any other normal scope or the `@Dependent` pseudo-scope is eagerly initialized, definition error occurs.
If a bean of any other pseudo-scope is eagerly initialized, non-portable behavior results.

[[stereotypes]]

=== Stereotypes
Expand All @@ -769,7 +783,8 @@ A stereotype may also specify that:
* all beans with the stereotype have defaulted bean names, or that
* all beans with the stereotype are alternatives, or that
* all beans with the stereotype have predefined `@Priority`, or that
* all beans with the stereotype are reserves.
* all beans with the stereotype are reserves, or that
* all beans with the stereotype are eagerly initialized.

A bean may declare zero, one or multiple stereotypes.

Expand Down Expand Up @@ -917,6 +932,26 @@ public @interface DefaultImpl {}

If a stereotype declares both `@Alternative` and `@Reserve`, the container automatically detects the problem and treats it as a definition error.

[[eager_init_stereotype]]

===== Declaring an `@Eager` stereotype

A stereotype may declare an `@Eager` annotation, which specifies that every bean with the stereotype is eagerly initialized.

We may specify that all beans annotated with `@InitOnStartup` are `@ApplicationScoped` and eagerly initialized:

[source, java]
----
@ApplicationScoped
@Eager
@Stereotype
@Target(TYPE)
@Retention(RUNTIME)
public @interface InitOnStartup {}
----

If a stereotype declares both `@Eager` and a default scope for which eager initialization is not permitted (see <<eager_initialization>>), the container automatically detects the problem and treats it as a definition error.

[[stereotypes_with_additional_stereotypes]]

===== Stereotypes with additional stereotypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ The following built-in annotations define a `Literal` static nested class to sup
* `jakarta.enterprise.inject.Specializes`
* `jakarta.enterprise.inject.Vetoed`
* `jakarta.enterprise.util.Nonbinding`
* `jakarta.enterprise.context.Eager`
* `jakarta.enterprise.context.Initialized`
* `jakarta.enterprise.context.Destroyed`
* `jakarta.enterprise.context.RequestScoped`
Expand Down
6 changes: 4 additions & 2 deletions spec/src/main/asciidoc/core/spi_full.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ public interface BeanAttributes<T> {
public Set<Class<? extends Annotation>> getStereotypes();
public boolean isAlternative();
public boolean isReserve();
public boolean isEager();
}
----

* `getTypes()`, `getQualifiers()`, `getScope()`, `getName()` and `getStereotypes()` must return the bean types, qualifiers, scope type, bean name and stereotypes of the bean, as defined in <<concepts>>.
* `isAlternative()` must return `true` if the bean is an alternative, and `false` otherwise.
* `isReserve()` must return `true` if the bean is a reserve, and `false` otherwise.
* `isEager()` must return `true` if the bean is eagerly initialized, and `false` otherwise.

The interface `jakarta.enterprise.inject.spi.Bean` defines everything the container needs to manage instances of a certain bean.

Expand Down Expand Up @@ -505,7 +507,7 @@ public BeanAttributes<?> createBeanAttributes(AnnotatedMember<?> member);

The method `BeanManager.createBean()` returns a container provided implementation of `Bean`. The methods accept:

* a `BeanAttributes`, which determines the bean types, qualifiers, scope, name and stereotypes of the returned `Bean`, the return values of `isAlternative()` and `isReserve()`, and
* a `BeanAttributes`, which determines the bean types, qualifiers, scope, name and stereotypes of the returned `Bean`, the return values of `isAlternative()`, `isReserve()`, and `isEager()`, and
* a class, which determines the return value of `Bean.getClass()`, and
* an `InjectionTargetFactory`, which is used to obtain an `InjectionTarget`. The `InjectionTarget` is used to create and destroy instances of the bean, to perform dependency injection and lifecycle callbacks, and which determines the return value of `Bean.getInjectionPoints()`.

Expand All @@ -519,7 +521,7 @@ public <T> Bean<T> createBean(BeanAttributes<T> attributes, Class<T> beanClass,
A second version of the method is provided to create a `Bean` from a producer.
The method accepts:

* a `BeanAttributes`, which determines the bean types, qualifiers, scope, name and stereotypes of the returned `Bean`, the return values of `isAlternative()` and `isReserve()`, and
* a `BeanAttributes`, which determines the bean types, qualifiers, scope, name and stereotypes of the returned `Bean`, the return values of `isAlternative()`, `isReserve()`, and `isEager()`, and
* a class, which determines the return value of `Bean.getClass()`, and
* a `ProducerFactory`, which is used to obtain a `Producer`. The `Producer` is used to create and destroy instances of the bean, and which determines the return value of `Bean.getInjectionPoints()`.

Expand Down
1 change: 1 addition & 0 deletions spec/src/main/asciidoc/core/spi_lite.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ public interface BeanInfo {
boolean isAlternative();
boolean isReserve();
Integer priority();
boolean isEager();
String name();
DisposerInfo disposer();
Collection<StereotypeInfo> stereotypes();
Expand Down