原文, 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.