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
This commit is contained in:
parent
5bc701d4fe
commit
ec6a19fc6b
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.generator;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
|
||||
/**
|
||||
* Thrown when a bean definition could not be generated.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BeanDefinitionGenerationException extends RuntimeException {
|
||||
|
||||
private final String beanName;
|
||||
|
||||
private final BeanDefinition beanDefinition;
|
||||
|
||||
public BeanDefinitionGenerationException(String beanName, BeanDefinition beanDefinition, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.beanName = beanName;
|
||||
this.beanDefinition = beanDefinition;
|
||||
}
|
||||
|
||||
public BeanDefinitionGenerationException(String beanName, BeanDefinition beanDefinition, String message) {
|
||||
super(message);
|
||||
this.beanName = beanName;
|
||||
this.beanDefinition = beanDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean name that could not be generated.
|
||||
* @return the bean name
|
||||
*/
|
||||
public String getBeanName() {
|
||||
return this.beanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean definition that could not be generated.
|
||||
* @return the bean definition
|
||||
*/
|
||||
public BeanDefinition getBeanDefinition() {
|
||||
return this.beanDefinition;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.generator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
|
||||
/**
|
||||
* A {@link BeanFactoryContribution} that generates the bean definitions of a
|
||||
* bean factory, using {@link BeanRegistrationContributionProvider} to use
|
||||
* appropriate customizations if necessary.
|
||||
*
|
||||
* <p>{@link BeanRegistrationContributionProvider} can be ordered, with the default
|
||||
* implementation always coming last.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
* @see DefaultBeanRegistrationContributionProvider
|
||||
*/
|
||||
public class BeanDefinitionsContribution implements BeanFactoryContribution {
|
||||
|
||||
private final DefaultListableBeanFactory beanFactory;
|
||||
|
||||
private final List<BeanRegistrationContributionProvider> contributionProviders;
|
||||
|
||||
private final Map<String, BeanFactoryContribution> contributions;
|
||||
|
||||
BeanDefinitionsContribution(DefaultListableBeanFactory beanFactory,
|
||||
List<BeanRegistrationContributionProvider> contributionProviders) {
|
||||
this.beanFactory = beanFactory;
|
||||
this.contributionProviders = contributionProviders;
|
||||
this.contributions = new HashMap<>();
|
||||
}
|
||||
|
||||
public BeanDefinitionsContribution(DefaultListableBeanFactory beanFactory) {
|
||||
this(beanFactory, initializeProviders(beanFactory));
|
||||
}
|
||||
|
||||
private static List<BeanRegistrationContributionProvider> initializeProviders(DefaultListableBeanFactory beanFactory) {
|
||||
List<BeanRegistrationContributionProvider> providers = new ArrayList<>(SpringFactoriesLoader.loadFactories(
|
||||
BeanRegistrationContributionProvider.class, beanFactory.getBeanClassLoader()));
|
||||
providers.add(new DefaultBeanRegistrationContributionProvider(beanFactory));
|
||||
return providers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTo(BeanFactoryInitialization initialization) {
|
||||
writeBeanDefinitions(initialization);
|
||||
}
|
||||
|
||||
private void writeBeanDefinitions(BeanFactoryInitialization initialization) {
|
||||
for (String beanName : this.beanFactory.getBeanDefinitionNames()) {
|
||||
handleMergedBeanDefinition(beanName, beanDefinition -> {
|
||||
BeanFactoryContribution registrationContribution = getBeanRegistrationContribution(
|
||||
beanName, beanDefinition);
|
||||
registrationContribution.applyTo(initialization);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private BeanFactoryContribution getBeanRegistrationContribution(
|
||||
String beanName, RootBeanDefinition beanDefinition) {
|
||||
return this.contributions.computeIfAbsent(beanName, name -> {
|
||||
for (BeanRegistrationContributionProvider provider : this.contributionProviders) {
|
||||
BeanFactoryContribution contribution = provider.getContributionFor(
|
||||
beanName, beanDefinition);
|
||||
if (contribution != null) {
|
||||
return contribution;
|
||||
}
|
||||
}
|
||||
throw new BeanRegistrationContributionNotFoundException(beanName, beanDefinition);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleMergedBeanDefinition(String beanName, Consumer<RootBeanDefinition> consumer) {
|
||||
RootBeanDefinition beanDefinition = (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition(beanName);
|
||||
try {
|
||||
consumer.accept(beanDefinition);
|
||||
}
|
||||
catch (BeanDefinitionGenerationException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
String msg = String.format("Failed to handle bean with name '%s' and type '%s'",
|
||||
beanName, beanDefinition.getResolvableType());
|
||||
throw new BeanDefinitionGenerationException(beanName, beanDefinition, msg, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.generator;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
|
||||
/**
|
||||
* Thrown when no suitable {@link BeanFactoryContribution} can be provided
|
||||
* for the registration of a given bean definition.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BeanRegistrationContributionNotFoundException extends BeanDefinitionGenerationException {
|
||||
|
||||
public BeanRegistrationContributionNotFoundException(String beanName, BeanDefinition beanDefinition) {
|
||||
super(beanName, beanDefinition, String.format(
|
||||
"No suitable contribution found for bean with name '%s' and type '%s'",
|
||||
beanName, beanDefinition.getResolvableType()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.generator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.BDDMockito;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.aot.generator.DefaultGeneratedTypeContext;
|
||||
import org.springframework.aot.generator.GeneratedType;
|
||||
import org.springframework.aot.generator.GeneratedTypeContext;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.support.CodeSnippet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link BeanDefinitionsContribution}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class BeanDefinitionsContributionTests {
|
||||
|
||||
@Test
|
||||
void contributeThrowsContributionNotFoundIfNoContributionIsAvailable() {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition("test", new RootBeanDefinition());
|
||||
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory,
|
||||
List.of(Mockito.mock(BeanRegistrationContributionProvider.class)));
|
||||
BeanFactoryInitialization initialization = new BeanFactoryInitialization(createGenerationContext());
|
||||
assertThatThrownBy(() -> contribution.applyTo(initialization))
|
||||
.isInstanceOfSatisfying(BeanRegistrationContributionNotFoundException.class, ex -> {
|
||||
assertThat(ex.getBeanName()).isEqualTo("test");
|
||||
assertThat(ex.getBeanDefinition()).isSameAs(beanFactory.getMergedBeanDefinition("test"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributeThrowsBeanRegistrationExceptionIfContributionThrowsException() {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition("test", new RootBeanDefinition());
|
||||
BeanFactoryContribution testContribution = Mockito.mock(BeanFactoryContribution.class);
|
||||
IllegalStateException testException = new IllegalStateException();
|
||||
BDDMockito.willThrow(testException).given(testContribution).applyTo(ArgumentMatchers.any(BeanFactoryInitialization.class));
|
||||
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory,
|
||||
List.of(new TestBeanRegistrationContributionProvider("test", testContribution)));
|
||||
BeanFactoryInitialization initialization = new BeanFactoryInitialization(createGenerationContext());
|
||||
assertThatThrownBy(() -> contribution.applyTo(initialization))
|
||||
.isInstanceOfSatisfying(BeanDefinitionGenerationException.class, ex -> {
|
||||
assertThat(ex.getBeanName()).isEqualTo("test");
|
||||
assertThat(ex.getBeanDefinition()).isSameAs(beanFactory.getMergedBeanDefinition("test"));
|
||||
assertThat(ex.getCause()).isEqualTo(testException);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributeGeneratesBeanDefinitionsInOrder() {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition("counter", BeanDefinitionBuilder
|
||||
.rootBeanDefinition(Integer.class, "valueOf").addConstructorArgValue(42).getBeanDefinition());
|
||||
beanFactory.registerBeanDefinition("name", BeanDefinitionBuilder
|
||||
.rootBeanDefinition(String.class).addConstructorArgValue("Hello").getBeanDefinition());
|
||||
CodeSnippet code = contribute(beanFactory, createGenerationContext());
|
||||
assertThat(code.getSnippet()).isEqualTo("""
|
||||
BeanDefinitionRegistrar.of("counter", Integer.class).withFactoryMethod(Integer.class, "valueOf", int.class)
|
||||
.instanceSupplier((instanceContext) -> instanceContext.create(beanFactory, (attributes) -> Integer.valueOf(attributes.get(0)))).customize((bd) -> bd.getConstructorArgumentValues().addIndexedArgumentValue(0, 42)).register(beanFactory);
|
||||
BeanDefinitionRegistrar.of("name", String.class).withConstructor(String.class)
|
||||
.instanceSupplier((instanceContext) -> instanceContext.create(beanFactory, (attributes) -> new String(attributes.get(0, String.class)))).customize((bd) -> bd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hello")).register(beanFactory);
|
||||
""");
|
||||
}
|
||||
|
||||
private CodeSnippet contribute(DefaultListableBeanFactory beanFactory, GeneratedTypeContext generationContext) {
|
||||
BeanDefinitionsContribution contribution = new BeanDefinitionsContribution(beanFactory);
|
||||
BeanFactoryInitialization initialization = new BeanFactoryInitialization(generationContext);
|
||||
contribution.applyTo(initialization);
|
||||
return CodeSnippet.of(initialization.toCodeBlock());
|
||||
}
|
||||
|
||||
private GeneratedTypeContext createGenerationContext() {
|
||||
return new DefaultGeneratedTypeContext("com.example", packageName ->
|
||||
GeneratedType.of(ClassName.get(packageName, "Test")));
|
||||
}
|
||||
|
||||
static class TestBeanRegistrationContributionProvider implements BeanRegistrationContributionProvider {
|
||||
|
||||
private final String beanName;
|
||||
|
||||
private final BeanFactoryContribution contribution;
|
||||
|
||||
public TestBeanRegistrationContributionProvider(String beanName, BeanFactoryContribution contribution) {
|
||||
this.beanName = beanName;
|
||||
this.contribution = contribution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BeanFactoryContribution getContributionFor(String beanName, RootBeanDefinition beanDefinition) {
|
||||
return (beanName.equals(this.beanName) ? this.contribution : null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue