在测试中无法反射地改变Spring Controller内部的字段

时间:2015-03-04 03:29:03

标签: java spring spring-mvc reflection

我的高级开发人员和我昨天尝试使用模拟服务测试Spring控制器时遇到了这个奇怪的问题。我们没有使用Mockito,因为这是一个遗留系统,我们也不能为Mockito添加麻烦。

当我们在创建应用程序上下文后尝试使用Reflection来更改Controller中包含的@Autowired服务实例时,就开始出现这种陌生感。

我们尝试使用ReflectionUtilsReflectionTestUtils而只是使用vanilla反射,但类的实例根本不会改变。我们确认反射使该字段可访问,因为它是一个私有字段(在field.isAccessible()之前和之后使用field.setAccessible(true),但实例ID拒绝更改。通过检查确认多次在模拟和真实服务中使用toString()println()语句的实例。

我将发布以下代码:

控制器测试类:

public class PromoteFooControllerTest extends SpringTest {

    private MockHttpServletRequest request;
    private MockHttpServletResponse response; 
    private AnnotationMethodHandlerAdapter adapter;
    private FooController fooController;
    @Autowired
    @Qualifier("MockFooHelperSuccess")
    private FooHelper mockFooHelper;

    @Before
    public void setUp() throws Exception {
        adapter = new AnnotationMethodHandlerAdapter();

        HttpMessageConverter[] messageConverters = {new MappingJacksonHttpMessageConverter()};
        adapter.setMessageConverters(messageConverters); 
        /*Snipping boring request setup*/
    }   
    @Test 
    public void promoteFooTest() {
        System.out.println("Testing auto promote controller ");
        try {
            PromoteServiceResponse response = executePromoteController();
            assertTrue(response.getBrokerCenterURL() != null);
            assertTrue(response.getError() == null);

        } catch (Exception e) {
            e.printStackTrace();
            fail("Should have passed.");
        }
    }
    private PromoteServiceResponse executePromoteController() throws Exception {
        fooController = applicationContext.getBean(FooController.class);
        //Reflection happening here
        Field field = fooController.class.getField("fooHelper");
        field.setAccessible(true);
        //Next line does nothing!!!
        field.set(fooController, mockFooHelper);
        response = new MockHttpServletResponse();
        adapter.handle(request, response, fooController);
        String fooServiceResponse = response.getContentAsString();
        System.out.println(fooServiceResponse); 
        return Miscellaneous.fromJSON(new TypeReference<PromoteServiceResponse>() {
            }, fooServiceResponse);
    }
}

实际的FooController实现:

@Controller
@RequestMapping(value = "/foo")
public class FooController {

    @Autowired
    FooHelper fooHelper;
}

SpringTest类供参考:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/default-servlet.xml")
public class SpringTest implements ApplicationContextAware {

    protected ApplicationContext applicationContext;
    protected MockHttpServletRequest request;
    protected ServletRequestAttributes attributes;

    public void setApplicationContext(ApplicationContext applicationContext1)
            throws BeansException {
        applicationContext = applicationContext1;
    }

    protected void scopedBeansConfig() {
        ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) applicationContext
                .getAutowireCapableBeanFactory();
        configurableBeanFactory.registerScope("session", new SessionScope());
        configurableBeanFactory.registerScope("request", new RequestScope());
        request = new MockHttpServletRequest();
        attributes = new ServletRequestAttributes(request);
        RequestContextHolder.setRequestAttributes(attributes);
    }
}

Mocked Helper:

@Component("MockFooHelperSuccess")
public class MockFooHelperSuccessImpl implements FooHelper {

    @Override
    public FooServiceResponse exceuteCreateFoo(String userName,
            String password, String callingSystem, String keepLocking,
            String updateToken, SourceFoo sourceFoo) {
        return null;
    }

    @Override
    public FooSearchResponse executeSearchFoo(String userName,
            String password, FooSearchInput searchInput) {
        return null;
    }

    @Override
    public FooServiceResponse executeRetrieveFoo(String userName,
            String password, String fooId, String integrationId,
            String system, Boolean lockBar) {
        return null;
    }
    @Override
    public PromoteServiceResponse executePromoteFoo(String userName,
            String password, String fooId, Boolean systemReminder) {
        return PromoteServiceResponse.createSuccessResponse("testurl", "1-test");
    }

    @Override
    public PromoteValidateResponse executeValidateFoo(String employeeId,
            String personId, String userName, String password) {
        return PromoteValidateResponse.createSuccessResponse();
    }
}

默认-servlet.xml中:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">

    <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->

    <!-- Enables the Spring MVC @Controller programming model -->
    <annotation-driven />

    <!-- Imports user-defined @Controller beans that process client requests -->
    <beans:import resource="beans-config.xml" />
    <beans:import resource="dozer-mapping-config.xml" />
    <beans:import resource="aop-config.xml" />

</beans:beans>

AOP-config.xml中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">

    <bean id="exceptionHandler" class="au.com.foosystem.exceptions.ExceptionHandlerImpl"/>

    <aop:config>
        <aop:aspect ref="exceptionHandler">
            <aop:after-returning pointcut="execution(* au.com.foosystem.controller..*(..))" returning="result" method="sendExceptionEmail"/>
        </aop:aspect>
    </aop:config>
</beans>

我们最终通过更改测试类中的单行代码来实现它。我们改变了

fooController = applicationContext.getBean(FooController.class);

fooController = new FooController();

使用香草反射和ReflectionTestUtils整个过程完美无缺。

我们还尝试在控制器中添加一个getter / setter(是的,我们是绝望的)并从应用程序上下文中手动设置控制器内FooHelper的实例

fooController = applicationContext.getBean(FooController.class);
fooController.setFooHelper(mockFooHelper);
response = new MockHttpServletResponse();
adapter.handle(request, response, fooController);

但是,我们希望不将一个二传手放在一个控制器中,我们希望得到一个测试工作。

所以我们的问题是,为什么Reflection无法真正改变字段实例。我们的理解是,反射类似于所有物体的最终骨架键。我们怀疑Spring应用程序上下文中的某些内容要么阻止更改发生,要么在下一行运行之前的某个时刻恢复更改(因为我们在下一行上有println()

1 个答案:

答案 0 :(得分:1)

大概你的控制器在au.com.foosystem.controller包中,这是通过你的AOP配置建议的

<aop:config>
    <aop:aspect ref="exceptionHandler">
        <aop:after-returning pointcut="execution(* au.com.foosystem.controller..*(..))" returning="result" method="sendExceptionEmail"/>
    </aop:aspect>
</aop:config>

要应用此建议,Spring需要代理您的控制器bean。也就是说,它将生成一个动态类的代理对象,该类扩展了您的FooController类。此行为可能因版本而异,但在这种情况下,似乎代理对象实际上是指实际实例。

通过

fooController = applicationContext.getBean(FooController.class);

您将获得对代理对象的引用,该代理对象在内部保留对目标实际FooController实例的引用。现在因为它是FooController的动态子类,它通过继承也有一个FooHelper字段,但它没有使用它,而是将所有调用委托给目标FooController维护FooHelper字段。