Spring JUnit4 手动/自动装配困境

发布于 2024-10-01 12:10:47 字数 5252 浏览 0 评论 0原文

我遇到了一个问题,这个问题只能用我对 Spring 的 IoC 容器设施和上下文设置的根本缺乏理解来解释,所以我要求对此进行澄清。

仅供参考,我维护的应用程序具有以下技术堆栈:

  • Java 1.6
  • Spring 2.5.6
  • RichFaces 3.3.1-GA UI
  • Spring 框架用于 bean 管理,Spring JDBC 模块用于 DAO 支持
  • Maven 用作构建管理器
  • JUnit 4.4 现在被引入作为测试引擎

我正在追溯(原文如此!)为应用程序编写 JUnit 测试,令我惊讶的是,我无法通过使用 setter 注入将 bean 注入到测试类中,而不求助于 @Autowire 表示法。

让我提供一个设置示例和随附的配置文件。

测试类 TypeTest 非常简单:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest {

    @Autowired
    private IType type;

    @Test
    public void testFindAllTypes() {
        List<Type> result;

        try {
            result = type.findAlltTypes();
            assertNotNull(result);
        } catch (Exception e) {
            e.printStackTrace();
            fail("Exception caught with " + e.getMessage());
        }
    }
}

它的上下文在 TestStackOverflowExample-context.xml 中定义:

<context:property-placeholder location="classpath:testContext.properties" />
<context:annotation-config />
<tx:annotation-driven />

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${db.connection.driver.class}" />
    <property name="url" value="${db.connection.url}" />
    <property name="username" value="${db.connection.username}" />
    <property name="password" value="${db.connection.password}" />
</bean>

<bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="beanDAO" class="com.example.BeanDAOImpl">
    <property name="ds" ref="dataSource"></property>
    <property name="beanDAOTwo" ref="beanDAOTwo"></property>
</bean>

<bean id="beanDAOTwo" class="com.example.BeanDAOTwoImpl">
    <property name="ds" ref="dataSource"></property>
</bean>

<bean id="type" class="com.example.TypeImpl">
    <property name="beanDAO" ref="beanDAO"></property>
</bean>

TestContext.properties 位于类路径中,仅包含数据源所需的特定于数据库的数据。

