From 9b28d2af0ab3a64ad5137bd61bdda45a9deab2e8 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 26 Sep 2023 17:32:09 +0200 Subject: [PATCH] Config: native build verification - verify the current ImageMode when a dependent config property is being injected --- .../ConfigInjectionStaticInitBuildItem.java | 6 ++ .../deployment/NativeBuildConfigSteps.java | 51 ++++++++++ .../quarkus/arc/config/NativeBuildTime.java | 20 ++++ .../arc/runtime/NativeBuildConfigCheck.java | 20 ++++ .../NativeBuildConfigCheckInterceptor.java | 94 +++++++++++++++++++ .../arc/runtime/NativeBuildConfigContext.java | 14 +++ .../NativeBuildConfigContextCreator.java | 22 +++++ 7 files changed, 227 insertions(+) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/NativeBuildConfigSteps.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/config/NativeBuildTime.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheck.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheckInterceptor.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContext.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContextCreator.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigInjectionStaticInitBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigInjectionStaticInitBuildItem.java index efb1919e33b02..d3674722fa64b 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigInjectionStaticInitBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigInjectionStaticInitBuildItem.java @@ -4,7 +4,13 @@ import io.quarkus.builder.item.MultiBuildItem; +/** + * + * @deprecated TODO + */ +@Deprecated(forRemoval = true) public final class ConfigInjectionStaticInitBuildItem extends MultiBuildItem { + private final DotName declaringCandidate; public ConfigInjectionStaticInitBuildItem(final DotName declaringCandidate) { diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/NativeBuildConfigSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/NativeBuildConfigSteps.java new file mode 100644 index 0000000000000..6270efcc09115 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/NativeBuildConfigSteps.java @@ -0,0 +1,51 @@ +package io.quarkus.arc.deployment; + +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.runtime.NativeBuildConfigCheck; +import io.quarkus.arc.runtime.NativeBuildConfigContext; +import io.quarkus.arc.runtime.NativeBuildConfigContextCreator; +import io.quarkus.arc.runtime.NativeBuildConfigCheckInterceptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ConfigurationBuildItem; +import io.quarkus.deployment.pkg.steps.NativeBuild; + +public class NativeBuildConfigSteps { + + @BuildStep(onlyIf = NativeBuild.class) + void nativeBuildConfigValidation(ConfigurationBuildItem config, + BuildProducer annotationTransformers, + BuildProducer additionalBeans, + BuildProducer syntheticBeans) { + + syntheticBeans.produce(SyntheticBeanBuildItem.configure(NativeBuildConfigContext.class) + .param( + "buildAndRunTimeFixed", + config.getReadResult().getBuildTimeRunTimeValues().keySet().toArray(String[]::new)) + .creator(NativeBuildConfigContextCreator.class) + .scope(Singleton.class) + .done()); + + additionalBeans.produce(AdditionalBeanBuildItem.builder().addBeanClasses(NativeBuildConfigCheckInterceptor.class, + NativeBuildConfigCheck.class).build()); + + DotName configProducerName = DotName.createSimple("io.smallrye.config.inject.ConfigProducer"); + DotName configPropertyName = DotName.createSimple(ConfigProperty.class.getName()); + annotationTransformers + .produce(new AnnotationsTransformerBuildItem(AnnotationsTransformer.appliedToMethod().whenMethod(m -> { + // Apply to all producer methods declared on io.smallrye.config.inject.ConfigProducer + return m.declaringClass().name().equals(configProducerName) + && m.hasAnnotation(DotNames.PRODUCES) + && m.hasAnnotation(configPropertyName); + }).thenTransform(t -> { + t.add(NativeBuildConfigCheck.class); + }))); + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/NativeBuildTime.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/NativeBuildTime.java new file mode 100644 index 0000000000000..132ab2bbcc00c --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/config/NativeBuildTime.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.config; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A config property injected during the static initialization phase of a native image build may result in unexpected errors + * because the injected value was obtained at build time and cannot be updated at runtime. + *

+ * If it's intentional and expected then use this annotation to eliminate the false positive. + */ +@Retention(RUNTIME) +@Target({ FIELD, PARAMETER }) +public @interface NativeBuildTime { + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheck.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheck.java new file mode 100644 index 0000000000000..6528b0de43f59 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheck.java @@ -0,0 +1,20 @@ +package io.quarkus.arc.runtime; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.interceptor.InterceptorBinding; + +/** + * Interceptor binding for {@link NativeBuildConfigCheckInterceptor}. + */ +@InterceptorBinding +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface NativeBuildConfigCheck { + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheckInterceptor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheckInterceptor.java new file mode 100644 index 0000000000000..f3462a856aca3 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigCheckInterceptor.java @@ -0,0 +1,94 @@ +package io.quarkus.arc.runtime; + +import java.lang.annotation.Annotation; +import java.util.Set; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.quarkus.arc.config.NativeBuildTime; +import io.quarkus.arc.impl.InjectionPointProvider; +import io.quarkus.runtime.ImageMode; +import jakarta.annotation.Priority; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedConstructor; +import jakarta.enterprise.inject.spi.AnnotatedField; +import jakarta.enterprise.inject.spi.AnnotatedParameter; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +/** + * The goal of this interceptor is to verify the current ImageMode when a dependent config property is being injected. + */ +@Priority(jakarta.interceptor.Interceptor.Priority.PLATFORM_BEFORE) +@Interceptor +@NativeBuildConfigCheck +public class NativeBuildConfigCheckInterceptor { + + @Inject + NativeBuildConfigContext nativeBuildConfigContext; + + @AroundInvoke + Object aroundInvoke(InvocationContext context) throws Exception { + verifyCurrentImageMode(nativeBuildConfigContext.getBuildAndRunTimeFixed()); + return context.proceed(); + } + + static void verifyCurrentImageMode(Set buildAndRunTimeFixed) { + InjectionPoint injectionPoint = InjectionPointProvider.get(); + if (injectionPoint != null) { + // Skip injection points annotated with NativeBuildTime + Annotated annotated = injectionPoint.getAnnotated(); + if (annotated != null && annotated.isAnnotationPresent(NativeBuildTime.class)) { + return; + } + // Skip BUILD_AND_RUN_TIME_FIXED properties + String propertyName = null; + for (Annotation qualifier : injectionPoint.getQualifiers()) { + if (qualifier instanceof ConfigProperty) { + propertyName = ((ConfigProperty) qualifier).name(); + } + } + if (propertyName != null && buildAndRunTimeFixed.contains(propertyName)) { + return; + } + } + if (ImageMode.current() == ImageMode.NATIVE_BUILD) { + StringBuilder b = new StringBuilder(); + b.append("A config property was injected during the static initialization phase of a native image build"); + b.append( + "\n\t- This may result in unexpected errors because the injected value was obtained at build time and cannot be updated at runtime"); + if (injectionPoint != null) { + b.append("\n\t- Injection point in question: "); + b.append(injectionPointToString(injectionPoint)); + } + b.append("\n\t- If that's intentional then annotate the injected field/parameter with @"); + b.append(NativeBuildTime.class.getName()); + b.append(" to eliminate the false positive"); + b.append( + "\n\t- You can also leverage the programmatic lookup to delay the injection of a config property; for example '@ConfigProperty(name = \"foo\") Instance foo'"); + b.append( + "\n\t- Note that a normal scoped bean is initialized lazily; this may help if the is only injected but not directly used during the static initialization phase"); + throw new IllegalStateException(b.toString()); + } + } + + private static String injectionPointToString(InjectionPoint injectionPoint) { + Annotated annotated = injectionPoint.getAnnotated(); + if (annotated instanceof AnnotatedField) { + AnnotatedField field = (AnnotatedField) annotated; + return field.getDeclaringType().getJavaClass().getName() + "#" + field.getJavaMember().getName(); + } else if (annotated instanceof AnnotatedParameter) { + AnnotatedParameter param = (AnnotatedParameter) annotated; + if (param.getDeclaringCallable() instanceof AnnotatedConstructor) { + return param.getDeclaringCallable().getDeclaringType().getJavaClass().getName() + "()"; + } else { + return param.getDeclaringCallable().getDeclaringType().getJavaClass().getName() + "#" + + param.getDeclaringCallable().getJavaMember().getName(); + } + } + return injectionPoint.toString(); + } +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContext.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContext.java new file mode 100644 index 0000000000000..bc530ae6d4df2 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContext.java @@ -0,0 +1,14 @@ +package io.quarkus.arc.runtime; + +import java.util.Set; + +import io.quarkus.arc.config.NativeBuildTime; + +/** + * @see NativeBuildTime + * @see NativeBuildConfigCheckInterceptor + */ +public interface NativeBuildConfigContext { + + Set getBuildAndRunTimeFixed(); +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContextCreator.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContextCreator.java new file mode 100644 index 0000000000000..297ad94ec42c4 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/NativeBuildConfigContextCreator.java @@ -0,0 +1,22 @@ +package io.quarkus.arc.runtime; + +import java.util.Set; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; + +public class NativeBuildConfigContextCreator implements BeanCreator { + + @Override + public NativeBuildConfigContext create(SyntheticCreationalContext context) { + Set buildAndRunTimeFixed = Set.of((String[]) context.getParams().get("buildAndRunTimeFixed")); + return new NativeBuildConfigContext() { + + @Override + public Set getBuildAndRunTimeFixed() { + return buildAndRunTimeFixed; + } + }; + } + +}