Skip to content

Commit 0a83bcd

Browse files
nosanwilkinsona
authored andcommitted
Fix binding of structured logging properties in a native image
Add RuntimeHints for GraylogExtendedLogFormatProperties, StructuredLoggingJsonProperties and ElasticCommonSchemaProperties properties. Add BeanFactoryInitializationAotProcessor to register RuntimeHints for a custom StructuredLoggingJsonMembersCustomizer. See gh-43862 Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
1 parent 9036249 commit 0a83bcd

9 files changed

+280
-5
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaProperties.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.logging.structured;
1818

19+
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
1920
import org.springframework.boot.context.properties.bind.Binder;
2021
import org.springframework.boot.json.JsonWriter;
2122
import org.springframework.boot.json.JsonWriter.Members;
@@ -91,4 +92,12 @@ Service withDefaults(Environment environment) {
9192

9293
}
9394

95+
static class ElasticCommonSchemaPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {
96+
97+
ElasticCommonSchemaPropertiesRuntimeHints() {
98+
super(ElasticCommonSchemaProperties.class);
99+
}
100+
101+
}
102+
94103
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatProperties.java

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.logging.structured;
1818

19+
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
1920
import org.springframework.boot.context.properties.bind.Binder;
2021
import org.springframework.boot.json.JsonWriter;
2122
import org.springframework.core.env.Environment;
@@ -91,4 +92,12 @@ void jsonMembers(JsonWriter.Members<?> members) {
9192

9293
}
9394

95+
static class GraylogExtendedLogFormatPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {
96+
97+
GraylogExtendedLogFormatPropertiesRuntimeHints() {
98+
super(GraylogExtendedLogFormatProperties.class);
99+
}
100+
101+
}
102+
94103
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.logging.structured;
18+
19+
import java.util.Optional;
20+
21+
import org.springframework.aot.generate.GenerationContext;
22+
import org.springframework.aot.hint.MemberCategory;
23+
import org.springframework.aot.hint.RuntimeHints;
24+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
25+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
26+
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
27+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
28+
import org.springframework.core.env.Environment;
29+
30+
/**
31+
* {@link BeanFactoryInitializationAotProcessor} that registers {@link RuntimeHints} for
32+
* {@link StructuredLoggingJsonPropertiesJsonMembersCustomizer}.
33+
*
34+
* @author Dmytro Nosan
35+
*/
36+
class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor
37+
implements BeanFactoryInitializationAotProcessor {
38+
39+
private static final String ENVIRONMENT_BEAN_NAME = "environment";
40+
41+
@Override
42+
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
43+
Environment environment = beanFactory.getBean(ENVIRONMENT_BEAN_NAME, Environment.class);
44+
return Optional.ofNullable(StructuredLoggingJsonProperties.get(environment))
45+
.map(StructuredLoggingJsonProperties::customizer)
46+
.map(AotContribution::new)
47+
.orElse(null);
48+
}
49+
50+
private static final class AotContribution implements BeanFactoryInitializationAotContribution {
51+
52+
private final Class<? extends StructuredLoggingJsonMembersCustomizer<?>> customizer;
53+
54+
private AotContribution(Class<? extends StructuredLoggingJsonMembersCustomizer<?>> customizer) {
55+
this.customizer = customizer;
56+
}
57+
58+
@Override
59+
public void applyTo(GenerationContext generationContext,
60+
BeanFactoryInitializationCode beanFactoryInitializationCode) {
61+
generationContext.getRuntimeHints()
62+
.reflection()
63+
.registerType(this.customizer, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
64+
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
65+
}
66+
67+
}
68+
69+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Map;
2020
import java.util.Set;
2121

22+
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
2223
import org.springframework.boot.context.properties.bind.Binder;
2324
import org.springframework.core.env.Environment;
2425

@@ -42,4 +43,12 @@ static StructuredLoggingJsonProperties get(Environment environment) {
4243
.orElse(null);
4344
}
4445

46+
static class StructuredLoggingJsonPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {
47+
48+
StructuredLoggingJsonPropertiesRuntimeHints() {
49+
super(StructuredLoggingJsonProperties.class);
50+
}
51+
52+
}
53+
4554
}

spring-boot-project/spring-boot/src/main/resources/META-INF/spring/aot.factories

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ org.springframework.boot.jdbc.DataSourceBuilderRuntimeHints,\
1010
org.springframework.boot.json.JacksonRuntimeHints,\
1111
org.springframework.boot.logging.java.JavaLoggingSystemRuntimeHints,\
1212
org.springframework.boot.logging.logback.LogbackRuntimeHints,\
13+
org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints,\
14+
org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints,\
15+
org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints,\
1316
org.springframework.boot.web.embedded.undertow.UndertowWebServer.UndertowWebServerRuntimeHints,\
1417
org.springframework.boot.web.server.MimeMappings.MimeMappingsRuntimeHints
1518

