Skip to content

Commit ea956d0

Browse files
authored
Merge pull request #36281 from mkouba/static-init-config-validation
Config: detect injected config value mismatch during static init
2 parents 39113e3 + 60c24cb commit ea956d0

File tree

23 files changed

+525
-284
lines changed

23 files changed

+525
-284
lines changed

core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java

+14
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
import io.quarkus.gizmo.TryBlock;
7878
import io.quarkus.runtime.Application;
7979
import io.quarkus.runtime.ApplicationLifecycleManager;
80+
import io.quarkus.runtime.ExecutionModeManager;
8081
import io.quarkus.runtime.LaunchMode;
8182
import io.quarkus.runtime.NativeImageRuntimePropertiesRecorder;
8283
import io.quarkus.runtime.PreventFurtherStepsException;
@@ -106,6 +107,14 @@ public class MainClassBuildStep {
106107
void.class, StartupContext.class);
107108
public static final MethodDescriptor CONFIGURE_STEP_TIME_ENABLED = ofMethod(StepTiming.class.getName(), "configureEnabled",
108109
void.class);
110+
public static final MethodDescriptor RUNTIME_EXECUTION_STATIC_INIT = ofMethod(ExecutionModeManager.class.getName(),
111+
"staticInit", void.class);
112+
public static final MethodDescriptor RUNTIME_EXECUTION_RUNTIME_INIT = ofMethod(ExecutionModeManager.class.getName(),
113+
"runtimeInit", void.class);
114+
public static final MethodDescriptor RUNTIME_EXECUTION_RUNNING = ofMethod(ExecutionModeManager.class.getName(),
115+
"running", void.class);
116+
public static final MethodDescriptor RUNTIME_EXECUTION_UNSET = ofMethod(ExecutionModeManager.class.getName(),
117+
"unset", void.class);
109118
public static final MethodDescriptor CONFIGURE_STEP_TIME_START = ofMethod(StepTiming.class.getName(), "configureStart",
110119
void.class);
111120
private static final DotName QUARKUS_APPLICATION = DotName.createSimple(QuarkusApplication.class.getName());
@@ -170,6 +179,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
170179
lm);
171180

172181
mv.invokeStaticMethod(CONFIGURE_STEP_TIME_ENABLED);
182+
mv.invokeStaticMethod(RUNTIME_EXECUTION_STATIC_INIT);
173183

174184
mv.invokeStaticMethod(ofMethod(Timing.class, "staticInitStarted", void.class, boolean.class),
175185
mv.load(launchMode.isAuxiliaryApplication()));
@@ -227,6 +237,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
227237
mv.load(i.getKey()), mv.load(i.getValue()));
228238
}
229239
mv.invokeStaticMethod(ofMethod(NativeImageRuntimePropertiesRecorder.class, "doRuntime", void.class));
240+
mv.invokeStaticMethod(RUNTIME_EXECUTION_RUNTIME_INIT);
230241

231242
// Set the SSL system properties
232243
if (!javaLibraryPathAdditionalPaths.isEmpty()) {
@@ -268,6 +279,8 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
268279
loaders, constants, gizmoOutput, startupContext, tryBlock);
269280
}
270281

