Skip to content

Commit 7c2453c

Browse files
committed
Integrate class proxy generation in AOT processing
This commit updates ApplicationContextAotGenerator to register a handler that process Cglib generated classes. The handler registers such classes to the GeneratedFiles and provide a hint so that it can be instantiated using reflection. Closes gh-28954
1 parent d6f7399 commit 7c2453c

File tree

7 files changed

+237
-22
lines changed

7 files changed

+237
-22
lines changed

spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.context.aot;
1818

19+
import java.util.function.Supplier;
20+
1921
import org.springframework.aot.generate.GenerationContext;
2022
import org.springframework.beans.factory.BeanFactory;
2123
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
24+
import org.springframework.cglib.core.ReflectUtils;
2225
import org.springframework.context.ApplicationContext;
2326
import org.springframework.context.ApplicationContextInitializer;
2427
import org.springframework.context.support.GenericApplicationContext;
@@ -47,12 +50,24 @@ public class ApplicationContextAotGenerator {
4750
*/
4851
public ClassName processAheadOfTime(GenericApplicationContext applicationContext,
4952
GenerationContext generationContext) {
50-
applicationContext.refreshForAotProcessing();
51-
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
52-
ApplicationContextInitializationCodeGenerator codeGenerator =
53-
new ApplicationContextInitializationCodeGenerator(generationContext);
54-
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
55-
return codeGenerator.getGeneratedClass().getName();
53+
return withGeneratedClassHandler(new GeneratedClassHandler(generationContext), () -> {
54+
applicationContext.refreshForAotProcessing();
55+
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
56+
ApplicationContextInitializationCodeGenerator codeGenerator =
57+
new ApplicationContextInitializationCodeGenerator(generationContext);
58+
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
59+
return codeGenerator.getGeneratedClass().getName();
60+
});
61+
}
62+
63+
private <T> T withGeneratedClassHandler(GeneratedClassHandler generatedClassHandler, Supplier<T> task) {
64+
try {
65+
ReflectUtils.setGeneratedClassHandler(generatedClassHandler);
66+
return task.get();
67+
}
68+
finally {
69+
ReflectUtils.setGeneratedClassHandler(null);
70+
}
5671
}
5772

5873
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2022 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.context.aot;
18+
19+
import java.util.function.BiConsumer;
20+
import java.util.function.Consumer;
21+
22+
import org.springframework.aot.generate.GeneratedFiles;
23+
import org.springframework.aot.generate.GeneratedFiles.Kind;
24+
import org.springframework.aot.generate.GenerationContext;
25+
import org.springframework.aot.hint.MemberCategory;
26+
import org.springframework.aot.hint.RuntimeHints;
27+
import org.springframework.aot.hint.TypeHint.Builder;
28+
import org.springframework.aot.hint.TypeReference;
29+
import org.springframework.cglib.core.ReflectUtils;
30+
import org.springframework.core.io.ByteArrayResource;
31+
32+
/**
33+
* Handle generated classes by adding them to a {@link GenerationContext},
34+
* and register the necessary hints so that they can be instantiated.
35+
*
36+
* @author Stephane Nicoll
37+
* @see ReflectUtils#setGeneratedClassHandler(BiConsumer)
38+
*/
39+
class GeneratedClassHandler implements BiConsumer<String, byte[]> {
40+
41+
private static final Consumer<Builder> asCglibProxy = hint ->
42+
hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
43+
44+
private final RuntimeHints runtimeHints;
45+
46+
private final GeneratedFiles generatedFiles;
47+
48+
GeneratedClassHandler(GenerationContext generationContext) {
49+
this.runtimeHints = generationContext.getRuntimeHints();
50+
this.generatedFiles = generationContext.getGeneratedFiles();
51+
}
52+
53+
@Override
54+
public void accept(String className, byte[] content) {
55+
this.runtimeHints.reflection().registerType(TypeReference.of(className), asCglibProxy);
56+
String path = className.replace(".", "/") + ".class";
57+
this.generatedFiles.addFile(Kind.CLASS, path, new ByteArrayResource(content));
58+
}
59+
60+
}

spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

1717
package org.springframework.context.aot;
1818

19+
import java.io.IOException;
1920
import java.util.function.BiConsumer;
2021

2122
import org.junit.jupiter.api.Test;
2223

24+
import org.springframework.aot.generate.GeneratedFiles.Kind;
25+
import org.springframework.aot.hint.MemberCategory;
26+
import org.springframework.aot.hint.TypeReference;
27+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
2328
import org.springframework.aot.test.generator.compile.Compiled;
2429
import org.springframework.aot.test.generator.compile.TestCompiler;
2530
import org.springframework.beans.BeansException;
@@ -36,11 +41,13 @@
3641
import org.springframework.beans.factory.support.RegisteredBean;
3742
import org.springframework.beans.factory.support.RootBeanDefinition;
3843
import org.springframework.context.ApplicationContextInitializer;
44+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3945
import org.springframework.context.annotation.AnnotationConfigUtils;
4046
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
4147
import org.springframework.context.support.GenericApplicationContext;
4248
import org.springframework.context.testfixture.context.generator.SimpleComponent;
4349
import org.springframework.context.testfixture.context.generator.annotation.AutowiredComponent;
50+
import org.springframework.context.testfixture.context.generator.annotation.CglibConfiguration;
4451
import org.springframework.context.testfixture.context.generator.annotation.InitDestroyComponent;
4552
import org.springframework.core.testfixture.aot.generate.TestGenerationContext;
4653

@@ -161,13 +168,30 @@ void processAheadOfTimeWhenHasBeanRegistrationAotProcessorExcludesProcessor() {
161168
});
162169
}
163170

