Skip to content

Commit

Permalink
Rename OverrideMetadata for Bean Overrides
Browse files Browse the repository at this point in the history
Prior to this commit, OverrideMetadata was the only public type in the
org.springframework.test.context.bean.override package whose name did
not start with BeanOverride. In addition, an OverrideMetadata component
plays multiple roles in addition to serving as a holder for metadata.

This commit therefore renames OverrideMetadata to BeanOverrideHandler.

In addition, this commit updates the affected documentation and renames
the following related methods in the Bean Override support.

- BeanOverrideHandler: createOverride() -> createOverrideInstance()
- BeanOverrideHandler: track() -> trackOverrideInstance()
- BeanOverrideProcessor: createMetadata() -> createHandler()
- BeanOverrideContextCustomizer: getMetadata() -> getBeanOverrideHandlers()
- BeanOverrideRegistrar: registerNameForMetadata() -> registerBeanOverrideHandler()
- BeanOverrideRegistrar: markWrapEarly() -> registerWrappingBeanOverrideHandler()

Closes gh-33702
  • Loading branch information
sbrannen committed Oct 16, 2024
1 parent ab4fe5a commit c0c78bd
Show file tree
Hide file tree
Showing 28 changed files with 669 additions and 658 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ https://site.mockito.org/[Mockito] third-party library.
The three annotations mentioned above build upon the `@BeanOverride` meta-annotation and
associated infrastructure, which allows one to define custom bean overriding variants.

To create custom bean override support, the following is needed:
To implement custom bean override support, the following is needed:

* An annotation meta-annotated with `@BeanOverride` that defines the
`BeanOverrideProcessor` to use
* A custom `BeanOverrideProcessor` implementation
* One or more concrete `OverrideMetadata` implementations provided by the processor
* One or more concrete `BeanOverrideHandler` implementations created by the processor

The Spring TestContext framework includes implementations of the following APIs that
support bean overriding and are responsible for setting up the rest of the infrastructure.
Expand All @@ -43,11 +43,11 @@ properties file].

The bean overriding infrastructure searches in test classes for any field meta-annotated
with `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
responsible for registering appropriate `OverrideMetadata`.
responsible for creating an appropriate `BeanOverrideHandler`.

The internal `BeanOverrideBeanFactoryPostProcessor` then uses that information to alter
the test's `ApplicationContext` by registering and replacing beans as defined by the
corresponding `BeanOverrideStrategy`:
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
defined by the corresponding `BeanOverrideStrategy`:

`REPLACE`::
Replaces the bean. Throws an exception if a corresponding bean does not exist.
Expand All @@ -65,11 +65,11 @@ heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` c
the name of the bean to override, or it can be unambiguously selected given the type of
the annotated field and its qualifying annotations.
Typically, the bean is selected by type by the `BeanOverrideFactoryPostProcessor`.
Typically, the bean is selected "by type" by the `BeanOverrideFactoryPostProcessor`.
Alternatively, the user can directly provide the bean name in the custom annotation.
Some `BeanOverrideProcessor` implementations could also internally compute a bean name
based on a convention or another advanced method.
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
a convention or some other method.
====

NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@
* accordingly.
*
* <p>For each override, the bean factory is prepared according to the chosen
* {@linkplain BeanOverrideStrategy override strategy}. The override value is created,
* if necessary, and the necessary infrastructure is updated to allow the value
* to be injected in the corresponding {@linkplain OverrideMetadata#getField() field}
* of the test class.
* {@linkplain BeanOverrideStrategy override strategy}. The bean override instance
* is created, if necessary, and the related infrastructure is updated to allow
* the bean override instance to be injected into the corresponding
* {@linkplain BeanOverrideHandler#getField() field} of the test class.
*
* <p>This processor does not work against a particular test class, but rather
* <p>This processor does not work against a particular test class but rather
* only prepares the bean factory for the identified, unique set of bean overrides.
*
* @author Simon Baslé
Expand All @@ -66,22 +66,23 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,

private static final BeanNameGenerator beanNameGenerator = DefaultBeanNameGenerator.INSTANCE;

private final Set<OverrideMetadata> metadata;
private final Set<BeanOverrideHandler> beanOverrideHandlers;

private final BeanOverrideRegistrar overrideRegistrar;
private final BeanOverrideRegistrar beanOverrideRegistrar;


/**
* Create a new {@code BeanOverrideBeanFactoryPostProcessor} with the supplied
* set of {@link OverrideMetadata} to process, using the given
* {@link BeanOverrideRegistrar}.
* @param metadata the {@link OverrideMetadata} instances to process
* @param overrideRegistrar the {@code BeanOverrideRegistrar} used to track
* metadata
* set of {@link BeanOverrideHandler BeanOverrideHandlers} to process, using
* the given {@link BeanOverrideRegistrar}.
* @param beanOverrideHandlers the bean override handlers to process
* @param beanOverrideRegistrar the registrar used to track bean override handlers
*/
BeanOverrideBeanFactoryPostProcessor(Set<OverrideMetadata> metadata, BeanOverrideRegistrar overrideRegistrar) {
this.metadata = metadata;
this.overrideRegistrar = overrideRegistrar;
BeanOverrideBeanFactoryPostProcessor(Set<BeanOverrideHandler> beanOverrideHandlers,
BeanOverrideRegistrar beanOverrideRegistrar) {

this.beanOverrideHandlers = beanOverrideHandlers;
this.beanOverrideRegistrar = beanOverrideRegistrar;
}


Expand All @@ -92,27 +93,27 @@ public int getOrder() {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (OverrideMetadata metadata : this.metadata) {
registerBeanOverride(beanFactory, metadata);
for (BeanOverrideHandler handler : this.beanOverrideHandlers) {
registerBeanOverride(beanFactory, handler);
}
}

private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
String beanName = overrideMetadata.getBeanName();
Field field = overrideMetadata.getField();
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
String beanName = handler.getBeanName();
Field field = handler.getField();
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));

switch (overrideMetadata.getStrategy()) {
case REPLACE -> replaceBean(beanFactory, overrideMetadata, true);
case REPLACE_OR_CREATE -> replaceBean(beanFactory, overrideMetadata, false);
case WRAP -> wrapBean(beanFactory, overrideMetadata);
switch (handler.getStrategy()) {
case REPLACE -> replaceOrCreateBean(beanFactory, handler, true);
case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, false);
case WRAP -> wrapBean(beanFactory, handler);
}
}

private void replaceBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata,
private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean requireExistingBean) {

// NOTE: This method supports 3 distinct scenarios which must be accounted for.
Expand All @@ -121,10 +122,10 @@ private void replaceBean(ConfigurableListableBeanFactory beanFactory, OverrideMe
// 2) AOT processing
// 3) AOT runtime

String beanName = overrideMetadata.getBeanName();
String beanName = handler.getBeanName();
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
beanName = getBeanNameForType(beanFactory, overrideMetadata, requireExistingBean);
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
if (beanName != null) {
// We are overriding an existing bean by-type.
beanName = BeanFactoryUtils.transformedBeanName(beanName);
Expand All @@ -141,7 +142,7 @@ private void replaceBean(ConfigurableListableBeanFactory beanFactory, OverrideMe
}
}
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (candidates.contains(beanName)) {
// We are overriding an existing bean by-name.
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
Expand All @@ -150,7 +151,7 @@ else if (requireExistingBean) {
throw new IllegalStateException("""
Unable to override bean: there is no bean to replace \
with name [%s] and type [%s]."""
.formatted(beanName, overrideMetadata.getBeanType()));
.formatted(beanName, handler.getBeanType()));
}
}

Expand Down Expand Up @@ -180,7 +181,7 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
"that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass().getName());
}

RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(overrideMetadata);
RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(handler);

// Generate a name for the nonexistent bean.
if (PSEUDO_BEAN_NAME_PLACEHOLDER.equals(beanName)) {
Expand All @@ -190,9 +191,9 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
registry.registerBeanDefinition(beanName, pseudoBeanDefinition);
}

Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
overrideMetadata.track(override, beanFactory);
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
Object override = handler.createOverrideInstance(beanName, existingBeanDefinition, null);
handler.trackOverrideInstance(override, beanFactory);
this.beanOverrideRegistrar.registerBeanOverrideHandler(handler, beanName);

