怎样在Spring中使用JSR 330 Inject

Posted on 2010-02-02 17:47 Chi 阅读(762) 评论(0)  编辑  收藏

原文, http://earldouglas.com/node/21

JSR-330: Dependency Injection for Java  defines a collection of annotations which are used to define dependencies and their providers and scopes within a compliant application or framework. It is immediately recognizable by developers familiar with Google Guice, but is less so to developers familiar with Spring. Nevertheless, Spring's analog to (and influence on) JSR-330 is presented in the similarities between the two (not to mention Rod Johnson's participation as a Specification Lead of the JSR-330 team). Spring is not quite JSR-330 compliant, so a thin layer of code and configuration is needed to get there.

This example looks at some approaches to bringing Spring toward JSR-330 compliance, as well as some of the pitfalls in getting there. It begins with the familiarGreeter example, with a new implementation: SpanishGreeter.

package com.earldouglas.greeter;

public interface Greeter {

    public String getGreeting();
}

package com.earldouglas.greeter;

public class DefaultGreeter implements Greeter {

    public String getGreeting() {
        return "Hello World!";
    }
}

package com.earldouglas.greeter;

public class SpanishGreeter implements Greeter {

    public String getGreeting() {
        return "¡Hola Mundo!";
    }
}

To recap: an interface has been defined called Greeter which is implemented by DefaultGreeter and SpanishGreeter.

The first JSR-330 artifact to be implemented is @Inject, which is analogous to Spring's @Autowired annotation. The following test, AutowiredTest, demonstrates how @Autowired is used by Spring to inject a dependency.

package com.earldouglas.springjsr330.inject;

import static junit.framework.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.earldouglas.greeter.Greeter;

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

    @Autowired
    private Greeter greeter;

    @Test
    public void testGreeter() {
        assertEquals("Hello World!", greeter.getGreeting());
    }
}

AutowiredTest is injected by Spring with an instance of Greeter, as specified in the corresponding configuration file.

AutowiredTest-context.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"
   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean class="com.earldouglas.greeter.DefaultGreeter" />

</beans>

For Spring to inject dependencies annotated with @Inject as it does dependencies annotated with @Autowired, it is a simple matter of instantiating anAutowredAnnotationBeanPostProcessor, provided out of the box by Spring, and passing the javax.inject.Inject class name to itssetAutowiredAnnotationType() method. This tells Spring to treat @Inject-annotated entities as it does @Autowired-annotated entities. This is demonstrated in the following test, InjectTest.

package com.earldouglas.springjsr330.inject;

import static junit.framework.Assert.assertEquals;

import javax.inject.Inject;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.earldouglas.greeter.Greeter;

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

    @Inject
    private Greeter greeter;

    @Test
    public void testGreeter() {
        assertEquals("Hello World!", greeter.getGreeting());
    }
}

InjectTest is injected by Spring with an instance of Greeter, because Spring is told to inject @Inject-annotated entities in the corresponding configuration file.

InjectTest-context.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"
   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean class="com.earldouglas.greeter.DefaultGreeter" />

    <bean
       class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor">
        <property name="autowiredAnnotationType" value="javax.inject.Inject" />
    </bean>
    
</beans>

The next JSR-330 artifact to be implemented is @Qualifier, and more specifically Named. In JSR-330, Named is a certain type of @Qualifier, and is analogous to Spring's own @Qualifier annotation.

The following test, QualifierTest, demonstrates how Spring's @Qualifier annotation is used by Spring to inject a dependency.

package com.earldouglas.springjsr330.named;

import static junit.framework.Assert.assertEquals;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.earldouglas.greeter.Greeter;

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

    @Autowired
    @Qualifier("spanishGreeter")
    private Greeter greeter;

    @Test
    public void testGreeter() {
        assertEquals("¡Hola Mundo!", greeter.getGreeting());
    }
}

In the corresponding configuration file, two instances of Greeter are specified, but only one with the name spanishGreeter. This is the bean specified by the Spring @Qualifier annotation, and the bean injected into QualifierTest.

QualifierTest-context.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"
   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean class="com.earldouglas.greeter.DefaultGreeter" />

    <bean id="spanishGreeter" class="com.earldouglas.greeter.SpanishGreeter" />

</beans>