这就像一个魅力,但我的问题是 - 为什么当我尝试手动连接 bean 并执行 setter 注入时它不起作用,如下所示:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest {

    private IType type;

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

我在这里缺少什么?这里的哪一部分配置是错误的?当我尝试通过 setter 手动注入 bean 时,测试失败,因为这部分

result = type.findAlltTypes();

在运行时被解析为 null。当然,我查阅了 Spring 参考手册并尝试了 XML 配置的各种组合;我能得出的结论是 Spring 无法注入 beans,因为它以某种方式无法正确取消引用 Spring Test Context 引用,但是通过使用 @Autowired 这会“自动”发生,我真的不明白为什么会这样,因为两个 的 JavaDoc Autowired 注释及其 PostProcessor 类没有提到这一点。

另外值得补充的是,@Autowired 仅在此处的应用程序中使用。在其他地方仅执行手动接线,因此这也带来了问题 - 为什么在我的测试中它在那里工作,而不是在这里?我缺少 DI 配置的哪一部分? @Autowired 如何获取 Spring Context 的引用?

编辑: 我也尝试过这个,但结果相同:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest implements ApplicationContextAware{

    private IType type;

    private ApplicationContext ctx;

    public TypeTest(){
              super();
              ctx = new FileSystemXmlApplicationContext("/TypeTest-context.xml");
              ctx.getBean("type");
    }

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

也许还有其他想法吗?

编辑2: 我找到了一种无需编写自己的 TestContextListener 或 BeanPostProcessor 的方法。它非常简单,事实证明我上次编辑的方向是正确的:

1)基于构造函数的上下文解析:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest{

    private IType type;

    private ApplicationContext ctx;

    public TypeTest(){
         super();
         ctx = new FileSystemXmlApplicationContext("/TypeTest-context.xml");
         type = ctx.getBean("type");
    }

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

2)通过实现 ApplicationContextAware 接口:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest implements ApplicationContextAware{

    private IType type;
    private ApplicationContext ctx;

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

@Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
    this.ctx = ctx;
    type = (Type) ctx.getBean("type");
}

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

这两种方法都正确实例化了 bean。

I ran into an issue that can only be explained with my fundamental lack of understanding of Spring's IoC container facilities and context setup, so I would ask for clarification regarding this.

Just for reference, an application I am maintaing has the following stack of technologies:

  • Java 1.6
  • Spring 2.5.6
  • RichFaces 3.3.1-GA UI
  • Spring framework is used for bean management with Spring JDBC module used for DAO support
  • Maven is used as build manager
  • JUnit 4.4 is now introduced as test engine

I am retroactively (sic!) writing JUnit tests for the application and what suprised me is that I wasn't able to inject a bean into a test class by using setter injection without resorting to @Autowire notation.

Let me provide set up an example and accompanying configuration files.

The test class TypeTest is really simple:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest {

    @Autowired
    private IType type;

    @Test
    public void testFindAllTypes() {
        List<Type> result;

        try {
            result = type.findAlltTypes();
            assertNotNull(result);
        } catch (Exception e) {
            e.printStackTrace();
            fail("Exception caught with " + e.getMessage());
        }
    }
}

Its context is defined in TestStackOverflowExample-context.xml:

<context:property-placeholder location="classpath:testContext.properties" />
<context:annotation-config />
<tx:annotation-driven />

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="${db.connection.driver.class}" />
    <property name="url" value="${db.connection.url}" />
    <property name="username" value="${db.connection.username}" />
    <property name="password" value="${db.connection.password}" />
</bean>

<bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="beanDAO" class="com.example.BeanDAOImpl">
    <property name="ds" ref="dataSource"></property>
    <property name="beanDAOTwo" ref="beanDAOTwo"></property>
</bean>

<bean id="beanDAOTwo" class="com.example.BeanDAOTwoImpl">
    <property name="ds" ref="dataSource"></property>
</bean>

<bean id="type" class="com.example.TypeImpl">
    <property name="beanDAO" ref="beanDAO"></property>
</bean>

TestContext.properties is in classpath and contains only db-specific data needed for datasource.

This works like a charm but my question is - why doesn't it work when I try to manually wire beans and perform setter injection as in:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest {

    private IType type;

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

What am I missing here? What part of configuration is wrong here? When I try to manually inject beans via setters, test fails because this part

result = type.findAlltTypes();

is resolved as null in runtime. I've, of course, consulted the Spring reference manual and tried various combinations of XML configuration; all I could conclude is that Spring was unable to inject beans because it somehow fails to properly dereference Spring Test Context reference but by using @Autowired this happens "automagically" and I really can't see why is that because JavaDoc of both Autowired annotation and its PostProcessor class doesn't mention this.

Also worth adding is the fact that @Autowired is used in application only here. Elsewhere only manual wiring is performed, so this also brings forth question - why is it working there and not here, in my test? What part of DI configuration am I missing? How does @Autowired get reference of Spring Context?

EDIT:
I've also tried this but with same results:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest implements ApplicationContextAware{

    private IType type;

    private ApplicationContext ctx;

    public TypeTest(){
              super();
              ctx = new FileSystemXmlApplicationContext("/TypeTest-context.xml");
              ctx.getBean("type");
    }

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

Any other ideas, perhaps?

EDIT2:
I've found a way without resorting to writing own TestContextListener or BeanPostProcessor. It is suprisingly simple and it turns out that I was on the right track with my last edit:

1) Constructor-based context resolving:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest{

    private IType type;

    private ApplicationContext ctx;

    public TypeTest(){
         super();
         ctx = new FileSystemXmlApplicationContext("/TypeTest-context.xml");
         type = ctx.getBean("type");
    }

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

2) By implementing ApplicationContextAware interface:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TypeTest implements ApplicationContextAware{

    private IType type;
    private ApplicationContext ctx;

    public IType getType () {
        return type;
    }

    public void setType(IType type) {
        this.type= type;
    }

@Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
    this.ctx = ctx;
    type = (Type) ctx.getBean("type");
}

    @Test
    public void testFindAllTypes(){
    //snip, snip...
    }
}

Both of these approaches properly instanced beans.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

离鸿 2024-10-08 12:10:47

如果您查看 org.springframework.test.context.support.DependencyInjectionTestExecutionListener 的源代码,您将看到以下方法(为了清晰起见,进行了格式化和注释)

protected void injectDependencies(final TestContext testContext)
throws Exception {
    Object bean = testContext.getTestInstance();
    AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext()
            .getAutowireCapableBeanFactory();
    beanFactory.autowireBeanProperties(bean, 

            AutowireCapableBeanFactory.AUTOWIRE_NO,
            // no autowiring!!!!!!!!

            false
        );

    beanFactory.initializeBean(bean, testContext.getTestClass().getName());
    // but here, bean post processors are run

    testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}

:自动接线。然而,@AutoWired、@Resource等不使用自动装配机制,它们使用BeanPostProcessor。因此,当且仅当使用注释时(或者如果您注册了其他执行此操作的 BeanPostProcessor ),才会注入依赖项。

(上面的代码来自 Spring 3.0.x,但我敢打赌它在 2.5.x 中是相同的)

If you take a look at the source of org.springframework.test.context.support.DependencyInjectionTestExecutionListener, you will see the following method (formatted and commented for clarity):

protected void injectDependencies(final TestContext testContext)
throws Exception {
    Object bean = testContext.getTestInstance();
    AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext()
            .getAutowireCapableBeanFactory();
    beanFactory.autowireBeanProperties(bean, 

            AutowireCapableBeanFactory.AUTOWIRE_NO,
            // no autowiring!!!!!!!!

            false
        );

    beanFactory.initializeBean(bean, testContext.getTestClass().getName());
    // but here, bean post processors are run

    testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}

So the test object is a bean without auto-wiring. However, @AutoWired, @Resource etc, don't use the autowiring mechanism, they use BeanPostProcessor. And so the dependencies are injected if and only if the annotations are used (or if you register some other BeanPostProcessor that does it).

(The above code is from Spring 3.0.x, but I bet it was the same in 2.5.x)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文