// Now we have an instance (the override) that we can manually register as a singleton.
//
Expand All @@ -202,7 +203,7 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
//
// As a bonus, by manually registering a singleton during "AOT processing", we allow
// GenericApplicationContext's preDetermineBeanType() method to transparently register
// runtime hints for a proxy generated by the above createOverride() invocation --
// runtime hints for a proxy generated by the above createOverrideInstance() invocation --
// for example, when @MockitoBean creates a mock based on a JDK dynamic proxy.
if (beanFactory.containsSingleton(beanName)) {
destroySingleton(beanFactory, beanName);
Expand All @@ -211,73 +212,74 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
}

/**
* Check that the expected bean name is registered and matches the type to override.
* <p>If so, put the override metadata in the early tracking map.
* Check that a bean with the specified {@link BeanOverrideHandler#getBeanName() name}
* and {@link BeanOverrideHandler#getBeanType() type} is registered.
* <p>If so, put the {@link BeanOverrideHandler} in the early tracking map.
* <p>The map will later be checked to see if a given bean should be wrapped
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference}
* phase.
*/
private void wrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
String beanName = overrideMetadata.getBeanName();
private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
String beanName = handler.getBeanName();
if (beanName == null) {
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
int candidateCount = candidateNames.size();
if (candidateCount != 1) {
Field field = overrideMetadata.getField();
Field field = handler.getField();
throw new IllegalStateException("""
Unable to select a bean to override by wrapping: found %d bean instances of type %s \
(as required by annotated field '%s.%s')%s"""
.formatted(candidateCount, overrideMetadata.getBeanType(),
.formatted(candidateCount, handler.getBeanType(),
field.getDeclaringClass().getSimpleName(), field.getName(),
(candidateCount > 0 ? ": " + candidateNames : "")));
}
beanName = BeanFactoryUtils.transformedBeanName(candidateNames.iterator().next());
}
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException("""
Unable to override bean by wrapping: there is no existing bean \
with name [%s] and type [%s]."""
.formatted(beanName, overrideMetadata.getBeanType()));
.formatted(beanName, handler.getBeanType()));
}
}
validateBeanDefinition(beanFactory, beanName);
this.overrideRegistrar.markWrapEarly(overrideMetadata, beanName);
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
this.beanOverrideRegistrar.registerWrappingBeanOverrideHandler(handler, beanName);
this.beanOverrideRegistrar.registerBeanOverrideHandler(handler, beanName);
}

@Nullable
private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata,
private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean requireExistingBean) {

Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
int candidateCount = candidateNames.size();
if (candidateCount == 1) {
return candidateNames.iterator().next();
}
else if (candidateCount == 0) {
if (requireExistingBean) {
Field field = overrideMetadata.getField();
Field field = handler.getField();
throw new IllegalStateException(
"Unable to override bean: no beans of type %s (as required by annotated field '%s.%s')"
.formatted(overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(), field.getName()));
.formatted(handler.getBeanType(), field.getDeclaringClass().getSimpleName(), field.getName()));
}
return null;
}

Field field = overrideMetadata.getField();
Field field = handler.getField();
throw new IllegalStateException("""
Unable to select a bean to override: found %s beans of type %s \
(as required by annotated field '%s.%s'): %s"""
.formatted(candidateCount, overrideMetadata.getBeanType(), field.getDeclaringClass().getSimpleName(),
.formatted(candidateCount, handler.getBeanType(), field.getDeclaringClass().getSimpleName(),
field.getName(), candidateNames));
}

private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata,
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean checkAutowiredCandidate) {

ResolvableType resolvableType = metadata.getBeanType();
ResolvableType resolvableType = handler.getBeanType();
Class<?> type = resolvableType.toClass();

// Start with matching bean names for type, excluding FactoryBeans.
Expand All @@ -296,15 +298,15 @@ private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory b

// Filter out non-matching autowire candidates.
if (checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(metadata.getField(), true);
DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
}
// Filter out scoped proxy targets.
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);