For Spring to inject dependencies annotated with @Named as it does dependencies annotated with @Qualifier, it is a simple matter of instantiating aCustomAutowireConfigurer, provided out of the box by Spring, and passing a Set including the javax.inject.Named class name to itssetCustomQualifierTypes() method. This tells Spring to treat @Named-annotated entities as it does @Qualifier-annotated entities. This is demonstrated in the following test, NamedTest.

package com.earldouglas.springjsr330.named;

import static junit.framework.Assert.assertEquals;

import javax.inject.Named;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.earldouglas.greeter.Greeter;

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

    @Autowired
    @Named("spanishGreeter")
    private Greeter greeter;

    @Test
    public void testGreeter() {
        assertEquals("¡Hola Mundo!", greeter.getGreeting());
    }
}

NamedTest is injected by Spring with the specified instance of Greeter, because Spring is told to inject @Named-annotated entities in the corresponding configuration file.

NamedTest-context.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:util="http://www.springframework.org/schema/util"
   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <bean class="com.earldouglas.greeter.DefaultGreeter" />

    <bean id="spanishGreeter" class="com.earldouglas.greeter.SpanishGreeter" />

    <bean
       class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
        <property name="customQualifierTypes">
            <util:set>
                <value>javax.inject.Named</value>
            </util:set>
        </property>
    </bean>

</beans>

The next JSR-330 artifact to be implemented is @Provider. In JSR-330, a Provider provides bound instances of classes based on a generic supplied in its instantiation through a get() method. To implement this, a BeanFactoryAware class called DefaultProvider is designed to look through a bean factory for an instance of a bean of the appropriate type.

package com.earldouglas.springjsr330;

import java.util.Map;

import javax.inject.Provider;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;

public class DefaultProvider<T> implements Provider<T>, BeanFactoryAware,
        InitializingBean {

    private BeanFactory beanFactory;
    final Class<T> type;

    DefaultProvider(Class<T> type) {
        this.type = type;
    }

    @SuppressWarnings("unchecked")
    @Override
    public T get() {
        ListableBeanFactory listableBeanFactory = (ListableBeanFactory) this.beanFactory;
        Map beans = listableBeanFactory.getBeansOfType(type);

        if (beans.values().size() != 1) {
            throw new NoSuchBeanDefinitionException(
                    type.getName(),
                    "No unique bean of type ["
                            + type.getName()
                            + "] is defined: expected single matching bean but found "
                            + beans.values().size() + ": "
                            + beans.keySet().toString());
        }

        return (T) beans.values().iterator().next();
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (!(this.beanFactory instanceof ListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "bean factory must be a ListableBeanFactory");
        }
    }

}

The following test, ProviderTest, demonstrates how @Provider is used by Spring to retrieve a dependency.

package com.earldouglas.springjsr330.provider;

import static junit.framework.Assert.assertEquals;

import javax.inject.Provider;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.earldouglas.greeter.Greeter;

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

    @Autowired
    private Provider<Greeter> provider;

    @Test
    public void testGreeter() {
        assertEquals("Hello World!", provider.get().getGreeting());
    }
}

An instance of DefaultProvider is injected by Spring with an instance of Greeter, and is injected into ProviderTest as specified in the corresponding configuration file.

ProviderTest-context.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"
   xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean class="com.earldouglas.greeter.DefaultGreeter" />

    <bean class="com.earldouglas.springjsr330.DefaultProvider">
        <constructor-arg value="com.earldouglas.greeter.Greeter" />
    </bean>

</beans>

The final JSR-330 artifact to be implemented is Scope, and more specifically Singleton. This one is much trickier, and presents an interesting integration problem. A number of approaches exist for this, such as taking advantage of Spring's CustomScopeConfigurer or writing a custom BeanFactoryPostProcessor to manage the scope of beans, but none have worked well enough to be considered adequate for presentation here. Part of this is due to the behavior of the DefaultListableBeanFactory in that it ignores scope changes after processing the context. Another part of this is that Spring scopes are altogether different from JSR-330 scopes, and as more come into the standard, each will require a custom correspondent in Spring to define the appropriate scope behavior. It will be left as an excercise for the reader, as well as for a future example.

The difficulties of scope aside, it is a relatively simple task to bring Spring up to the JSR-330 standard.


只有注册用户登录后才能发表评论。


网站导航: