Skip to content

Conversation

Ladicek
Copy link
Contributor

@Ladicek Ladicek commented Jan 13, 2025

Injection points are registered on a SyntheticBeanBuilder using withInjectionPoint(). The synthetic bean creation/destruction functions can look up injectable references for registered injection points from SyntheticInjections.

The SyntheticBeanCreator and SyntheticBeanDisposer interfaces used to have one method. That method is now deprecated for removal and a second method is added. The second method no longer has an Instance<Object> parameter for arbitrary lookups; instead, is has a parameter of type SyntheticInjections for pre-registered injections.

@Ladicek Ladicek added this to the CDI 5.0 milestone Jan 13, 2025
@Ladicek
Copy link
Contributor Author

Ladicek commented Jan 13, 2025

This is a draft, because it's very much not ready. The biggest open question is: does SyntheticBeanBuilder.withInjectionPoint() in any way affect the Instance<Object> lookup parameter in SyntheticBeanCreator / SyntheticBeanDisposer?

One way would be to say something like:

Implementations are allowed to only support looking up beans from {@code lookup} that have previously been registered using {@code withInjectionPoint()}.

That is obviously a breaking change, but I don't see a better way that wouldn't be more breaking.

@Ladicek
Copy link
Contributor Author

Ladicek commented Jan 13, 2025

CC @manovotn @mkouba

@mkouba
Copy link
Contributor

mkouba commented Jan 13, 2025

This is a draft, because it's very much not ready. The biggest open question is: does SyntheticBeanBuilder.withInjectionPoint() in any way affect the Instance<Object> lookup parameter in SyntheticBeanCreator / SyntheticBeanDisposer?

One way would be to say something like:

Implementations are allowed to only support looking up beans from {@code lookup} that have previously been registered using {@code withInjectionPoint()}.

That is obviously a breaking change, but I don't see a better way that wouldn't be more breaking.

👍 Although a breaking change it makes perfect sense IMO.

@Azquelt
Copy link
Member

Azquelt commented Jan 13, 2025

What are you trying to fix by adding this?

I don't see an explanation of the problem or a linked issue.

@mkouba
Copy link
Contributor

mkouba commented Jan 13, 2025

What are you trying to fix by adding this?

I don't see an explanation of the problem or a linked issue.

In a build-time environment, such as Quarkus, injection point metadata has to be recorded for any @Dependent synthetic bean since we don't know if it defines an InjectionPoint dependency or not. In other words, the current API prevents some build-time optimizations and enforces unnecessary bytecode generation and reflection.

@Ladicek
Copy link
Contributor Author

Ladicek commented Jan 13, 2025

What @mkouba says is just the reason why I'm submitting this PR now.

The ultimate cause is feature parity -- Portable Extensions allow this, while Build Compatible Extensions do not.

@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch from 03541db to 039589f Compare January 14, 2025 15:41
@Ladicek
Copy link
Contributor Author

Ladicek commented Jan 14, 2025

This is a draft, because it's very much not ready. The biggest open question is: does SyntheticBeanBuilder.withInjectionPoint() in any way affect the Instance<Object> lookup parameter in SyntheticBeanCreator / SyntheticBeanDisposer?

One way would be to say something like:

Implementations are allowed to only support looking up beans from {@code lookup} that have previously been registered using {@code withInjectionPoint()}.

That is obviously a breaking change, but I don't see a better way that wouldn't be more breaking.

I eventually decided that this is something we should keep doing in Quarkus, outside of the specification. The spec should stay silent on this.

@Ladicek Ladicek marked this pull request as ready for review January 14, 2025 15:43
@Azquelt
Copy link
Member

Azquelt commented Jan 14, 2025

Thanks @Ladicek for the explanation on the call. As I understand it now:

With portable extensions, you can create a synthetic bean by implementing Bean<T> which also allows you to implement the getInjectionPoints method. The injection points specified here aren't used in the creation of the bean instance, but are validated by the container and are available for inspection and validation by other extensions observing ProcessBean.

Other extensions can also modify the injection points by observing ProcessInjectionPoint, but any modifications would likely be ignored by the synthetic bean.

As far as I can see, there's no way to link an injection point to an injected dependent bean to allow the dependent bean to inspect where it's been injected.

This PR would add equivalent functionality to build compatible extensions. Injection points can be added to the synthetic bean and will be validated by the container and available for other extensions to inspect with @Registration and BeanInfo.