// In case of multiple matches, fall back on the field's name as a last resort.
if (beanNames.size() > 1) {
String fieldName = metadata.getField().getName();
String fieldName = handler.getField().getName();
if (beanNames.contains(fieldName)) {
return Set.of(fieldName);
}
Expand All @@ -313,21 +315,21 @@ private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory b
}

/**
* Create a pseudo-{@link BeanDefinition} for the supplied {@link OverrideMetadata},
* Create a pseudo-{@link BeanDefinition} for the supplied {@link BeanOverrideHandler},
* whose {@linkplain RootBeanDefinition#getTargetType() target type} and
* {@linkplain RootBeanDefinition#getQualifiedElement() qualified element} are
* the {@linkplain OverrideMetadata#getBeanType() bean type} and
* the {@linkplain OverrideMetadata#getField() field} of the {@code OverrideMetadata},
* the {@linkplain BeanOverrideHandler#getBeanType() bean type} and
* the {@linkplain BeanOverrideHandler#getField() field} of the {@code BeanOverrideHandler},
* respectively.
* <p>The returned bean definition should <strong>not</strong> be used to create
* a bean instance but rather only for the purpose of having suitable bean
* definition metadata available in the {@link BeanFactory} &mdash; for example,
* for autowiring candidate resolution.
*/
private static RootBeanDefinition createPseudoBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve());
definition.setTargetType(metadata.getBeanType());
definition.setQualifiedElement(metadata.getField());
private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) {
RootBeanDefinition definition = new RootBeanDefinition(handler.getBeanType().resolve());
definition.setTargetType(handler.getBeanType());
definition.setQualifiedElement(handler.getField());
return definition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class BeanOverrideContextCustomizer implements ContextCustomizer {
"org.springframework.test.context.bean.override.internalWrapEarlyBeanPostProcessor";


private final Set<OverrideMetadata> metadata;
private final Set<BeanOverrideHandler> handlers;

BeanOverrideContextCustomizer(Set<OverrideMetadata> metadata) {
this.metadata = metadata;
BeanOverrideContextCustomizer(Set<BeanOverrideHandler> handlers) {
this.handlers = handlers;
}

@Override
Expand All @@ -58,17 +58,17 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte
// to register them as manual singleton instances. In addition, registration of
// the BeanOverrideBeanFactoryPostProcessor as a singleton is a requirement for
// AOT processing, since a bean definition cannot be generated for the
// Set<OverrideMetadata> argument that it accepts in its constructor.
// Set<BeanOverrideHandler> argument that it accepts in its constructor.
BeanOverrideRegistrar beanOverrideRegistrar = new BeanOverrideRegistrar(beanFactory);
beanFactory.registerSingleton(REGISTRAR_BEAN_NAME, beanOverrideRegistrar);
beanFactory.registerSingleton(INFRASTRUCTURE_BEAN_NAME,
new BeanOverrideBeanFactoryPostProcessor(this.metadata, beanOverrideRegistrar));
new BeanOverrideBeanFactoryPostProcessor(this.handlers, beanOverrideRegistrar));
beanFactory.registerSingleton(EARLY_INFRASTRUCTURE_BEAN_NAME,
new WrapEarlyBeanPostProcessor(beanOverrideRegistrar));
}

Set<OverrideMetadata> getMetadata() {
return this.metadata;
Set<BeanOverrideHandler> getBeanOverrideHandlers() {
return this.handlers;
}

@Override
Expand All @@ -80,12 +80,12 @@ public boolean equals(Object other) {
return false;
}
BeanOverrideContextCustomizer that = (BeanOverrideContextCustomizer) other;
return this.metadata.equals(that.metadata);
return this.handlers.equals(that.handlers);
}

@Override
public int hashCode() {
return this.metadata.hashCode();
return this.handlers.hashCode();
}

}
Loading

0 comments on commit c0c78bd

Please sign in to comment.