1619
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
1720
org.springframework.boot.context.properties.ConfigurationPropertiesBeanFactoryInitializationAotProcessor,\
1821
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener.EnvironmentBeanFactoryInitializationAotProcessor,\
19-
org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor
22+
org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor,\
23+
org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor
2024

2125
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
2226
org.springframework.boot.context.properties.ConfigurationPropertiesBeanRegistrationAotProcessor,\

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPropertiesTests.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,12 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21+
import org.springframework.aot.hint.RuntimeHints;
22+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
23+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
24+
import org.springframework.beans.factory.aot.AotServices;
2125
import org.springframework.boot.json.JsonWriter;
26+
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.ElasticCommonSchemaPropertiesRuntimeHints;
2227
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties.Service;
2328
import org.springframework.mock.env.MockEnvironment;
2429

@@ -77,4 +82,24 @@ void addToJsonMembersCreatesValidJson() {
7782
+ "\"service.environment\":\"prod\",\"service.node.name\":\"boot\"}");
7883
}
7984

85+
@Test
86+
void shouldRegisterRuntimeHints() throws Exception {
87+
RuntimeHints hints = new RuntimeHints();
88+
new ElasticCommonSchemaPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader());
89+
assertThat(RuntimeHintsPredicates.reflection().onType(ElasticCommonSchemaProperties.class)).accepts(hints);
90+
assertThat(RuntimeHintsPredicates.reflection()
91+
.onConstructor(ElasticCommonSchemaProperties.class.getConstructor(Service.class))
92+
.invoke()).accepts(hints);
93+
assertThat(RuntimeHintsPredicates.reflection().onType(Service.class)).accepts(hints);
94+
assertThat(RuntimeHintsPredicates.reflection()
95+
.onConstructor(Service.class.getConstructor(String.class, String.class, String.class, String.class))
96+
.invoke()).accepts(hints);
97+
}
98+
99+
@Test
100+
void elasticCommonSchemaPropertiesRuntimeHintsIsRegistered() {
101+
assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class))
102+
.anyMatch(ElasticCommonSchemaPropertiesRuntimeHints.class::isInstance);
103+
}
104+
80105
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/GraylogExtendedLogFormatPropertiesTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21+
import org.springframework.aot.hint.RuntimeHints;
22+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
23+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
24+
import org.springframework.beans.factory.aot.AotServices;
2125
import org.springframework.boot.json.JsonWriter;
26+
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.GraylogExtendedLogFormatPropertiesRuntimeHints;
2227
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties.Service;
2328
import org.springframework.mock.env.MockEnvironment;
2429

@@ -98,4 +103,24 @@ void addToJsonMembersCreatesValidJson() {
98103
assertThat(writer.writeToString(properties)).isEqualTo("{\"host\":\"spring\",\"_service_version\":\"1.2.3\"}");
99104
}
100105

