Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace bean at runtime

The codebase is typical spring based enterprise codebase with about 1.5m lines of code. We have quite a few spring context files. The test infra is a problem.

For the test cases, I created another set of test-spring files (mostly it imports relevant project spring contexts) and for few beans contains mocked beans for external services. All Test classes use the same set of context configuration files and things are well 90% of the times.

But in some cases, there would be a bean which I would like to mock. But I do not wish to edit the spring-text.xml (as it will disturb all classes), nor do I wish to have separate set of xml's for each test class. One very simple say of doing it would be:

@Autowired
@Qualifier("fundManager")
FundManager fundManager;

@Test
public void testSomething(){
    TransactionManager tx = mock(TransactionManager.class);
    fundManager.setTransactionManager(tx);
    //now all is well.
}

This works in some cases. But sometimes, it is desired that this new temporary bean tx should be set where ever TransactionManager bean was being used all across the code base.

Proxy class IMHO is not a great solution, because I would then have to wrap all the beans with a wrapper. This is what I am ideally looking for:

@Test
public void testSomething(){
    TransactionManager tx = mock(TransactionManager.class);
    replace("transactionManagerBean",tx);
    //For bean with id:transactionManagerBean should be replace with `tx`
}

BeanPostProcessor looks like an alternate suggestion but I have faced a few hiccups with it.

like image 461
Jatin Avatar asked Oct 08 '15 13:10

Jatin


1 Answers

Imagine you have bean A injected into bean B:

public static class A {
}

public static class B {
    @Autowired
    private A a;

    @Override
    public String toString() {
        return "B [a=" + a + ']';
    }
}

And spring context to initialize your application:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
    <context:annotation-config/>
    <bean id="a" class="test.Test$A"/>
    <bean id="b" class="test.Test$B"/>
</beans>

Then the following snippet will replace all beans A across the context:

public static void main(String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("test.xml");

    System.out.println(ctx.getBean("b"));

    final A replacement = new A();
    for (String name : ctx.getBeanDefinitionNames()) {
        final Object bean = ctx.getBean(name);
        ReflectionUtils.doWithFields(bean.getClass(),
            field -> {
                field.setAccessible(true);
                field.set(bean, replacement);
            },
            // Here you can provide your filtering.
            field -> field.getType().equals(A.class)
        );
    }

    System.out.println(ctx.getBean("b"));
}

This example was made with Java 8 + Spring 4.1. However it would be simple to modify the code for the elder versions of both Java and Spring.

like image 124
ursa Avatar answered Oct 26 '22 20:10

ursa