Skip to content

Commit ea419d2

Browse files
committed
Avoid unnecessary CGLIB processing on configuration classes
Closes gh-34486 (cherry picked from commit aff9ac7)
1 parent c02d07f commit ea419d2

File tree

4 files changed

+84
-40
lines changed

4 files changed

+84
-40
lines changed

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-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.
@@ -191,6 +191,15 @@ Set<BeanMethod> getBeanMethods() {
191191
return this.beanMethods;
192192
}
193193

194+
boolean hasNonStaticBeanMethods() {
195+
for (BeanMethod beanMethod : this.beanMethods) {
196+
if (!beanMethod.getMetadata().isStatic()) {
197+
return true;
198+
}
199+
}
200+
return false;
201+
}
202+
194203
void addImportedResource(String importedResource, Class<? extends BeanDefinitionReader> readerClass) {
195204
this.importedResources.put(importedResource, readerClass);
196205
}
@@ -212,7 +221,7 @@ void validate(ProblemReporter problemReporter) {
212221

213222
// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
214223
if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) {
215-
if (this.metadata.isFinal()) {
224+
if (hasNonStaticBeanMethods() && this.metadata.isFinal()) {
216225
problemReporter.error(new FinalConfigurationProblem());
217226
}
218227
for (BeanMethod beanMethod : this.beanMethods) {

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

+44-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-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.
@@ -81,10 +81,9 @@
8181
* any number of ConfigurationClass objects because one Configuration class may import
8282
* another using the {@link Import} annotation).
8383
*
84-
* <p>This class helps separate the concern of parsing the structure of a Configuration
85-
* class from the concern of registering BeanDefinition objects based on the content of
86-
* that model (with the exception of {@code @ComponentScan} annotations which need to be
87-
* registered immediately).
84+
* <p>This class helps separate the concern of parsing the structure of a Configuration class
85+
* from the concern of registering BeanDefinition objects based on the content of that model
86+
* (except {@code @ComponentScan} annotations which need to be registered immediately).
8887
*
8988
* <p>This ASM-based implementation avoids reflection and eager class loading in order to
9089
* interoperate effectively with lazy class loading in a Spring ApplicationContext.
@@ -161,14 +160,22 @@ public void parse(Set<BeanDefinitionHolder> configCandidates) {
161160
for (BeanDefinitionHolder holder : configCandidates) {
162161
BeanDefinition bd = holder.getBeanDefinition();
163162
try {
163+
ConfigurationClass configClass;
164164
if (bd instanceof AnnotatedBeanDefinition annotatedBeanDef) {
165-
parse(annotatedBeanDef.getMetadata(), holder.getBeanName());
165+
configClass = parse(annotatedBeanDef.getMetadata(), holder.getBeanName());
166166
}
167167
else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef.hasBeanClass()) {
168-
parse(abstractBeanDef.getBeanClass(), holder.getBeanName());
168+
configClass = parse(abstractBeanDef.getBeanClass(), holder.getBeanName());
169169
}
170170
else {
171-
parse(bd.getBeanClassName(), holder.getBeanName());
171+
configClass = parse(bd.getBeanClassName(), holder.getBeanName());
172+
}
173+
174+
// Downgrade to lite (no enhancement) in case of no instance-level @Bean methods.
175+
if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(
176+
bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) {
177+
bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE,
178+
ConfigurationClassUtils.CONFIGURATION_CLASS_LITE);
172179
}
173180
}
174181
catch (BeanDefinitionStoreException ex) {
@@ -183,31 +190,37 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef
183190
this.deferredImportSelectorHandler.process();
184191
}
185192

186-
protected final void parse(@Nullable String className, String beanName) throws IOException {
187-
Assert.notNull(className, "No bean class name for configuration class bean definition");
188-
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
189-
processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
193+
final ConfigurationClass parse(AnnotationMetadata metadata, String beanName) {
194+
ConfigurationClass configClass = new ConfigurationClass(metadata, beanName);
195+
processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER);
196+
return configClass;
190197
}
191198

192-
protected final void parse(Class<?> clazz, String beanName) throws IOException {
193-
processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
199+
final ConfigurationClass parse(Class<?> clazz, String beanName) {
200+
ConfigurationClass configClass = new ConfigurationClass(clazz, beanName);
201+
processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER);
202+
return configClass;
194203
}
195204

196-
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
197-
processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
205+
final ConfigurationClass parse(@Nullable String className, String beanName) throws IOException {
206+
Assert.notNull(className, "No bean class name for configuration class bean definition");
207+
MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
208+
ConfigurationClass configClass = new ConfigurationClass(reader, beanName);
209+
processConfigurationClass(configClass, DEFAULT_EXCLUSION_FILTER);
210+
return configClass;
198211
}
199212

200213
/**
201214
* Validate each {@link ConfigurationClass} object.
202215
* @see ConfigurationClass#validate
203216
*/
204-
public void validate() {
217+
void validate() {
205218
for (ConfigurationClass configClass : this.configurationClasses.keySet()) {
206219
configClass.validate(this.problemReporter);
207220
}
208221
}
209222

210-
public Set<ConfigurationClass> getConfigurationClasses() {
223+
Set<ConfigurationClass> getConfigurationClasses() {
211224
return this.configurationClasses.keySet();
212225
}
213226

@@ -216,7 +229,7 @@ List<PropertySourceDescriptor> getPropertySourceDescriptors() {
216229
Collections.emptyList());
217230
}
218231

219-
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
232+
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) {
220233
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
221234
return;
222235
}
@@ -448,7 +461,7 @@ private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass)
448461

449462

450463
/**
451-
* Returns {@code @Import} class, considering all meta-annotations.
464+
* Returns {@code @Import} classes, considering all meta-annotations.
452465
*/
453466
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
454467
Set<SourceClass> imports = new LinkedHashSet<>();
@@ -636,7 +649,7 @@ private static class ImportStack extends ArrayDeque<ConfigurationClass> implemen
636649

637650
private final MultiValueMap<String, AnnotationMetadata> imports = new LinkedMultiValueMap<>();
638651

639-
public void registerImport(AnnotationMetadata importingClass, String importedClass) {
652+
void registerImport(AnnotationMetadata importingClass, String importedClass) {
640653
this.imports.add(importedClass, importingClass);
641654
}
642655

@@ -691,7 +704,7 @@ private class DeferredImportSelectorHandler {
691704
* @param configClass the source configuration class
692705
* @param importSelector the selector to handle
693706
*/
694-
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
707+
void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
695708
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
696709
if (this.deferredImportSelectors == null) {
697710
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
@@ -703,7 +716,7 @@ public void handle(ConfigurationClass configClass, DeferredImportSelector import
703716
}
704717
}
705718

706-
public void process() {
719+
void process() {
707720
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
708721
this.deferredImportSelectors = null;
709722
try {
@@ -727,7 +740,7 @@ private class DeferredImportSelectorGroupingHandler {
727740

728741
private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
729742

730-
public void register(DeferredImportSelectorHolder deferredImport) {
743+
void register(DeferredImportSelectorHolder deferredImport) {
731744
Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
732745
DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
733746
(group != null ? group : deferredImport),
@@ -737,7 +750,7 @@ public void register(DeferredImportSelectorHolder deferredImport) {
737750
deferredImport.getConfigurationClass());
738751
}
739752

740-
public void processGroupImports() {
753+
void processGroupImports() {
741754
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
742755
Predicate<String> exclusionFilter = grouping.getCandidateFilter();
743756
grouping.getImports().forEach(entry -> {
@@ -775,16 +788,16 @@ private static class DeferredImportSelectorHolder {
775788

776789
private final DeferredImportSelector importSelector;
777790

778-
public DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) {
791+
DeferredImportSelectorHolder(ConfigurationClass configClass, DeferredImportSelector selector) {
779792
this.configurationClass = configClass;
780793
this.importSelector = selector;
781794
}
782795

783-
public ConfigurationClass getConfigurationClass() {
796+
ConfigurationClass getConfigurationClass() {
784797
return this.configurationClass;
785798
}
786799

787-
public DeferredImportSelector getImportSelector() {
800+
DeferredImportSelector getImportSelector() {
788801
return this.importSelector;
789802
}
790803
}
@@ -800,23 +813,23 @@ private static class DeferredImportSelectorGrouping {
800813
this.group = group;
801814
}
802815

803-
public void add(DeferredImportSelectorHolder deferredImport) {
816+
void add(DeferredImportSelectorHolder deferredImport) {
804817
this.deferredImports.add(deferredImport);
805818
}
806819

807820
/**
808821
* Return the imports defined by the group.
809822
* @return each import with its associated configuration class
810823
*/
811-
public Iterable<Group.Entry> getImports() {
824+
Iterable<Group.Entry> getImports() {
812825
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
813826
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
814827
deferredImport.getImportSelector());
815828
}
816829
return this.group.selectImports();
817830
}
818831

819-
public Predicate<String> getCandidateFilter() {
832+
Predicate<String> getCandidateFilter() {
820833
Predicate<String> mergedFilter = DEFAULT_EXCLUSION_FILTER;
821834
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
822835
Predicate<String> selectorFilter = deferredImport.getImportSelector().getExclusionFilter();

spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-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.
@@ -67,6 +67,7 @@
6767
import org.springframework.core.task.SyncTaskExecutor;
6868
import org.springframework.stereotype.Component;
6969
import org.springframework.util.Assert;
70+
import org.springframework.util.ClassUtils;
7071

7172
import static org.assertj.core.api.Assertions.assertThat;
7273
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -104,6 +105,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespected() {
104105
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
105106
pp.postProcessBeanFactory(beanFactory);
106107
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
108+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR);
107109
Foo foo = beanFactory.getBean("foo", Foo.class);
108110
Bar bar = beanFactory.getBean("bar", Bar.class);
109111
assertThat(bar.foo).isSameAs(foo);
@@ -118,6 +120,7 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() {
118120
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
119121
pp.postProcessBeanFactory(beanFactory);
120122
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
123+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR);
121124
Foo foo = beanFactory.getBean("foo", Foo.class);
122125
Bar bar = beanFactory.getBean("bar", Bar.class);
123126
assertThat(bar.foo).isSameAs(foo);
@@ -132,6 +135,7 @@ void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() {
132135
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
133136
pp.postProcessBeanFactory(beanFactory);
134137
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
138+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR);
135139
Foo foo = beanFactory.getBean("foo", Foo.class);
136140
Bar bar = beanFactory.getBean("bar", Bar.class);
137141
assertThat(bar.foo).isNotSameAs(foo);
@@ -143,6 +147,7 @@ void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() {
143147
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
144148
pp.postProcessBeanFactory(beanFactory);
145149
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
150+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR);
146151
Foo foo = beanFactory.getBean("foo", Foo.class);
147152
Bar bar = beanFactory.getBean("bar", Bar.class);
148153
assertThat(bar.foo).isNotSameAs(foo);
@@ -154,6 +159,7 @@ void enhancementIsNotPresentForStaticMethods() {
154159
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
155160
pp.postProcessBeanFactory(beanFactory);
156161
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
162+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR);
157163
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue();
158164
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue();
159165
Foo foo = beanFactory.getBean("foo", Foo.class);
@@ -167,13 +173,23 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() {
167173
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
168174
pp.postProcessBeanFactory(beanFactory);
169175
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
176+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR);
170177
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("foo")).hasBeanClass()).isTrue();
171178
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("bar")).hasBeanClass()).isTrue();
172179
Foo foo = beanFactory.getBean("foo", Foo.class);
173180
Bar bar = beanFactory.getBean("bar", Bar.class);
174181
assertThat(bar.foo).isNotSameAs(foo);
175182
}
176183

184+
@Test
185+
void enhancementIsNotPresentWithEmptyConfig() {
186+
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class));
187+
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
188+
pp.postProcessBeanFactory(beanFactory);
189+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).hasBeanClass()).isTrue();
190+
assertThat(((RootBeanDefinition) beanFactory.getBeanDefinition("config")).getBeanClass().getName()).doesNotContain(ClassUtils.CGLIB_CLASS_SEPARATOR);
191+
}
192+
177193
@Test
178194
void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() {
179195
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(getClass().getName() + ".SingletonBeanConfig"));
@@ -1158,7 +1174,7 @@ static class NonEnhancedSingletonBeanConfig {
11581174
}
11591175

11601176
@Configuration
1161-
static class StaticSingletonBeanConfig {
1177+
static final class StaticSingletonBeanConfig {
11621178

11631179
@Bean
11641180
public static Foo foo() {
@@ -1171,6 +1187,10 @@ public static Bar bar() {
11711187
}
11721188
}
11731189

1190+
@Configuration
1191+
static final class EmptyConfig {
1192+
}
1193+
11741194
@Configuration
11751195
@Order(2)
11761196
static class OverridingSingletonBeanConfig {

spring-context/src/test/java/org/springframework/context/annotation/InvalidConfigurationClassDefinitionTests.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-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.
@@ -37,16 +37,18 @@ class InvalidConfigurationClassDefinitionTests {
3737
@Test
3838
void configurationClassesMayNotBeFinal() {
3939
@Configuration
40-
final class Config { }
40+
final class Config {
41+
@Bean String dummy() { return "dummy"; }
42+
}
4143

4244
BeanDefinition configBeanDef = rootBeanDefinition(Config.class).getBeanDefinition();
4345
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
4446
beanFactory.registerBeanDefinition("config", configBeanDef);
4547

4648
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
47-
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
48-
pp.postProcessBeanFactory(beanFactory))
49-
.withMessageContaining("Remove the final modifier");
49+
assertThatExceptionOfType(BeanDefinitionParsingException.class)
50+
.isThrownBy(() -> pp.postProcessBeanFactory(beanFactory))
51+
.withMessageContaining("Remove the final modifier");
5052
}
5153

5254
}

0 commit comments

Comments
 (0)