Skip to content

Commit

Permalink
Config: injection verification during the native image build
Browse files Browse the repository at this point in the history
- verify the current ImageMode when a config object is injected
- fail if injected  during the static initialization phase of a native image build
  • Loading branch information
mkouba committed Sep 27, 2023
1 parent f278b9b commit 3ca71cc
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.configuration.definition.RootDefinition;
import io.quarkus.deployment.pkg.PackageConfig;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.runtime.annotations.ConfigPhase;
Expand All @@ -83,7 +84,7 @@ public class ConfigBuildStep {
private static final Logger LOGGER = Logger.getLogger(ConfigBuildStep.class.getName());

private static final DotName MP_CONFIG = DotName.createSimple(Config.class.getName());
private static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
private static final DotName MP_CONFIG_PROPERTIES_NAME = DotName.createSimple(ConfigProperties.class.getName());
private static final DotName MP_CONFIG_VALUE_NAME = DotName.createSimple(ConfigValue.class.getName());

Expand Down Expand Up @@ -290,6 +291,7 @@ void registerConfigMappingsBean(
BeanRegistrationPhaseBuildItem beanRegistration,
List<ConfigClassBuildItem> configClasses,
CombinedIndexBuildItem combinedIndex,
PackageConfig packageConfig,
BuildProducer<BeanConfiguratorBuildItem> beanConfigurator) {

if (configClasses.isEmpty()) {
Expand Down Expand Up @@ -323,7 +325,8 @@ void registerConfigMappingsBean(
.creator(ConfigMappingCreator.class)
.addInjectionPoint(ClassType.create(DotNames.INJECTION_POINT))
.param("type", configClass.getConfigClass())
.param("prefix", configClass.getPrefix());
.param("prefix", configClass.getPrefix())
.param("nativeBuild", packageConfig.type.equalsIgnoreCase(PackageConfig.BuiltInType.NATIVE.getValue()));

if (configClass.getConfigClass().isAnnotationPresent(Unremovable.class)) {
bean.unremovable();
Expand All @@ -338,6 +341,7 @@ void registerConfigPropertiesBean(
BeanRegistrationPhaseBuildItem beanRegistration,
List<ConfigClassBuildItem> configClasses,
CombinedIndexBuildItem combinedIndex,
PackageConfig packageConfig,
BuildProducer<BeanConfiguratorBuildItem> beanConfigurator) {

if (configClasses.isEmpty()) {
Expand Down Expand Up @@ -371,7 +375,9 @@ void registerConfigPropertiesBean(
.creator(ConfigMappingCreator.class)
.addInjectionPoint(ClassType.create(DotNames.INJECTION_POINT))
.param("type", configClass.getConfigClass())
.param("prefix", configClass.getPrefix())));
.param("prefix", configClass.getPrefix())
.param("nativeBuild",
packageConfig.type.equalsIgnoreCase(PackageConfig.BuiltInType.NATIVE.getValue()))));
}
}

Expand Down
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,73 @@
package io.quarkus.arc.deployment;

import java.util.HashSet;
import java.util.Set;

import jakarta.inject.Singleton;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;

import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
import io.quarkus.arc.runtime.NativeBuildConfigCheck;
import io.quarkus.arc.runtime.NativeBuildConfigCheckInterceptor;
import io.quarkus.arc.runtime.NativeBuildConfigContext;
import io.quarkus.arc.runtime.NativeBuildConfigContextCreator;
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)
SyntheticBeanBuildItem registerNativeBuildConfigContext(ConfigurationBuildItem config,
BeanDiscoveryFinishedBuildItem beanDiscoveryFinished) {

// Collect all @ConfigProperty injection points
Set<String> injectedProperties = new HashSet<>();
for (InjectionPointInfo injectionPoint : beanDiscoveryFinished.getInjectionPoints()) {
if (injectionPoint.hasDefaultedQualifier()) {
continue;
}
AnnotationInstance configProperty = injectionPoint.getRequiredQualifier(ConfigBuildStep.MP_CONFIG_PROPERTY_NAME);
if (configProperty != null) {
injectedProperties.add(configProperty.value("name").asString());
}
}

// Retain only BUILD_AND_RUN_TIME_FIXED properties
injectedProperties.retainAll(config.getReadResult().getBuildTimeRunTimeValues().keySet());

return SyntheticBeanBuildItem.configure(NativeBuildConfigContext.class)
.param(
"buildAndRunTimeFixed",
injectedProperties.toArray(String[]::new))
.creator(NativeBuildConfigContextCreator.class)
.scope(Singleton.class)
.done();

}

@BuildStep(onlyIf = NativeBuild.class)
AdditionalBeanBuildItem registerBeans() {
return AdditionalBeanBuildItem.builder().addBeanClasses(NativeBuildConfigCheckInterceptor.class,
NativeBuildConfigCheck.class).build();
}

@BuildStep(onlyIf = NativeBuild.class)
AnnotationsTransformerBuildItem transformConfigProducer() {
DotName configProducerName = DotName.createSimple("io.smallrye.config.inject.ConfigProducer");

return 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(ConfigBuildStep.MP_CONFIG_PROPERTY_NAME);
}).thenTransform(t -> {
t.add(NativeBuildConfigCheck.class);
}));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.arc.test.config.nativebuild;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

@Singleton
public class Fail {

@ConfigProperty(name = "foo", defaultValue = "bar")
String value;

// triggers init during static init of native build
void init(@Observes @Initialized(ApplicationScoped.class) Object event) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.arc.test.config.nativebuild;

import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusProdModeTest;

public class NativeBuildConfigInjectionFailTest {

@RegisterExtension
static final QuarkusProdModeTest TEST = new QuarkusProdModeTest()
.setBuildNative(true)
.withApplicationRoot(
root -> root.addClass(Fail.class))
// ImageGenerationFailureException is private
.setExpectedException(RuntimeException.class);

@Test
void test() {
fail();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.quarkus.arc.test.config.nativebuild;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusProdModeTest;

public class NativeBuildConfigInjectionOkTest {

@RegisterExtension
static final QuarkusProdModeTest TEST = new QuarkusProdModeTest()
.setBuildNative(true)
.withApplicationRoot(
root -> root.addClass(Ok.class));

@Test
void test() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.arc.test.config.nativebuild;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;

import io.quarkus.arc.config.NativeBuildTime;

@Singleton
public class Ok {

@NativeBuildTime
@ConfigProperty(name = "foo", defaultValue = "bar")
String value;

// triggers init during static init of native build
void init(@Observes @Initialized(ApplicationScoped.class) Object event) {
}
}
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
Expand Up @@ -2,6 +2,7 @@

import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.Set;

import jakarta.enterprise.inject.spi.Annotated;
import jakarta.enterprise.inject.spi.InjectionPoint;
Expand All @@ -23,6 +24,10 @@ public Object create(SyntheticCreationalContext<Object> context) {
throw new IllegalStateException("No current injection point found");
}

if ((boolean) context.getParams().get("nativeBuild")) {
NativeBuildConfigCheckInterceptor.verifyCurrentImageMode(Set.of());
}

Class<?> interfaceType = (Class<?>) context.getParams().get("type");
String prefix = (String) context.getParams().get("prefix");

Expand Down
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 {

}
Loading

0 comments on commit 3ca71cc

Please sign in to comment.