Spring - 3. 注入
高级注入
环境注入
通过 profile, Spring 支持为不同的运行环境,如 dev, qa, prod 注入不同的 bean. 体现在:
- 为不同的环境配置不同的 configuration/xml
- 在同一个 configuration/xml 中, 为不同的环境配置不同的 bean
Java Config 配置 profile
为不同的环境配置不同的 configuration
@Configuration
@Profile("prod")
public class ProductionProfileConfig {
}
在同一个 configuration 中, 为不同的环境配置不同的 bean
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod="shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
// ...
}
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
// ...
}
}
XML 配置 profile
为不同的环境配置不同的 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:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
在同一个 xml 中, 为不同的环境配置不同的 bean
<?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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="qa">
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:url="jdbc:h2:tcp://dbserver/~/test"
p:driverClassName="org.h2.Driver"
p:username="sa"
p:password="password"
p:initialSize="20"
p:maxActive="30" />
</beans>
激活 profile
Spring 有两个参数来指定哪个 profile 被激活:
spring.profiles.active
, 若设置, 则 default 值无效spring.profiles.default
若都没设, 则没有 profile 被激活.
profile 设置的方法有:
-
DispatcherServlet
的初始化参数 -
Web application 的 context param
-
JNDI entries
-
环境变量
-
JVM 系统属性
-
@ActiveProfiles
注解, 一般用于集成测试.@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={PersistenceTestConfig.class}) @ActiveProfiles("dev") public class PersistenceTest { }
条件注入
条件注入适用于以下场景:
- 当 classpath 中存在特定的库, 才会创建 bean
- 当 某一个 bean 被声明, 才会创建 bean
- 当 某环境变量被设置, 才会创建 bean
条件注入方法:
-
创建 Bean 时, 使用
@Conditional(条件类.class)
@Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); }
-
创建 条件类, 该类需要实现
Condition
接口, 重写其match
方法public class MagicExistsCondition implements Condition { public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
ConditionContext
和 AnnotatedTypeMetadata
通过 ConditionContext
可以获取的信息有:
public interface ConditionContext {
BeanDefinitionRegistry getRegistry(); // 检查 Bean Definition
ConfigurableListableBeanFactory getBeanFactory(); // 获取 BeanFactory, 检查 Bean 的注入情况
Environment getEnvironment(); // 检查环境变量
ResourceLoader getResourceLoader(); // 获取资源
ClassLoader getClassLoader(); // 获取 ClassLoader
}
通过 AnnotatedTypeMetadata
可以获取的信息有:
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationType); // 是否被标注
Map<String, Object> getAnnotationAttributes(String annotationType); // 获取标注的属性
Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
}
自动装配消除二义
如果在装配时有多个 Bean 满足条件, 那么会抛出:
org.springframework.beans.factory.NoUniqueBeanDefinitionException
异常
解决方法:
@Primary
, 提高一个 Bean 注入的优先级@Qualifier("BeanID")
, 注入时选择要被注入的 Bean.- 自定义
@Qualifier
标注
@Primary
-
可以作用在类名上, 修饰
@Component
@Component @Primary public class IceCream implements Dessert { ... }
-
可以作用在方法上, 修饰
@Bean
@Bean @Primary public Dessert iceCream() { return new IceCream(); }
-
也可以使用 XML
<bean id="iceCream class="com.desserteater.IceCream" primary="true" />
@Primary
的不足在于, 同一种类型可以指定多个 @Primary
, 此时, Spring 还是无法决定哪个 Bean 被注入, 会抛出异常.
@Qualifier
@Qualifier
可以和 @Autowired
或者 @Inject
一起连用, 可以指明要注入的 Bean.
@Autowired
@Qualifier("iceCream") // 注入 ID 或者 qualifier-name 为 iceCream 的 Bean
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
@Qualifier("ID/QualifierName")
可以用 qualifier-name 来匹配. qualifier-name 可以通过以下方式指定:
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
或作用在方法上:
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
使用 @Qualifier
的问题是:
- Bean 的 ID 是用字符串指定的, 而字符串不会被编译器检查, 相当于硬编码, 日后注入 Bean 的 Id 改动, 就会报错.
- 当两个 Bean 的 qualifier-name 相同时, 又会产生二义性.
自定义 @Qualifier
标注
使用自定义的 @Qualifer
标注可以解决:
-
首先定义一个注解
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Creamy { }
-
然后用注解修饰生成 Bean 的方法或者类
@Component @Cold // 注解 Code @Creamy // 注解 Creamy public class IceCream implements Dessert { ... }
-
在注入的时候, 指定注解的 Bean
@Autowired @Cold @Creamy public void setDessert(Dessert dessert) { this.dessert = dessert; }
这样即解决了二义性的问题, 又解决了硬编码的问题.
Bean 的作用域
默认 Spring 的 Bean 为 Singleton. Spring 提供了以下作用域:
- Singleton (默认)
- Prototype
- Session
- Request
如何指定 Prototype 作用域?
-
JavaConfig 中的 @Scope 注解可以指定 bean 的作用域:
@Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Notepad notepad() { return new Notepad(); }
-
XML 的 scope 属性
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
Request 和 Session 作用域
为什么 Singleton 和 Prototype 不适用于 Web?
比如购物车, 不能所有人公用一个购物车(Singleton), 也不能系统每次需要就注入一个新的购物车.
@Component
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
与 Prototype 不同的是, Session 还要指定一个 proxyMode. 这是应为, 当服务启动 Spring 开始注入时, ShoppingCart
要注入到很多服务中:
@Component
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart) {
this.shoppingCart = shoppingCart;
}
}
但是此时这个对象还没有被创建. ShoppingCart
的创建是第一个相应用户请求到来时. 而且, Service 实际是要服务于多个用户, 意思是要注入不同的购物车.
所以, Spring 此时选择注入一个代理. 等到真正调用方法时, proxy 会将调用委托到真正的 ShoppingCart
方法上.
proxyMode 的选择
- 如果
ShoppingCart
是接口, 那么 proxyMode 应该为proxyMode=ScopedProxyMode.INTERFACES
. JDK 实现的动态代理. - 如果
ShoppingCart
是具体类, 那么 proxyMode 应该未proxyMode=ScopeProxyMode.TARGET_CLASS
. CGLib 实现.
XML 方式注入 Session Bean
-
接口注入
<bean id="cart" class="com.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false" /> </bean>
-
具体类注入
<bean id="cart" class="com.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy /> </bean>
运行时值注入
字符串直接在 JavaConfig 或者 XML 中注入属于 hardcode, 怎么解决呢?
- Property 占位符
- SpEL