106+
@Test
107+
void shouldRegisterRuntimeHints() throws Exception {
108+
RuntimeHints hints = new RuntimeHints();
109+
new GraylogExtendedLogFormatPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader());
110+
assertThat(RuntimeHintsPredicates.reflection().onType(GraylogExtendedLogFormatProperties.class)).accepts(hints);
111+
assertThat(RuntimeHintsPredicates.reflection()
112+
.onConstructor(GraylogExtendedLogFormatProperties.class.getConstructor(String.class, Service.class))
113+
.invoke()).accepts(hints);
114+
assertThat(RuntimeHintsPredicates.reflection().onType(Service.class)).accepts(hints);
115+
assertThat(RuntimeHintsPredicates.reflection()
116+
.onConstructor(GraylogExtendedLogFormatProperties.Service.class.getConstructor(String.class))
117+
.invoke()).accepts(hints);
118+
}
119+
120+
@Test
121+
void graylogExtendedLogFormatPropertiesRuntimeHintsIsRegistered() {
122+
assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class))
123+
.anyMatch(GraylogExtendedLogFormatPropertiesRuntimeHints.class::isInstance);
124+
}
125+
101126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.logging.structured;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.aot.hint.MemberCategory;
22+
import org.springframework.aot.hint.RuntimeHints;
23+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
24+
import org.springframework.aot.test.generate.TestGenerationContext;
25+
import org.springframework.beans.factory.aot.AotServices;
26+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
27+
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
28+
import org.springframework.boot.json.JsonWriter.Members;
29+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
30+
import org.springframework.core.env.ConfigurableEnvironment;
31+
import org.springframework.mock.env.MockEnvironment;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Tests for
37+
* {@link StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor}.
38+
*
39+
* @author Dmytro Nosan
40+
*/
41+
class StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorTests {
42+
43+
@Test
44+
void structuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessorIsRegistered() {
45+
assertThat(AotServices.factories().load(BeanFactoryInitializationAotProcessor.class))
46+
.anyMatch(StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor.class::isInstance);
47+
}
48+
49+
@Test
50+
void shouldRegisterStructuredLoggingJsonMembersCustomizerRuntimeHints() {
51+
MockEnvironment environment = new MockEnvironment();
52+
environment.setProperty("logging.structured.json.customizer", TestCustomizer.class.getName());
53+
54+
BeanFactoryInitializationAotContribution contribution = getContribution(environment);
55+
assertThat(contribution).isNotNull();
56+
57+
RuntimeHints hints = getRuntimeHints(contribution);
58+
assertThat(RuntimeHintsPredicates.reflection()
59+
.onType(TestCustomizer.class)
60+
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
61+
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS))
62+
.accepts(hints);
63+
}
64+
65+
@Test
66+
void shouldNotRegisterStructuredLoggingJsonMembersCustomizerRuntimeHints() {
67+
MockEnvironment environment = new MockEnvironment();
68+
BeanFactoryInitializationAotContribution contribution = getContribution(environment);
69+
assertThat(contribution).isNull();
70+
}
71+
72+
@Test
73+
void shouldNotRegisterStructuredLoggingJsonMembersCustomizerRuntimeHintsWhenCustomizerIsNotSet() {
74+
MockEnvironment environment = new MockEnvironment();
75+
environment.setProperty("logging.structured.json.exclude", "something");
76+
BeanFactoryInitializationAotContribution contribution = getContribution(environment);
77+
assertThat(contribution).isNull();
78+
}
79+
80+
private BeanFactoryInitializationAotContribution getContribution(ConfigurableEnvironment environment) {
81+
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
82+
context.setEnvironment(environment);
83+
context.refresh();
84+
return new StructuredLoggingJsonMembersCustomizerBeanFactoryInitializationAotProcessor()
85+
.processAheadOfTime(context.getBeanFactory());
86+
}
87+
}
88+
89+
private RuntimeHints getRuntimeHints(BeanFactoryInitializationAotContribution contribution) {
90+
TestGenerationContext generationContext = new TestGenerationContext();
91+
contribution.applyTo(generationContext, null);
92+
return generationContext.getRuntimeHints();
93+
}
94+
95+
static class TestCustomizer implements StructuredLoggingJsonMembersCustomizer<String> {
96+
97+
@Override
98+
public void customize(Members<String> members) {
99+
}
100+
101+
}
102+
103+
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,7 +21,12 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.aot.hint.RuntimeHints;
25+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
26+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
27+
import org.springframework.beans.factory.aot.AotServices;
2428
import org.springframework.boot.json.JsonWriter.Members;
29+
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints;
2530
import org.springframework.mock.env.MockEnvironment;
2631

2732
import static org.assertj.core.api.Assertions.assertThat;
@@ -52,6 +57,23 @@ void getWhenNoBoundPropertiesReturnsNull() {
5257
StructuredLoggingJsonProperties.get(environment);
5358
}
5459

60+
@Test
61+
void shouldRegisterRuntimeHints() throws Exception {
62+
RuntimeHints hints = new RuntimeHints();
63+
new StructuredLoggingJsonPropertiesRuntimeHints().registerHints(hints, getClass().getClassLoader());
64+
assertThat(RuntimeHintsPredicates.reflection().onType(StructuredLoggingJsonProperties.class)).accepts(hints);
65+
assertThat(RuntimeHintsPredicates.reflection()
66+
.onConstructor(StructuredLoggingJsonProperties.class.getDeclaredConstructor(Set.class, Set.class, Map.class,
67+
Map.class, Class.class))
68+
.invoke()).accepts(hints);
69+
}
70+
71+
@Test
72+
void structuredLoggingJsonPropertiesRuntimeHintsRuntimeHintsIsRegistered() {
73+
assertThat(AotServices.factories().load(RuntimeHintsRegistrar.class))
74+
.anyMatch(StructuredLoggingJsonPropertiesRuntimeHints.class::isInstance);
75+
}
76+
5577
static class TestCustomizer implements StructuredLoggingJsonMembersCustomizer<String> {
5678

5779
@Override

0 commit comments

Comments
 (0)