Skip to content

Commit

Permalink
Config: native build verification
Browse files Browse the repository at this point in the history
- verify the current ImageMode when a dependent config property is being
injected
  • Loading branch information
mkouba committed Sep 26, 2023
1 parent f278b9b commit 9b28d2a
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AnnotationsTransformerBuildItem> annotationTransformers,
BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<SyntheticBeanBuildItem> 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);
})));
}

}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* If it's intentional and expected then use this annotation to eliminate the false positive.
*/
@Retention(RUNTIME)
@Target({ FIELD, PARAMETER })
public @interface NativeBuildTime {

}
Original file line number Diff line number Diff line change
@@ -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 {

}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> getBuildAndRunTimeFixed();
}
Original file line number Diff line number Diff line change
@@ -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<NativeBuildConfigContext> {

@Override
public NativeBuildConfigContext create(SyntheticCreationalContext<NativeBuildConfigContext> context) {
Set<String> buildAndRunTimeFixed = Set.of((String[]) context.getParams().get("buildAndRunTimeFixed"));
return new NativeBuildConfigContext() {

@Override
public Set<String> getBuildAndRunTimeFixed() {
return buildAndRunTimeFixed;
}
};
}

}

0 comments on commit 9b28d2a

Please sign in to comment.