Additionally, Quarkus want the ability to do some additional out-of-spec optimisations (such as removing any beans which aren't needed), and having this functionality in build compatible extensions would add a way for the user to indicate that certain beans are used by their synthetic bean and can't be optimised away.

@Ladicek Ladicek marked this pull request as draft January 15, 2025 12:33
@Ladicek
Copy link
Contributor Author

Ladicek commented Jan 15, 2025

Actually, let me push this PR back to draft, because there's a bit of a conflict we need to resolve: the Portable Extensions API allows this, but expect a direct implementation of InjectionPoint, which contains a lot more than just type and qualifiers. And at least in Weld, those extra members are used in validation, for example. So we need to unify those two APIs somehow...

@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch from 039589f to 591d477 Compare July 4, 2025 12:20
@Ladicek Ladicek changed the title add SyntheticBeanBuilder.withInjectionPoint() add support for synthetic bean injection points Jul 4, 2025
@Ladicek
Copy link
Contributor Author

Ladicek commented Jul 4, 2025

I found some time and updated the proposal with a somewhat better (I believe) API. The main question remains (and it's why I'm keeping this PR as a draft): how can we bridge this with Portable Extensions, which expect full blown InjectionPoint? Need to discuss with @manovotn, because I'm no expert in that area.

@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch 2 times, most recently from da88af4 to bc1874b Compare July 4, 2025 12:46
@manovotn
Copy link
Contributor

I found some time and updated the proposal with a somewhat better (I believe) API. The main question remains (and it's why I'm keeping this PR as a draft): how can we bridge this with Portable Extensions, which expect full blown InjectionPoint? Need to discuss with @manovotn, because I'm no expert in that area.

Pardon my ignorance, I am pretty sure we discussed this but I don't see it recorder here - what was the issue with introducing SyntheticInjectionPoint interface (a parent of jakarta.enterprise.inject.spi.InjectionPoint) that would have just getters for type and qualifiers?
I know Weld ATM assumes the other properties of the IP are non-null but that seems mendable.

@Ladicek
Copy link
Contributor Author

Ladicek commented Jul 15, 2025

I don't think there's an issue with that. (Besides the name. We need a good name.) If you think that's a reasonable way forward, I'll try to figure something out.

@manovotn
Copy link
Contributor

I don't think there's an issue with that. (Besides the name. We need a good name.) If you think that's a reasonable way forward, I'll try to figure something out.

It does sound reasonable to me, but we can discuss it further in the mtg today (assuming you are coming).

@manovotn
Copy link
Contributor

@Ladicek @Azquelt based on our discussion during the mtg, I checked for ProcessInjectionPoint event and it does not occur when the IP is defined by a synthetic bean.
[In order to check this, I used the TCK test BeanConfiguratorTest which has several synth beans declaring injection points, so you can just add PIP observer and see it right away]

It should therefore be safe to conclude that the synthetic IPs we are discussing here should not trigger the event either.
Which in turn means that adding the BaseInjectionPoint (a parent of current InjectioPoint) should be safe as users cannot really get their hands on an IP that would have null member value. Implementations will of course have to adapt accordingly. And we needn't change the javadoc of current InjectionPoint either.
Agree, disagree? :)

@Ladicek
Copy link
Contributor Author

Ladicek commented Jul 16, 2025

For an unexplainable reason, I'm quite fond of the idea of SyntheticInjectionPoint extending InjectionPoint :-)

@manovotn
Copy link
Contributor

For an unexplainable reason, I'm quite fond of the idea of SyntheticInjectionPoint extending InjectionPoint :-)

Sure, works for me.

@Azquelt
Copy link
Member

Azquelt commented Jul 16, 2025

Users can also obtain the injection point via:

  • Bean.getInjectionPoints()
  • injecting InjectionPoint into a dependent-scoped bean

If we allow synthetic beans to have an InjectionPoint with a null member, then the first case would obviously allow the user to see it. I think they should also see it in the second case if the bean was obtained via BeanManager.getInjectableReference(InjectionPoint, CreationalContext), or the new SyntheticInjections methods.

If we really want to guarantee that the user cannot get an InjectionPoint with a null member, I think we'd need InjectionPoint to extend BasicInjectionPoint which doesn't have getMember or getAnnotated, add new methods to allow users to obtain BasicInjectionPoint rather than an InjectionPoint, and decide what to do when a bean which injects InjectionPoint is injected using a BasicInjectionPoint.