164-
@SuppressWarnings({ "rawtypes", "unchecked" })
165-
private void testCompiledResult(GenericApplicationContext applicationContext,
166-
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
171+
@Test
172+
void processAheadOfTimeWhenHasCglibProxyWriteProxyAndGenerateReflectionHints() throws IOException {
173+
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
174+
applicationContext.registerBean(CglibConfiguration.class);
175+
TestGenerationContext context = processAheadOfTime(applicationContext);
176+
String proxyClassName = CglibConfiguration.class.getName() + "$$SpringCGLIB$$0";
177+
assertThat(context.getGeneratedFiles()
178+
.getGeneratedFileContent(Kind.CLASS, proxyClassName.replace('.', '/') + ".class")).isNotNull();
179+
assertThat(RuntimeHintsPredicates.reflection().onType(TypeReference.of(proxyClassName))
180+
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(context.getRuntimeHints());
181+
}
182+
183+
private static TestGenerationContext processAheadOfTime(GenericApplicationContext applicationContext) {
167184
ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
168185
TestGenerationContext generationContext = new TestGenerationContext();
169186
generator.processAheadOfTime(applicationContext, generationContext);
170187
generationContext.writeGeneratedContent();
188+
return generationContext;
189+
}
190+
191+
@SuppressWarnings({ "rawtypes", "unchecked" })
192+
private void testCompiledResult(GenericApplicationContext applicationContext,
193+
BiConsumer<ApplicationContextInitializer<GenericApplicationContext>, Compiled> result) {
194+
TestGenerationContext generationContext = processAheadOfTime(applicationContext);
171195
TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled ->
172196
result.accept(compiled.getInstance(ApplicationContextInitializer.class), compiled));
173197
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-2022 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.context.aot;
18+
19+
import java.io.ByteArrayOutputStream;
20+
import java.io.IOException;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.aot.generate.GeneratedFiles.Kind;
25+
import org.springframework.aot.generate.InMemoryGeneratedFiles;
26+
import org.springframework.aot.hint.MemberCategory;
27+
import org.springframework.aot.hint.TypeReference;
28+
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
29+
import org.springframework.core.io.InputStreamSource;
30+
import org.springframework.core.testfixture.aot.generate.TestGenerationContext;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Tests for {@link GeneratedClassHandler}.
36+
*
37+
* @author Stephane Nicoll
38+
*/
39+
class GeneratedClassHandlerTests {
40+
41+
private static final byte[] TEST_CONTENT = new byte[] { 'a' };
42+
43+
private final TestGenerationContext generationContext;
44+
45+
private final GeneratedClassHandler handler;
46+
47+
public GeneratedClassHandlerTests() {
48+
this.generationContext = new TestGenerationContext();
49+
this.handler = new GeneratedClassHandler(this.generationContext);
50+
}
51+
52+
@Test
53+
void handlerGenerateRuntimeHints() {
54+
String className = "com.example.Test$$Proxy$$1";
55+
this.handler.accept(className, TEST_CONTENT);
56+
assertThat(RuntimeHintsPredicates.reflection().onType(TypeReference.of(className))
57+
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
58+
.accepts(this.generationContext.getRuntimeHints());
59+
}
60+
61+
@Test
62+
void handlerRegisterGeneratedClass() throws IOException {
63+
String className = "com.example.Test$$Proxy$$1";
64+
this.handler.accept(className, TEST_CONTENT);
65+
InMemoryGeneratedFiles generatedFiles = this.generationContext.getGeneratedFiles();
66+
assertThat(generatedFiles.getGeneratedFiles(Kind.SOURCE)).isEmpty();
67+
assertThat(generatedFiles.getGeneratedFiles(Kind.RESOURCE)).isEmpty();
68+
String expectedPath = "com/example/Test$$Proxy$$1.class";
69+
assertThat(generatedFiles.getGeneratedFiles(Kind.CLASS)).containsOnlyKeys(expectedPath);
70+
assertContent(generatedFiles.getGeneratedFiles(Kind.CLASS).get(expectedPath), TEST_CONTENT);
71+
}
72+
73+
private void assertContent(InputStreamSource source, byte[] expectedContent) throws IOException {
74+
ByteArrayOutputStream out = new ByteArrayOutputStream();
75+
source.getInputStream().transferTo(out);
76+
assertThat(out.toByteArray()).isEqualTo(expectedContent);
77+
}
78+
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2002-2022 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.context.testfixture.context.generator.annotation;
18+
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
import org.springframework.context.annotation.Bean;
22+
import org.springframework.context.annotation.Configuration;
23+
24+
@Configuration
25+
public class CglibConfiguration {
26+
27+
private static final AtomicInteger counter = new AtomicInteger();
28+
29+
@Bean
30+
public String prefix() {
31+
return "Hello" + counter.getAndIncrement();
32+
}
33+
34+
@Bean
35+
public String text() {
36+
return prefix() + " World";
37+
}
38+
39+
}

spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,12 @@
2020
import java.beans.IntrospectionException;
2121
import java.beans.Introspector;
2222
import java.beans.PropertyDescriptor;
23-
import java.io.ByteArrayInputStream;
24-
import java.io.OutputStream;
2523
import java.lang.invoke.MethodHandles;
2624
import java.lang.reflect.Constructor;
2725
import java.lang.reflect.InvocationTargetException;
2826
import java.lang.reflect.Member;
2927
import java.lang.reflect.Method;
3028
import java.lang.reflect.Modifier;
31-
import java.nio.file.Files;
32-
import java.nio.file.Path;
3329
import java.security.ProtectionDomain;
3430
import java.util.ArrayList;
3531
import java.util.Arrays;
@@ -38,6 +34,7 @@
3834
import java.util.List;
3935
import java.util.Map;
4036
import java.util.Set;
37+
import java.util.function.BiConsumer;
4138

4239
import org.springframework.asm.Type;
4340

@@ -64,6 +61,8 @@ private ReflectUtils() {
6461

6562
private static final List<Method> OBJECT_METHODS = new ArrayList<Method>();
6663

64+
private static BiConsumer<String, byte[]> generatedClassHandler;
65+
6766
// SPRING PATCH BEGIN
6867
static {
6968
// Resolve protected ClassLoader.defineClass method for fallback use
@@ -441,20 +440,20 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader,
441440
return defineClass(className, b, loader, protectionDomain, null);
442441
}
443442

443+
public static void setGeneratedClassHandler(BiConsumer<String, byte[]> handler) {
444+
generatedClassHandler = handler;
445+
}
446+
444447
@SuppressWarnings({"deprecation", "serial"})
445448
public static Class defineClass(String className, byte[] b, ClassLoader loader,
446449
ProtectionDomain protectionDomain, Class<?> contextClass) throws Exception {
447450

448451
Class c = null;
449452
Throwable t = THROWABLE;
450453

451-
String generatedClasses = System.getProperty("cglib.generatedClasses");
452-
if (generatedClasses != null) {
453-
Path path = Path.of(generatedClasses + "/" + className.replace(".", "/") + ".class");
454-
Files.createDirectories(path.getParent());
455-
try (OutputStream os = Files.newOutputStream(path)) {
456-
new ByteArrayInputStream(b).transferTo(os);
457-
}
454+
BiConsumer<String, byte[]> handlerToUse = generatedClassHandler;
455+
if (handlerToUse != null) {
456+
handlerToUse.accept(className, b);
458457
}
459458

460459
// Preferred option: JDK 9+ Lookup.defineClass API if ClassLoader matches

spring-core/src/main/java/org/springframework/cglib/core/SpringNamingPolicy.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
* and using a plain counter suffix instead of a hash code suffix (as of 6.0).
2323
*
2424
* <p>This allows for reliably discovering pre-generated Spring proxy classes
25-
* in the classpath (as written at runtime when the "cglib.generatedClasses"
26-
* system property points to a specific directory to store the proxy classes).
25+
* in the classpath.
2726
*
2827
* @author Juergen Hoeller
2928
* @since 3.2.8 / 6.0

0 commit comments

Comments
 (0)