Log warning when one Bean Override overrides another Bean Override

It is currently possible for one Bean Override to override another
logically equivalent Bean Override.

For example, a @⁠TestBean can override a @⁠MockitoBean, and vice versa.

In fact, it's also possible for a @⁠MockitoBean to override another
@⁠MockitoBean, for a @⁠TestBean to override a @⁠TestBean, etc.

However, there may be viable use cases for one override overriding
another override. For example, one may have a need to spy on a bean
created by a @⁠TestBean factory method.

In light of that, we do not prohibit one Bean Override from overriding
another Bean Override; however, with this commit we now log a warning
to help developers diagnose issues in case such an override is
unintentional.

For example, given the following test class, where TestConfig registers
a single bean of type MyService named "myService"...

@⁠SpringJUnitConfig(TestConfig.class)
class MyTests {

  @⁠TestBean(methodName = "example.TestUtils#createMyService")
  MyService testService;

  @⁠MockitoBean
  MyService mockService;

  @⁠Test
  void test() {
    // ...
  }
}

... running that test class results in a log message similar to the
following, which has been formatted for readability.

WARN - Bean with name 'myService' was overridden by multiple handlers:
[
 [TestBeanOverrideHandler@44b21f9f
  field = example.MyService example.MyTests.testService,
  beanType = example.MyService,
  beanName = [null],
  strategy = REPLACE_OR_CREATE
 ],
 [MockitoBeanOverrideHandler@7ee8130e
  field = example.MyService example.MyTests.mockService,
  beanType = example.MyService,
  beanName = [null],
  strategy = REPLACE_OR_CREATE,
  reset = AFTER,
  extraInterfaces = set[[empty]],
  answers = RETURNS_DEFAULTS, serializable = false
 ]
]

NOTE: The last registered BeanOverrideHandler wins. In the above
example, that means that @⁠MockitoBean overrides @⁠TestBean, resulting
in a Mockito mock for the MyService bean in the test's
ApplicationContext.

Closes gh-34056
This commit is contained in:
Sam Brannen
2024-12-10 12:48:24 +01:00
parent a00ba8dbcf
commit 54948a4e88
5 changed files with 252 additions and 17 deletions

View File

@@ -17,8 +17,13 @@
package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@@ -37,9 +42,12 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideRegistry {
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new HashMap<>();
private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class);
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new HashMap<>();
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new LinkedHashMap<>();
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new LinkedHashMap<>();
private final ConfigurableBeanFactory beanFactory;
@@ -57,7 +65,25 @@ class BeanOverrideRegistry {
* bean via {@link #wrapBeanIfNecessary(Object, String)}.
*/
void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) {
Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () ->
"Cannot register BeanOverrideHandler for bean with name '%s'; detected multiple registrations for %s"
.formatted(beanName, handler));
// Check if beanName was already registered, before adding the new mapping.
boolean beanNameAlreadyRegistered = this.handlerToBeanNameMap.containsValue(beanName);
// Add new mapping before potentially logging a warning, to ensure that
// the current handler is logged as well.
this.handlerToBeanNameMap.put(handler, beanName);
if (beanNameAlreadyRegistered && logger.isWarnEnabled()) {
List<BeanOverrideHandler> competingHandlers = this.handlerToBeanNameMap.entrySet().stream()
.filter(entry -> entry.getValue().equals(beanName))
.map(Entry::getKey)
.toList();
logger.warn("Bean with name '%s' was overridden by multiple handlers: %s"
.formatted(beanName, competingHandlers));
}
if (handler.getStrategy() == BeanOverrideStrategy.WRAP) {
this.wrappingBeanOverrideHandlers.put(beanName, handler);
}