If we don't do that, I can't see how we could avoid altering InjectionPoint to allow getMember and getAnnotated to return null.

@manovotn
Copy link
Contributor

Bean.getInjectionPoints()

Right, you'd only be getting regular IPs here until now, or empty collections for synth beans (excepting some edge cases where a synth. bean would "mimic" actual IP for the sake of validation which has no real use).
So this is indeed breaking one way or the other.

I think they should also see it in the second case if the bean was obtained via BeanManager.getInjectableReference(InjectionPoint, CreationalContext), or the new SyntheticInjections methods

I am not sure I understand how would you need to see the new IP here? Or you mean to use it in place of the current method?

@Ladicek
Copy link
Contributor Author

Ladicek commented Jul 24, 2025

I was thinking it would be quite acceptable to do a small breaking change and say that InjectionPoint.getMember() may return null in case the bean declaring the injection point is a synthetic bean. But then, I found this: https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1.html#inter_module_injection_full Apparently, the spec relies upon getMember() never returning null, even in case of synthetic beans.

@manovotn
Copy link
Contributor

I was thinking it would be quite acceptable to do a small breaking change and say that InjectionPoint.getMember() may return null in case the bean declaring the injection point is a synthetic bean. But then, I found this: https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1.html#inter_module_injection_full Apparently, the spec relies upon getMember() never returning null, even in case of synthetic beans.

I was looking around and I didn't really find good explanation of what is this particular spec part about.
I do not dispute that the change would break this part of specification, I am just trying to grasp why it is there.
Specifically this part:

InjectionPoint.getMember() and then Member.getDeclaringClass() to determine the class that declares an injection point.

It has nothing to do with checking bean availability, we can do that with just type and qualifier and knowing which beans are enabled where. So it is likely meant to support the last bullet point:

the bean class is required to be accessible to classes in the module, according to the class accessibility requirements of the module architecture.

But how does it help there?
If you are breaking accessibility rules (I am thinking modules in java, or those in WFLY perhaps?) then the injection will not work regardless of whether the container checks the IP#getMember or not. You are essentially running into a problem that's more elementary than what CDI deals with.

@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch from bc1874b to f657c84 Compare October 3, 2025 08:50
@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch from f657c84 to 23493ca Compare October 3, 2025 08:52
Injection points are registered on a `SyntheticBeanBuilder` using `withInjectionPoint()`.
The synthetic bean creation/destruction functions can look up injectable references
for registered injection points from `SyntheticInjections`.

The `SyntheticBeanCreator` and `SyntheticBeanDisposer` interfaces used to have one method.
That method is now deprecated for removal and a second method is added. The second method
no longer has an `Instance<Object>` parameter for arbitrary lookups; instead, is has
a parameter of type `SyntheticInjections` for pre-registered injections.
@Ladicek Ladicek force-pushed the synthetic-bean-injection-points branch from 23493ca to f96f107 Compare October 7, 2025 07:02
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 Red Hat and others
Copy link
Contributor

Choose a reason for hiding this comment

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

Wrong copyright header?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. I'll fix.

Comment on lines +28 to +29
* on the {@link SyntheticBeanBuilder}. Returns {@code null} if the type/qualifiers
* combination was not registered on the {@code SyntheticBeanBuilder}.
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't it be an error to try and retrieve a non-registered bean?
Since it is just one party registering and then using the synth. injection point, they should know what to look for and a wrong input should give them some meaningful error.

Besides, there is always the edge case of a dependent scoped producer returning null as a bean which is then indistinguishable from not having any such bean.

Or we could return an Optional which would be empty if there was no bean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. We could throw IllegalArgumentException I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aside: I think I specified this to behave the same as the corresponding API we have in ArC, but I think throwing an exception is a better idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you talking about io.quarkus.arc.SyntheticCreationalContext.getInjectedReference(Class<R>, Annotation...)? If so, then we also throw an IllegalArgumentException if a corresponding synthetic injection point was not defined...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we? Then I have no idea why I wrote this the way I wrote it :-)

@Ladicek
Copy link
Contributor Author

Ladicek commented Oct 7, 2025

Just FYI, my force-pushes recently didn't change anything in this PR, I just rebased on latest main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants