Skip to content

Commit d65ba11

Browse files
authored
Merge pull request #31782 from mkouba/synthetic-bean-build-item-proxy
Synthetic beans - make it possible to use a recorded proxy directly
2 parents 5c4968d + 2a39861 commit d65ba11

File tree

5 files changed

+191
-28
lines changed

5 files changed

+191
-28
lines changed

docs/src/main/asciidoc/cdi-integration.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ You can:
286286

287287
1. Generate the bytecode of the `Contextual#create(CreationalContext<T>)` method directly via `ExtendedBeanConfigurator.creator(Consumer<MethodCreator>)`.
288288
2. Pass a subclass of `io.quarkus.arc.BeanCreator` via `ExtendedBeanConfigurator#creator(Class<? extends BeanCreator<U>>)`, and possibly specify some build-time parameters via `ExtendedBeanConfigurator#param()` and synthetic injection points via `ExtendedBeanConfigurator#addInjectionPoint()`.
289-
3. Produce the runtime instance through a proxy returned from a <<writing-extensions.adoc#bytecode-recording,`@Recorder` method>> and set it via `ExtendedBeanConfigurator#runtimeValue(RuntimeValue<?>)`, `ExtendedBeanConfigurator#supplier(Supplier<?>)` or `ExtendedBeanConfigurator#createWith(Function<SyntheticCreationalContext<?>, <?>)`.
289+
3. Produce the runtime instance through a proxy returned from a <<writing-extensions.adoc#bytecode-recording,`@Recorder` method>> and set it via `ExtendedBeanConfigurator#runtimeValue(RuntimeValue<?>)`, `ExtendedBeanConfigurator#runtimeProxy(Object)`, `ExtendedBeanConfigurator#supplier(Supplier<?>)` or `ExtendedBeanConfigurator#createWith(Function<SyntheticCreationalContext<?>, <?>)`.
290290

291291
.`SyntheticBeanBuildItem` Example 2
292292
[source,java]

extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeanBuildItem.java

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@
77

88
import org.jboss.jandex.AnnotationInstance;
99
import org.jboss.jandex.DotName;
10+
import org.jboss.jandex.Type;
1011

1112
import io.quarkus.arc.SyntheticCreationalContext;
1213
import io.quarkus.arc.processor.BeanConfiguratorBase;
1314
import io.quarkus.arc.processor.BeanRegistrar;
1415
import io.quarkus.builder.item.MultiBuildItem;
1516
import io.quarkus.deployment.annotations.ExecutionTime;
17+
import io.quarkus.deployment.recording.BytecodeRecorderImpl.ReturnedProxy;
1618
import io.quarkus.runtime.RuntimeValue;
1719

