Skip to content

Commit ec6a19f

Browse files
committed
Add BeanFactoryContribution for bean registrations
This commits adds an implementation that takes care of contributing code for each bean definition in the bean factory, invoking BeanRegistrationContributionProvider to determine the best candidate to use. Closes gh-28088
1 parent 5bc701d commit ec6a19f

File tree

4 files changed

+332
-0
lines changed

4 files changed

+332
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.beans.factory.generator;
18+
19+
import org.springframework.beans.factory.config.BeanDefinition;
20+
21+
/**
22+
* Thrown when a bean definition could not be generated.
23+
*
24+
* @author Stephane Nicoll
25+
* @since 6.0
26+
*/
27+
@SuppressWarnings("serial")
28+
public class BeanDefinitionGenerationException extends RuntimeException {
29+
30+
private final String beanName;
31+
32+
private final BeanDefinition beanDefinition;
33+
34+
public BeanDefinitionGenerationException(String beanName, BeanDefinition beanDefinition, String message, Throwable cause) {
35+
super(message, cause);
36+
this.beanName = beanName;
37+
this.beanDefinition = beanDefinition;
38+
}
39+
40+
public BeanDefinitionGenerationException(String beanName, BeanDefinition beanDefinition, String message) {
41+
super(message);
42+
this.beanName = beanName;
43+
this.beanDefinition = beanDefinition;
44+
}
45+
46+
/**
47+
* Return the bean name that could not be generated.
48+
* @return the bean name
49+
*/
50+
public String getBeanName() {
51+
return this.beanName;
52+
}
53+
54+
/**
55+
* Return the bean definition that could not be generated.
56+
* @return the bean definition
57+
*/
58+
public BeanDefinition getBeanDefinition() {
59+
return this.beanDefinition;
60+
}
61+
62+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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.beans.factory.generator;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.function.Consumer;
24+
25+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
26+
import org.springframework.beans.factory.support.RootBeanDefinition;
27+
import org.springframework.core.io.support.SpringFactoriesLoader;
28+
29+
/**
30+
* A {@link BeanFactoryContribution} that generates the bean definitions of a
31+
* bean factory, using {@link BeanRegistrationContributionProvider} to use
32+
* appropriate customizations if necessary.
33+
*
34+
* <p>{@link BeanRegistrationContributionProvider} can be ordered, with the default
35+
* implementation always coming last.
36+
*
37+
* @author Stephane Nicoll
38+
* @since 6.0
39+
* @see DefaultBeanRegistrationContributionProvider
40+
*/
41+
public class BeanDefinitionsContribution implements BeanFactoryContribution {
42+
43+
private final DefaultListableBeanFactory beanFactory;
44+
45+
private final List<BeanRegistrationContributionProvider> contributionProviders;
46+
47+
private final Map<String, BeanFactoryContribution> contributions;
48+
49+
BeanDefinitionsContribution(DefaultListableBeanFactory beanFactory,
50+
List<BeanRegistrationContributionProvider> contributionProviders) {
51+
this.beanFactory = beanFactory;
52+
this.contributionProviders = contributionProviders;
53+
this.contributions = new HashMap<>();
54+
}
55+
56+
public BeanDefinitionsContribution(DefaultListableBeanFactory beanFactory) {
57+
this(beanFactory, initializeProviders(beanFactory));
58+
}
59+
60+
private static List<BeanRegistrationContributionProvider> initializeProviders(DefaultListableBeanFactory beanFactory) {
61+
List<BeanRegistrationContributionProvider> providers = new ArrayList<>(SpringFactoriesLoader.loadFactories(
62+
BeanRegistrationContributionProvider.class, beanFactory.getBeanClassLoader()));
63+
providers.add(new DefaultBeanRegistrationContributionProvider(beanFactory));
64+
return providers;
65+
}
66+
67+
@Override
68+
public void applyTo(BeanFactoryInitialization initialization) {
69+
writeBeanDefinitions(initialization);
70+
}
71+
72+
private void writeBeanDefinitions(BeanFactoryInitialization initialization) {
73+
for (String beanName : this.beanFactory.getBeanDefinitionNames()) {
74+
handleMergedBeanDefinition(beanName, beanDefinition -> {
75+
BeanFactoryContribution registrationContribution = getBeanRegistrationContribution(
76+
beanName, beanDefinition);
77+
registrationContribution.applyTo(initialization);
78+
});
79+
}
80+
}
81+
82+
private BeanFactoryContribution getBeanRegistrationContribution(
83+
String beanName, RootBeanDefinition beanDefinition) {
84+
return this.contributions.computeIfAbsent(beanName, name -> {
85+
for (BeanRegistrationContributionProvider provider : this.contributionProviders) {
86+
BeanFactoryContribution contribution = provider.getContributionFor(
87+
beanName, beanDefinition);
88+
if (contribution != null) {
89+
return contribution;
90+
}
91+
}
92+
throw new BeanRegistrationContributionNotFoundException(beanName, beanDefinition);
93+
});
94+
}
95+
96+
private void handleMergedBeanDefinition(String beanName, Consumer<RootBeanDefinition> consumer) {
97+
RootBeanDefinition beanDefinition = (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition(beanName);
98+
try {
99+
consumer.accept(beanDefinition);
100+
}
101+
catch (BeanDefinitionGenerationException ex) {
102+
throw ex;
103+
}
104+
catch (Exception ex) {
105+
String msg = String.format("Failed to handle bean with name '%s' and type '%s'",
106+
beanName, beanDefinition.getResolvableType());
107+
throw new BeanDefinitionGenerationException(beanName, beanDefinition, msg, ex);
108+
}
109+
}
110+
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.beans.factory.generator;
18+
19+
import org.springframework.beans.factory.config.BeanDefinition;
20+
21+
/**
22+
* Thrown when no suitable {@link BeanFactoryContribution} can be provided
23+
* for the registration of a given bean definition.
24+
*
25+
* @author Stephane Nicoll
26+
* @since 6.0
27+
*/
28+
@SuppressWarnings("serial")
29+
public class BeanRegistrationContributionNotFoundException extends BeanDefinitionGenerationException {
30+
31+
public BeanRegistrationContributionNotFoundException(String beanName, BeanDefinition beanDefinition) {
32+
super(beanName, beanDefinition, String.format(
33+
"No suitable contribution found for bean with name '%s' and type '%s'",
34+
beanName, beanDefinition.getResolvableType()));
35+
}
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.beans.factory.generator;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.ArgumentMatchers;
23+
import org.mockito.BDDMockito;
24+
import org.mockito.Mockito;
25+
26+
import org.springframework.aot.generator.DefaultGeneratedTypeContext;
27+
import org.springframework.aot.generator.GeneratedType;
28+
import org.springframework.aot.generator.GeneratedTypeContext;
29+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
30+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
31+
import org.springframework.beans.factory.support.RootBeanDefinition;
32+
import org.springframework.javapoet.ClassName;
33+
import org.springframework.javapoet.support.CodeSnippet;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
37+
38+
/**
39+
* Tests for {@link BeanDefinitionsContribution}.
40+
*
41+
* @author Stephane Nicoll
42+
*/
43+
class BeanDefinitionsContributionTests {
44+
45+
@Test
46+
void contributeThrowsContributionNotFoundIfNoContributionIsAvailable() {
47+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
48+
beanFactory.registerBeanDefinition("test", new RootBeanDefinition());
49+
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory,
50+
List.of(Mockito.mock(BeanRegistrationContributionProvider.class)));
51+
BeanFactoryInitialization initialization = new BeanFactoryInitialization(createGenerationContext());
52+
assertThatThrownBy(() -> contribution.applyTo(initialization))
53+
.isInstanceOfSatisfying(BeanRegistrationContributionNotFoundException.class, ex -> {
54+
assertThat(ex.getBeanName()).isEqualTo("test");
55+
assertThat(ex.getBeanDefinition()).isSameAs(beanFactory.getMergedBeanDefinition("test"));
56+
});
57+
}
58+
59+
@Test
60+
void contributeThrowsBeanRegistrationExceptionIfContributionThrowsException() {
61+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
62+
beanFactory.registerBeanDefinition("test", new RootBeanDefinition());
63+
BeanFactoryContribution testContribution = Mockito.mock(BeanFactoryContribution.class);
64+
IllegalStateException testException = new IllegalStateException();
65+
BDDMockito.willThrow(testException).given(testContribution).applyTo(ArgumentMatchers.any(BeanFactoryInitialization.class));
66+
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory,
67+
List.of(new TestBeanRegistrationContributionProvider("test", testContribution)));
68+
BeanFactoryInitialization initialization = new BeanFactoryInitialization(createGenerationContext());
69+
assertThatThrownBy(() -> contribution.applyTo(initialization))
70+
.isInstanceOfSatisfying(BeanDefinitionGenerationException.class, ex -> {
71+
assertThat(ex.getBeanName()).isEqualTo("test");
72+
assertThat(ex.getBeanDefinition()).isSameAs(beanFactory.getMergedBeanDefinition("test"));
73+
assertThat(ex.getCause()).isEqualTo(testException);
74+
});
75+
}
76+
77+
@Test
78+
void contributeGeneratesBeanDefinitionsInOrder() {
79+
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
80+
beanFactory.registerBeanDefinition("counter", BeanDefinitionBuilder
81+
.rootBeanDefinition(Integer.class, "valueOf").addConstructorArgValue(42).getBeanDefinition());
82+
beanFactory.registerBeanDefinition("name", BeanDefinitionBuilder
83+
.rootBeanDefinition(String.class).addConstructorArgValue("Hello").getBeanDefinition());
84+
CodeSnippet code = contribute(beanFactory, createGenerationContext());
85+
assertThat(code.getSnippet()).isEqualTo("""
86+
BeanDefinitionRegistrar.of("counter", Integer.class).withFactoryMethod(Integer.class, "valueOf", int.class)
87+
.instanceSupplier((instanceContext) -> instanceContext.create(beanFactory, (attributes) -> Integer.valueOf(attributes.get(0)))).customize((bd) -> bd.getConstructorArgumentValues().addIndexedArgumentValue(0, 42)).register(beanFactory);
88+
BeanDefinitionRegistrar.of("name", String.class).withConstructor(String.class)
89+
.instanceSupplier((instanceContext) -> instanceContext.create(beanFactory, (attributes) -> new String(attributes.get(0, String.class)))).customize((bd) -> bd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hello")).register(beanFactory);
90+
""");
91+
}
92+
93+
private CodeSnippet contribute(DefaultListableBeanFactory beanFactory, GeneratedTypeContext generationContext) {
94+
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory);
95+
BeanFactoryInitialization initialization = new BeanFactoryInitialization(generationContext);
96+
contribution.applyTo(initialization);
97+
return CodeSnippet.of(initialization.toCodeBlock());
98+
}
99+
100+
private GeneratedTypeContext createGenerationContext() {
101+
return new DefaultGeneratedTypeContext("com.example", packageName ->
102+
GeneratedType.of(ClassName.get(packageName, "Test")));
103+
}
104+
105+
static class TestBeanRegistrationContributionProvider implements BeanRegistrationContributionProvider {
106+
107+
private final String beanName;
108+
109+
private final BeanFactoryContribution contribution;
110+
111+
public TestBeanRegistrationContributionProvider(String beanName, BeanFactoryContribution contribution) {
112+
this.beanName = beanName;
113+
this.contribution = contribution;
114+
}
115+
116+
@Override
117+
public BeanFactoryContribution getContributionFor(String beanName, RootBeanDefinition beanDefinition) {
118+
return (beanName.equals(this.beanName) ? this.contribution : null);
119+
}
120+
}
121+
122+
}

0 commit comments

Comments
 (0)