diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index 0de38d2d9940a..e6a776439e17b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -1198,7 +1198,7 @@ public void prepare(MethodContext context) { } } else { for (Constructor ctor : param.getClass().getConstructors()) { - if (ctor.isAnnotationPresent(RecordableConstructor.class)) { + if (RecordingAnnotationsUtil.isRecordableConstructor(ctor)) { nonDefaultConstructorHolder = new NonDefaultConstructorHolder(ctor, null); nonDefaultConstructorHandles = new DeferredParameter[ctor.getParameterCount()]; diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java index 6b544f2f4de1e..f6748b33ad4ad 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsProvider.java @@ -4,5 +4,11 @@ public interface RecordingAnnotationsProvider { - Class ignoredProperty(); + default Class ignoredProperty() { + return null; + } + + default Class recordableConstructor() { + return null; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java index b153e34b808dc..fe118e7c30a5c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/RecordingAnnotationsUtil.java @@ -2,27 +2,39 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; import java.util.HashSet; import java.util.List; import java.util.ServiceLoader; import java.util.Set; import io.quarkus.runtime.annotations.IgnoreProperty; +import io.quarkus.runtime.annotations.RecordableConstructor; final class RecordingAnnotationsUtil { static final List> IGNORED_PROPERTY_ANNOTATIONS; + static final List> RECORDABLE_CONSTRUCTOR_ANNOTATIONS; static { Set> ignoredPropertyAnnotations = new HashSet<>(); ignoredPropertyAnnotations.add(IgnoreProperty.class); + Set> recordableConstructorAnnotations = new HashSet<>(); + recordableConstructorAnnotations.add(RecordableConstructor.class); + for (RecordingAnnotationsProvider provider : ServiceLoader.load(RecordingAnnotationsProvider.class)) { Class ignoredProperty = provider.ignoredProperty(); if (ignoredProperty != null) { ignoredPropertyAnnotations.add(ignoredProperty); } + Class recordableConstructor = provider.recordableConstructor(); + if (recordableConstructor != null) { + recordableConstructorAnnotations.add(recordableConstructor); + } } + IGNORED_PROPERTY_ANNOTATIONS = List.copyOf(ignoredPropertyAnnotations); + RECORDABLE_CONSTRUCTOR_ANNOTATIONS = List.copyOf(recordableConstructorAnnotations); } private RecordingAnnotationsUtil() { @@ -37,4 +49,14 @@ static boolean isIgnored(AccessibleObject object) { } return false; } + + static boolean isRecordableConstructor(Constructor ctor) { + for (int i = 0; i < RECORDABLE_CONSTRUCTOR_ANNOTATIONS.size(); i++) { + Class annotation = RECORDABLE_CONSTRUCTOR_ANNOTATIONS.get(i); + if (ctor.isAnnotationPresent(annotation)) { + return true; + } + } + return false; + } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java index 2600ca97414b7..eff0271228d08 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/BytecodeRecorderTestCase.java @@ -293,6 +293,13 @@ public void testRecordableConstructor() throws Exception { TestRecorder recorder = generator.getRecordingProxy(TestRecorder.class); recorder.bean(bean); }, new TestConstructorBean("John", "Citizen").setAge(30)); + + runTest(generator -> { + OtherTestConstructorBean bean = new OtherTestConstructorBean("Jane", "Citizen"); + bean.setAge(30); + TestRecorder recorder = generator.getRecordingProxy(TestRecorder.class); + recorder.bean(bean); + }, new OtherTestConstructorBean("Jane", "Citizen").setAge(30)); } @Test diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java new file mode 100644 index 0000000000000..59f3fccdd520d --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/OtherTestConstructorBean.java @@ -0,0 +1,55 @@ +package io.quarkus.deployment.recording; + +import java.util.Objects; + +public class OtherTestConstructorBean { + + String first; + final String last; + int age; + + @TestRecordingAnnotationsProvider.TestRecordableConstructor + public OtherTestConstructorBean(String first, String last) { + this.first = first; + this.last = last; + } + + public void setFirst(String first) { + //should not be called, as it was initialized in the constructor + this.first = "Mrs " + first; + } + + public String getFirst() { + return first; + } + + public String getLast() { + return last; + } + + public int getAge() { + return age; + } + + public OtherTestConstructorBean setAge(int age) { + this.age = age; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + OtherTestConstructorBean that = (OtherTestConstructorBean) o; + return age == that.age && + Objects.equals(first, that.first) && + Objects.equals(last, that.last); + } + + @Override + public int hashCode() { + return Objects.hash(first, last, age); + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java index 41eb65ac76c45..50345ca827c3f 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecorder.java @@ -76,6 +76,10 @@ public void bean(TestConstructorBean bean) { RESULT.add(bean); } + public void bean(OtherTestConstructorBean bean) { + RESULT.add(bean); + } + public void result(RuntimeValue bean) { RESULT.add(bean.getValue()); } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java index a09698d944a24..c39a973afbd54 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/recording/TestRecordingAnnotationsProvider.java @@ -13,8 +13,18 @@ public class TestRecordingAnnotationsProvider implements RecordingAnnotationsPro public @interface TestIgnoreProperty { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.CONSTRUCTOR) + public @interface TestRecordableConstructor { + } + @Override public Class ignoredProperty() { return TestIgnoreProperty.class; } + + @Override + public Class recordableConstructor() { + return TestRecordableConstructor.class; + } } diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index c714a24ba857b..51eebdcda1435 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -1527,6 +1527,8 @@ The following objects can be passed to recorders: In cases where some fields of an object to be recorded should be ignored (i.e. the value that being at build time should not be reflected at runtime), the `@IgnoreProperty` can be placed on the field. If the class cannot depend on Quarkus, then Quarkus can use any custom annotation, as long as the extension implements the `io.quarkus.deployment.recording.RecordingAnnotationsProvider` SPI. + +This same SPI can also be used to provide a custom annotation that will substitute for `@RecordableConstructor`. ==== ==== Injecting Configuration into Recorders