1820
/**
1921
* Makes it possible to register a synthetic bean.
2022
* <p>
2123
* Bean instances can be easily produced through a recorder and set via {@link ExtendedBeanConfigurator#supplier(Supplier)},
22-
* {@link ExtendedBeanConfigurator#runtimeValue(RuntimeValue)} and {@link ExtendedBeanConfigurator#createWith(Function)}.
24+
* {@link ExtendedBeanConfigurator#runtimeValue(RuntimeValue)}, {@link ExtendedBeanConfigurator#createWith(Function)} and
25+
* {@link ExtendedBeanConfigurator#runtimeProxy(Object)}.
2326
*
2427
* @see ExtendedBeanConfigurator
2528
* @see BeanRegistrar
@@ -61,14 +64,16 @@ boolean isStaticInit() {
6164
}
6265

6366
boolean hasRecorderInstance() {
64-
return configurator.supplier != null || configurator.runtimeValue != null || configurator.fun != null;
67+
return configurator.supplier != null || configurator.runtimeValue != null || configurator.fun != null
68+
|| configurator.runtimeProxy != null;
6569
}
6670

6771
/**
6872
* This construct is not thread-safe and should not be reused.
6973
*/
7074
public static class ExtendedBeanConfigurator extends BeanConfiguratorBase<ExtendedBeanConfigurator, Object> {
7175

76+
private Object runtimeProxy;
7277
private Supplier<?> supplier;
7378
private RuntimeValue<?> runtimeValue;
7479
private Function<SyntheticCreationalContext<?>, ?> fun;
@@ -85,53 +90,75 @@ public static class ExtendedBeanConfigurator extends BeanConfiguratorBase<Extend
8590
* @return a new build item
8691
*/
8792
public SyntheticBeanBuildItem done() {
88-
if (supplier == null && runtimeValue == null && fun == null && creatorConsumer == null) {
93+
if (supplier == null && runtimeValue == null && fun == null && runtimeProxy == null && creatorConsumer == null) {
8994
throw new IllegalStateException(
9095
"Synthetic bean does not provide a creation method, use ExtendedBeanConfigurator#creator(), ExtendedBeanConfigurator#supplier(), ExtendedBeanConfigurator#createWith() or ExtendedBeanConfigurator#runtimeValue()");
9196
}
9297
return new SyntheticBeanBuildItem(this);
9398
}
9499

95100
/**
101+
* The contextual bean instance is supplied by a proxy returned from a recorder method.
102+
* <p>
96103
* Use {@link #createWith(Function)} if you want to leverage build-time parameters or synthetic injection points.
97104
*
98-
* @param supplier A supplier returned from a recorder
105+
* @param supplier A supplier returned from a recorder method
99106
* @return self
107+
* @throws IllegalArgumentException If the supplier argument is not a proxy returned from a recorder method
100108
*/
101109
public ExtendedBeanConfigurator supplier(Supplier<?> supplier) {
102-
if (runtimeValue != null || fun != null) {
103-
throw multipleCreationMethods();
104-
}
110+
checkReturnedProxy(supplier);
111+
checkMultipleCreationMethods();
105112
this.supplier = Objects.requireNonNull(supplier);
106113
return this;
107114
}
108115

109116
/**
117+
* The contextual bean instance is a proxy returned from a recorder method.
118+
* <p>
110119
* Use {@link #createWith(Function)} if you want to leverage build-time parameters or synthetic injection points.
111120
*
112-
* @param runtimeValue A runtime value returned from a recorder
121+
* @param runtimeValue A runtime value returned from a recorder method
113122
* @return self
123+
* @throws IllegalArgumentException If the runtimeValue argument is not a proxy returned from a recorder method
114124
*/
115125
public ExtendedBeanConfigurator runtimeValue(RuntimeValue<?> runtimeValue) {
116-
if (supplier != null || fun != null) {
117-
throw multipleCreationMethods();
118-
}
126+
checkReturnedProxy(runtimeValue);
127+
checkMultipleCreationMethods();
119128
this.runtimeValue = Objects.requireNonNull(runtimeValue);
120129
return this;
121130
}
122131

123132
/**
133+
* The contextual bean instance is created by a proxy returned from a recorder method.
134+
* <p>
124135
* This method is useful if you need to use build-time parameters or synthetic injection points during creation of a
125136
* bean instance.
126137
*
127-
* @param fun A function returned from a recorder
138+
* @param function A function returned from a recorder method
128139
* @return self
140+
* @throws IllegalArgumentException If the function argument is not a proxy returned from a recorder method
129141
*/
130-
public <B> ExtendedBeanConfigurator createWith(Function<SyntheticCreationalContext<B>, B> fun) {
131-
if (supplier != null || runtimeValue != null) {
132-
throw multipleCreationMethods();
133-
}
134-
this.fun = cast(Objects.requireNonNull(fun));
142+
public <B> ExtendedBeanConfigurator createWith(Function<SyntheticCreationalContext<B>, B> function) {
143+
checkReturnedProxy(function);
144+
checkMultipleCreationMethods();
145+
this.fun = cast(Objects.requireNonNull(function));
146+
return this;
147+
}
148+
149+
/**
150+
* The contextual bean instance is a proxy returned from a recorder method.
151+
* <p>
152+
* Use {@link #createWith(Function)} if you want to leverage build-time parameters or synthetic injection points.
153+
*
154+
* @param proxy A proxy returned from a recorder method
155+
* @return self
156+
* @throws IllegalArgumentException If the proxy argument is not a proxy returned from a recorder method
157+
*/
158+
public ExtendedBeanConfigurator runtimeProxy(Object proxy) {
159+
checkReturnedProxy(proxy);
160+
checkMultipleCreationMethods();
161+
this.runtimeProxy = Objects.requireNonNull(proxy);
135162
return this;
136163
}
137164

@@ -158,6 +185,10 @@ DotName getImplClazz() {
158185
return implClazz;
159186
}
160187

188+
Set<Type> getTypes() {
189+
return types;
190+
}
191+
161192
Set<AnnotationInstance> getQualifiers() {
162193
return qualifiers;
163194
}
@@ -174,8 +205,23 @@ RuntimeValue<?> getRuntimeValue() {
174205
return fun;
175206
}
176207

177-
private IllegalStateException multipleCreationMethods() {
178-
return new IllegalStateException("It is not possible to specify multiple creation methods");
208+
Object getRuntimeProxy() {
209+
return runtimeProxy;
210+
}
211+
212+
private void checkMultipleCreationMethods() {
213+
if (runtimeProxy == null && runtimeValue == null && supplier == null && fun == null) {
214+
return;
215+
}
216+
throw new IllegalStateException("It is not possible to specify multiple creation methods");
217+
}
218+
219+
private void checkReturnedProxy(Object object) {
220+
if (object instanceof ReturnedProxy) {
221+
return;
222+
}
223+
throw new IllegalArgumentException(
224+
"The object is not a proxy returned from a recorder method: " + object.toString());
179225
}
180226

181227
}

extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/SyntheticBeansProcessor.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88

99
import jakarta.enterprise.inject.CreationException;
1010

11-
import org.jboss.jandex.DotName;
12-
1311
import io.quarkus.arc.SyntheticCreationalContext;
1412
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem;
13+
import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator;
1514
import io.quarkus.arc.processor.BeanConfigurator;
1615
import io.quarkus.arc.runtime.ArcRecorder;
1716
import io.quarkus.deployment.annotations.BuildProducer;
@@ -75,26 +74,27 @@ void initRegular(List<SyntheticBeanBuildItem> syntheticBeans,
7574
private void configureSyntheticBean(ArcRecorder recorder,
7675
Map<String, Function<SyntheticCreationalContext<?>, ?>> functionsMap,
7776
BeanRegistrationPhaseBuildItem beanRegistration, SyntheticBeanBuildItem bean) {
78-
DotName implClazz = bean.configurator().getImplClazz();
79-
String name = createName(implClazz.toString(), bean.configurator().getQualifiers().toString());
77+
String name = createName(bean.configurator());
8078
if (bean.configurator().getRuntimeValue() != null) {
8179
functionsMap.put(name, recorder.createFunction(bean.configurator().getRuntimeValue()));
8280
} else if (bean.configurator().getSupplier() != null) {
8381
functionsMap.put(name, recorder.createFunction(bean.configurator().getSupplier()));
8482
} else if (bean.configurator().getFunction() != null) {
8583
functionsMap.put(name, bean.configurator().getFunction());
84+
} else if (bean.configurator().getRuntimeProxy() != null) {
85+
functionsMap.put(name, recorder.createFunction(bean.configurator().getRuntimeProxy()));
8686
}
87-
BeanConfigurator<?> configurator = beanRegistration.getContext().configure(implClazz)
87+
BeanConfigurator<?> configurator = beanRegistration.getContext().configure(bean.configurator().getImplClazz())
8888
.read(bean.configurator());
8989
if (bean.hasRecorderInstance()) {
9090
configurator.creator(creator(name, bean));
9191
}
9292
configurator.done();
9393
}
9494

95-
private String createName(String beanClass, String qualifiers) {
96-
return beanClass.replace(".", "_") + "_"
97-
+ HashUtil.sha1(qualifiers);
95+
private String createName(ExtendedBeanConfigurator configurator) {
96+
return configurator.getImplClazz().toString().replace(".", "_") + "_"
97+
+ HashUtil.sha1(configurator.getTypes().toString() + configurator.getQualifiers().toString());
9898
}
9999

100100
private Consumer<MethodCreator> creator(String name, SyntheticBeanBuildItem bean) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package io.quarkus.arc.test.synthetic;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import java.lang.reflect.Method;
8+
import java.util.function.Consumer;
9+
10+
import jakarta.enterprise.context.ApplicationScoped;
11+
import jakarta.enterprise.inject.Vetoed;
12+
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.RegisterExtension;
15+
16+
import io.quarkus.arc.Arc;
17+
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
18+
import io.quarkus.arc.deployment.SyntheticBeanBuildItem.ExtendedBeanConfigurator;
19+
import io.quarkus.builder.BuildChainBuilder;
20+
import io.quarkus.builder.BuildContext;
21+
import io.quarkus.builder.BuildStep;
22+
import io.quarkus.deployment.builditem.StaticBytecodeRecorderBuildItem;
23+
import io.quarkus.deployment.recording.BytecodeRecorderImpl;
24+
import io.quarkus.runtime.annotations.Recorder;
25+
import io.quarkus.test.QuarkusUnitTest;
26+
27+
public class SyntheticBeanBuildItemProxyTest {
28+
29+
@RegisterExtension
30+
static final QuarkusUnitTest config = new QuarkusUnitTest()
31+
.withApplicationRoot(root -> root.addClasses(SyntheticBeanBuildItemProxyTest.class, SynthBean.class))
32+
.addBuildChainCustomizer(buildCustomizer());
33+
34+
static Consumer<BuildChainBuilder> buildCustomizer() {
35+
return new Consumer<BuildChainBuilder>() {
36+
37+
@Override
38+
public void accept(BuildChainBuilder builder) {
39+
builder.addBuildStep(new BuildStep() {
40+
41+
@Override
42+
public void execute(BuildContext context) {
43+
BytecodeRecorderImpl bytecodeRecorder = new BytecodeRecorderImpl(true,
44+
TestRecorder.class.getSimpleName(),
45+
"test", "" + TestRecorder.class.hashCode(), true, s -> null);
46+
// We need to use reflection due to some class loading problems
47+
Object recorderProxy = bytecodeRecorder.getRecordingProxy(TestRecorder.class);
48+
try {
49+
Method test = recorderProxy.getClass().getDeclaredMethod("test");
50+
Object proxy = test.invoke(recorderProxy);
51+
ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem.configure(SynthBean.class)
52+
.scope(ApplicationScoped.class)
53+
.unremovable();
54+
// No creator
55+
assertThrows(IllegalStateException.class,
56+
() -> configurator.done());
57+
// Not a returned proxy
58+
assertThrows(IllegalArgumentException.class,
59+
() -> configurator.runtimeProxy(new SynthBean()));
60+
context.produce(configurator
61+
.runtimeProxy(proxy)
62+
.done());
63+
} catch (Exception e) {
64+
throw new RuntimeException(e);
65+
}
66+
context.produce(new StaticBytecodeRecorderBuildItem(bytecodeRecorder));
67+
}
68+
}).produces(StaticBytecodeRecorderBuildItem.class).produces(SyntheticBeanBuildItem.class).build();
69+
}
70+
};
71+
}
72+
73+
@Recorder
74+
public static class TestRecorder {
75+
76+
public SynthBean test() {
77+
SynthBean bean = new SynthBean();
78+
bean.setValue("ok");
79+
return bean;
80+
}
81+
82+
}
83+
84+
@Test
85+
public void testBeans() {
86+
SynthBean bean = Arc.container().instance(SynthBean.class).get();
87+
assertNotNull(bean);
88+
assertEquals("ok", bean.getValue());
89+
}
90+
91+
@Vetoed
92+
public static class SynthBean {
93+
94+
private String value;
95+
96+
public SynthBean() {
97+
}
98+
99+
public String getValue() {
100+
return value;
101+
}
102+
103+
public void setValue(String value) {
104+
this.value = value;
105+
}
106+
107+
}
108+
}

extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/ArcRecorder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ public Object apply(SyntheticCreationalContext<?> t) {
127127
};
128128
}
129129

130+
public Function<SyntheticCreationalContext<?>, Object> createFunction(Object returnedProxy) {
131+
return new Function<SyntheticCreationalContext<?>, Object>() {
132+
@Override
133+
public Object apply(SyntheticCreationalContext<?> t) {
134+
return returnedProxy;
135+
}
136+
};
137+
}
138+
130139
public void initTestApplicationClassPredicate(Set<String> applicationBeanClasses) {
131140
PreloadedTestApplicationClassPredicate predicate = Arc.container()
132141
.instance(PreloadedTestApplicationClassPredicate.class).get();

0 commit comments

Comments
 (0)