282+
tryBlock.invokeStaticMethod(RUNTIME_EXECUTION_RUNNING);
283+
271284
// Startup log messages
272285
List<String> featureNames = new ArrayList<>();
273286
for (FeatureBuildItem feature : features) {
@@ -324,6 +337,7 @@ void build(List<StaticBytecodeRecorderBuildItem> staticInitTasks,
324337

325338
mv = file.getMethodCreator("doStop", void.class);
326339
mv.setModifiers(Modifier.PROTECTED | Modifier.FINAL);
340+
mv.invokeStaticMethod(RUNTIME_EXECUTION_UNSET);
327341
startupContext = mv.readStaticField(scField.getFieldDescriptor());
328342
mv.invokeVirtualMethod(ofMethod(StartupContext.class, "close", void.class), startupContext);
329343
mv.returnValue(null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.quarkus.runtime;
2+
3+
/**
4+
* The runtime execution mode.
5+
*/
6+
public enum ExecutionMode {
7+
8+
/**
9+
* Static initializiation.
10+
*/
11+
STATIC_INIT,
12+
13+
/**
14+
* Runtime initialization.
15+
*/
16+
RUNTIME_INIT,
17+
18+
/**
19+
* The application is running.
20+
*/
21+
RUNNING,
22+
23+
UNSET,
24+
;
25+
26+
public static ExecutionMode current() {
27+
return ExecutionModeManager.getExecutionMode();
28+
}
29+
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkus.runtime;
2+
3+
public final class ExecutionModeManager {
4+
5+
private static volatile ExecutionMode executionMode = ExecutionMode.UNSET;
6+
7+
public static void staticInit() {
8+
executionMode = ExecutionMode.STATIC_INIT;
9+
}
10+
11+
public static void runtimeInit() {
12+
executionMode = ExecutionMode.RUNTIME_INIT;
13+
}
14+
15+
public static void running() {
16+
executionMode = ExecutionMode.RUNNING;
17+
}
18+
19+
public static void unset() {
20+
executionMode = ExecutionMode.UNSET;
21+
}
22+
23+
public static ExecutionMode getExecutionMode() {
24+
return executionMode;
25+
}
26+
}

core/runtime/src/main/java/io/quarkus/runtime/annotations/StaticInitSafe.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
package io.quarkus.runtime.annotations;
22

3+
import static java.lang.annotation.ElementType.FIELD;
4+
import static java.lang.annotation.ElementType.PARAMETER;
35
import static java.lang.annotation.ElementType.TYPE;
46
import static java.lang.annotation.RetentionPolicy.RUNTIME;
57

68
import java.lang.annotation.Documented;
79
import java.lang.annotation.Retention;
810
import java.lang.annotation.Target;
911

12+
import org.eclipse.microprofile.config.inject.ConfigProperty;
13+
1014
/**
11-
* Used to mark a {@link org.eclipse.microprofile.config.spi.ConfigSource},
12-
* {@link org.eclipse.microprofile.config.spi.ConfigSourceProvider} or {@link io.smallrye.config.ConfigSourceFactory}
13-
* as safe to be initialized during STATIC INIT.
15+
* Used to mark a configuration object as safe to be initialized during the STATIC INIT phase.
16+
* <p>
17+
* The target configuration objects include {@link org.eclipse.microprofile.config.spi.ConfigSource},
18+
* {@link org.eclipse.microprofile.config.spi.ConfigSourceProvider}, {@link io.smallrye.config.ConfigSourceFactory} and
19+
* {@link io.smallrye.config.ConfigMapping}. Moreover, this annotation can be used for
20+
* {@link org.eclipse.microprofile.config.inject.ConfigProperty} injection points.
21+
* <p>
1422
*
1523
* When a Quarkus application is starting up, Quarkus will execute first a static init method which contains some
1624
* extensions actions and configurations. Example:
@@ -36,7 +44,7 @@
3644
* previous code example and a ConfigSource that requires database access. In this case, it is impossible to properly
3745
* initialize such ConfigSource, because the database services are not yet available so the ConfigSource in unusable.
3846
*/
39-
@Target(TYPE)
47+
@Target({ TYPE, FIELD, PARAMETER })
4048
@Retention(RUNTIME)
4149
@Documented
4250
public @interface StaticInitSafe {

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

+1-69
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import jakarta.enterprise.context.Dependent;
3131
import jakarta.enterprise.inject.CreationException;
3232

33-
import org.eclipse.microprofile.config.Config;
3433
import org.eclipse.microprofile.config.ConfigValue;
3534
import org.eclipse.microprofile.config.inject.ConfigProperties;
3635
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -42,10 +41,8 @@
4241
import org.jboss.jandex.DotName;
4342
import org.jboss.jandex.FieldInfo;
4443
import org.jboss.jandex.MethodInfo;
45-
import org.jboss.jandex.ParameterizedType;
4644
import org.jboss.jandex.Type;
4745
import org.jboss.jandex.Type.Kind;
48-
import org.jboss.logging.Logger;
4946

5047
import io.quarkus.arc.Unremovable;
5148
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem;
@@ -67,7 +64,6 @@
6764
import io.quarkus.deployment.builditem.ConfigPropertiesBuildItem;
6865
import io.quarkus.deployment.builditem.ConfigurationBuildItem;
6966
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
70-
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
7167
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
7268
import io.quarkus.deployment.configuration.definition.RootDefinition;
7369
import io.quarkus.deployment.recording.RecorderContext;
@@ -80,14 +76,11 @@
8076
* MicroProfile Config related build steps.
8177
*/
8278
public class ConfigBuildStep {
83-
private static final Logger LOGGER = Logger.getLogger(ConfigBuildStep.class.getName());
8479

85-
private static final DotName MP_CONFIG = DotName.createSimple(Config.class.getName());
86-
private static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
80+
static final DotName MP_CONFIG_PROPERTY_NAME = DotName.createSimple(ConfigProperty.class.getName());
8781
private static final DotName MP_CONFIG_PROPERTIES_NAME = DotName.createSimple(ConfigProperties.class.getName());
8882
private static final DotName MP_CONFIG_VALUE_NAME = DotName.createSimple(ConfigValue.class.getName());
8983

90-
private static final DotName SR_CONFIG = DotName.createSimple(io.smallrye.config.SmallRyeConfig.class.getName());
9184
private static final DotName SR_CONFIG_VALUE_NAME = DotName.createSimple(io.smallrye.config.ConfigValue.class.getName());
9285

9386
private static final DotName MAP_NAME = DotName.createSimple(Map.class.getName());
@@ -474,67 +467,6 @@ void validateConfigPropertiesInjectionPoints(
474467
toRegister.forEach(configProperties::produce);
475468
}
476469

477-
@BuildStep
478-
void warnStaticInitInjectionPoints(
479-
CombinedIndexBuildItem indexBuildItem,
480-
ValidationPhaseBuildItem validationPhase,
481-
List<ConfigClassBuildItem> configClasses,
482-
List<ConfigInjectionStaticInitBuildItem> configInjectionStaticInit,
483-
BuildProducer<RunTimeConfigurationDefaultBuildItem> runTimeConfigurationDefault) {
484-
485-
// Add here annotated classes that are initialized during static init
486-
Set<DotName> declaringClassCandidates = configInjectionStaticInit.stream()
487-
.map(ConfigInjectionStaticInitBuildItem::getDeclaringCandidate).collect(toSet());
488-
489-
Set<Type> configClassesTypes = configClasses.stream().map(ConfigClassBuildItem::getTypes).flatMap(Collection::stream)
490-
.collect(toSet());
491-
492-
for (InjectionPointInfo injectionPoint : validationPhase.getContext().getInjectionPoints()) {
493-
if (injectionPoint.getType().name().equals(DotNames.INSTANCE)) {
494-
continue;
495-
}
496-
497-
Type type = Type.create(injectionPoint.getRequiredType().name(), Type.Kind.CLASS);
498-
DotName injectionTypeName = null;
499-
if (type.name().equals(MP_CONFIG) || type.name().equals(SR_CONFIG)) {
500-
injectionTypeName = type.name();
501-
} else if (injectionPoint.getRequiredQualifier(MP_CONFIG_PROPERTY_NAME) != null) {
502-
injectionTypeName = MP_CONFIG_PROPERTY_NAME;
503-
} else if (configClassesTypes.contains(type)) {
504-
injectionTypeName = type.name();
505-
}
506-
507-
if (injectionTypeName != null) {
508-
AnnotationTarget target = injectionPoint.getTarget();
509-
if (FIELD.equals(target.kind())) {
510-
FieldInfo field = target.asField();
511-
ClassInfo declaringClass = field.declaringClass();
512-
Map<DotName, List<AnnotationInstance>> annotationsMap = declaringClass.annotationsMap();
513-
for (DotName declaringClassCandidate : declaringClassCandidates) {
514-
List<AnnotationInstance> annotationInstances = annotationsMap.get(declaringClassCandidate);
515-
if (annotationInstances != null && annotationInstances.size() == 1) {
516-
AnnotationInstance annotationInstance = annotationInstances.get(0);
517-
if (annotationInstance.target().equals(declaringClass)) {
518-
LOGGER.warn("Directly injecting a " +
519-
injectionTypeName +
520-
" into a " +
521-
declaringClassCandidate +
522-
" may lead to unexpected results. To ensure proper results, please " +
523-
"change the type of the field to " +
524-
ParameterizedType.create(DotNames.INSTANCE, new Type[] { type }, null) +
525-
". Offending field is '" +
526-
field.name() +
527-
"' of class '" +
528-
field.declaringClass() +
529-
"'");
530-
}
531-
}
532-
}
533-
}
534-
}
535-
}
536-
}
537-
538470
@BuildStep
539471
@Record(RUNTIME_INIT)
540472
void registerConfigClasses(

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

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
import io.quarkus.builder.item.MultiBuildItem;
66

7+
/**
8+
*
9+
* @deprecated This build item is not used anymore
10+
*/
11+
@Deprecated(forRemoval = true)
712
public final class ConfigInjectionStaticInitBuildItem extends MultiBuildItem {
813
private final DotName declaringCandidate;
914

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.quarkus.arc.deployment;
2+
3+
import org.jboss.jandex.DotName;
4+
5+
import io.quarkus.arc.processor.AnnotationsTransformer;
6+
import io.quarkus.arc.processor.DotNames;
7+
import io.quarkus.arc.runtime.ConfigStaticInitCheck;
8+
import io.quarkus.arc.runtime.ConfigStaticInitCheckInterceptor;
9+
import io.quarkus.arc.runtime.ConfigStaticInitValues;
10+
import io.quarkus.deployment.annotations.BuildStep;
11+
12+
public class ConfigStaticInitBuildSteps {
13+
14+
@BuildStep
15+
AdditionalBeanBuildItem registerBeans() {
16+
return AdditionalBeanBuildItem.builder()
17+
.addBeanClasses(ConfigStaticInitCheckInterceptor.class, ConfigStaticInitValues.class,
18+
ConfigStaticInitCheck.class)
19+
.build();
20+
}
21+
22+
@BuildStep
23+
AnnotationsTransformerBuildItem transformConfigProducer() {
24+
DotName configProducerName = DotName.createSimple("io.smallrye.config.inject.ConfigProducer");
25+
26+
return new AnnotationsTransformerBuildItem(AnnotationsTransformer.appliedToMethod().whenMethod(m -> {
27+
// Apply to all producer methods declared on io.smallrye.config.inject.ConfigProducer
28+
return m.declaringClass().name().equals(configProducerName)
29+
&& m.hasAnnotation(DotNames.PRODUCES)
30+
&& m.hasAnnotation(ConfigBuildStep.MP_CONFIG_PROPERTY_NAME);
31+
}).thenTransform(t -> {
32+
t.add(ConfigStaticInitCheck.class);
33+
}));
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.quarkus.arc.test.config.staticinit;
2+
3+
import jakarta.enterprise.context.ApplicationScoped;
4+
import jakarta.enterprise.context.Initialized;
5+
import jakarta.enterprise.event.Observes;
6+
import jakarta.inject.Singleton;
7+
8+
import org.eclipse.microprofile.config.inject.ConfigProperty;
9+
10+
@Singleton
11+
public class StaticInitBean {
12+
13+
@ConfigProperty(name = "apfelstrudel")
14+
String value;
15+
16+
// bean is instantiated during STATIC_INIT
17+
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
18+
}
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.quarkus.arc.test.config.staticinit;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.junit.jupiter.api.Assertions.fail;
5+
6+
import org.eclipse.microprofile.config.spi.ConfigSource;
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.test.QuarkusUnitTest;
12+
13+
public class StaticInitConfigInjectionFailureTest {
14+
15+
@RegisterExtension
16+
static final QuarkusUnitTest config = new QuarkusUnitTest()
17+
.withApplicationRoot(root -> root
18+
.addClasses(StaticInitBean.class, StaticInitEagerBean.class, UnsafeConfigSource.class)
19+
.addAsServiceProvider(ConfigSource.class, UnsafeConfigSource.class)
20+
// the value from application.properties should be injected during STATIC_INIT
21+
.addAsResource(new StringAsset("apfelstrudel=jandex"), "application.properties"))
22+
.assertException(t -> {
23+
assertThat(t).isInstanceOf(IllegalStateException.class)
24+
.hasMessageContainingAll(
25+
"A runtime config property value differs from the value that was injected during the static intialization phase",
26+
"the runtime value of 'apfelstrudel' is [gizmo] but the value [jandex] was injected into io.quarkus.arc.test.config.staticinit.StaticInitBean#value",
27+
"the runtime value of 'apfelstrudel' is [gizmo] but the value [jandex] was injected into io.quarkus.arc.test.config.staticinit.StaticInitEagerBean#value");
28+
});
29+
30+
@Test
31+
public void test() {
32+
fail();
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkus.arc.test.config.staticinit;
2+
3+
import jakarta.enterprise.context.ApplicationScoped;
4+
import jakarta.enterprise.context.Initialized;
5+
import jakarta.enterprise.event.Observes;
6+
import jakarta.enterprise.inject.Instance;
7+
import jakarta.inject.Singleton;
8+
9+
import org.eclipse.microprofile.config.inject.ConfigProperty;
10+
11+
@Singleton
12+
public class StaticInitEagerBean {
13+
14+
@ConfigProperty(name = "apfelstrudel")
15+
Instance<String> value;
16+
17+
// bean is instantiated during STATIC_INIT
18+
void onInit(@Observes @Initialized(ApplicationScoped.class) Object event) {
19+
// this should trigger the failure
20+
value.get();
21+
}
22+
23+
}

0 commit comments

Comments
 (0)