今天读了《Spring实战》第三章,总结一下。
环境与profile
软件开发中,应用程序在不同环境之间迁移可能比较麻烦。不同系统中数据库配置、加密算法等可能不同,与外部系统的集成也完全不一样。
如在开发环境中可能会用嵌入式数据库并预先加载数据,在生产环境中可能使用JNDI从容器中获得数据源,在QA环境中可能要使用各种不同的数据源配置。
同一个bean在不同环境中内容可能完全不同。老旧的方式是在单独的配置类或XML中配置每个bean,然后环境迁移时用Maven的profiles
确定将哪个配置编译到应用中,这种方式是在构建时决策。
配置profile bean
Spring的bean profile可以在运行时确定使用哪个bean,这样就使同一个部署单元(如war包)能适用于所有环境,不需要重新构建。将这些不同的bean定义到不同的profile中,部署到环境时确保相应的profile处于激活状态即可。
在JavaConfig中配置
注解@Profile
可放在配置类上,如:
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
//各种@Bean...
}
表示在dev这个profile激活时该类的所有bean才会创建。常用"dev"
表示开发环境,用"prod"
表示生产环境。
注解@Profile
也可放在@Bean
方法上,这样可以在一个配置类中配置多个不同profile的bean。
在XML中配置
通过<beans>
元素的profile
属性,如profile="dev"
,来在XML中配置bean的profile。
当该属性配置在根<beans>
时,则整个文件中的bean都被配置成了这个profile;另外<beans>
可以有子<beans>
,通过在子<beans>
上配置,可以配置里面的一组bean在这个profile中。
激活profile
判断激活要看两个属性
- spring.profile.active
- spring.profile.default
如果有active
属性的话,它的值就确定那些profile被激活;如果没有active
的话,就由default
决定哪些profile被激活。
没有设置profile的bean始终会被创建,设置了profile的bean仅当其profile被激活时才被创建。
设置这两个属性
有多种方式设置这两个属性:
- 作为Web应用的上下文参数
- 作为DispatcherServlet的初始化参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在继承测试类上,使用
@ActiveProfile
注解设置
书上给出了第一和第二种设置方法,在
web.xml
中添加:
<context-param>
<param-name>spring.profile.default</param-name>
<param-value>dev</param-value>
</context-param>
在web.xml
中DispatcherServlet的<servlet>
标签内添加:
<init-param>
<param-name>spring.profile.default</param-name>
<param-value>dev</param-value>
</init-param>
如果要设置多个profile只要用逗号隔开。
书上给出了最后一种的设置方法,在 集成测试类上添加
@ActiveProfiles
注解:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
//...
}
集成测试时通常要激活的是开发环境的profile。
条件化的bean
@Conditional
注解
仅使用profile对bean分组有时会不够用,当需要更加复杂的条件时,就需要设置条件化的bean,可以在@Bean
方法上添加@Conditional
注解:
@Bean
@Conditional(UserExistsCondition.class)
public BookService bkSrvc(Book bk) {
BookService bookService = new BookServiceImp(bk);
return bookService;
}
该注解的参数类实现了Condition
接口:
public class UserExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//获取环境
Environment env=conditionContext.getEnvironment();
//判断"user"属性是否包含在环境中,返回true或false
return env.containsProperty("user");
}
}
实现了接口中的matches()
方法,该方法返回true时被@Conditional
注解的bean就会被创建,否则就会被忽略。
matches()
方法的两个参数
从matches()
方法会传入两个接口的实现类作为参数,里面定义了很多方法用来获取相关信息。
第一个参数ConditionContext
中的方法主要用来获取资源、类、环境的相关信息,第二个参数AnnotatedTypeMetadata
中的方法用来获取@Bean
方法还存在什么注解。
@Profile
注解本身就使用了
@Conditional
注解,使用
@Conditional
注解可以实现各种各样的条件,以实现复杂逻辑下bean的载入和忽略。
处理自动装配的歧义性
自动装配时,接口有多个实现bean时会出现歧义,使得自动装配不能进行。实际上出现歧义的场景很罕见,多数情况下一个接口只有一个实现bean,不会出现歧义。
两种方法,可以将冲突的bean中的某一个设为首选的,或者使用限定符将可选bean的范围缩小到一个。
标注首选bean
可以将@Primary
注解加在@Component
组件类上、或者加在JavaConfig的@Bean
方法上,使之成为首选bean。
在XML中,给<bean>
元素添加属性primary="true"
即指定其为首选bean。
限定自动装配的bean
使用@Qualifier
注解,配合@Autowired
或者@Inject
就可以限定哪些类可以被装配。以前一直以为@Qualifier
注解里面就是指定bean的id,实际上@Qualifier
注解里面指定的是限定符,而同时bean本身都会带有限定符,可以用@Qualifier
注解显式指明,限定符相同就允许该bean在这里自动装配。当不为bean指明限定符时,默认的限定符与bean的id相同。
自定义的限定符注解
限定符的命名,应该使用描述bean的部分特征的短语。一个bean可能有多个特征才能唯一标识,这时候就需要多个限定符来唯一确定它,但是没办法使用多个@Qualifier
注解。
在Java8之前,就是不允许在一个条目上出现多个相同类型的注解。在Java8中当注解本身带有
@Repeatable
时可以重复使用在同一条目上,但@Qualifier
注解不带有这个注解,所以不论如何目前@Qualifier
注解不能重复使用在同一条目上。
为解决这个问题,可以自定义注解,然后给自定义的注解加上@Qualifier
注解,这时候限定符就是这个自定义的注解,不但解决了这个问题,还能避免在@Qualifier
注解里面直接写限定符字符串的类型不安全。如:
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier//不需要在@Qualifier注解里添加属性了,这个自定义注解本身就会成为一个限定符
public @interface Cold {
}
总结这章的自定义注解
条件化注解
可以创建自定义注解,添加@Conditional
注解形成特定条件的注解,如@Profile
注解就是基于它实现的。
限定符注解
可以创建自定义注解,添加@Qualifier
注解形成自定义的限定符,类型更安全且可以多限定符修饰。
bean的scope
Spring的bean默认是单例的,但bean不一定是重用安全的,bean对象可能会在使用时发生变化被污染,重用可能会出现问题。
Spring的bean有以下几种作用域:
- 单例(Singleton):整个应用中只创建一个bean实例
- 原型(Prototype):在每次注入或者通过Spring上下文获取时都会创建一个bean实例
- 会话(Session):在Web应用中,为每个会话创建一个bean实例
- 请求(Request):在Web应用中,为每个请求创建一个bean实例
设置bean的scope
可以在@Component
标注的类或者@Bean
标注的bean方法上添加@Scope
注解,如:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Book book() {
return new Book();
}
在XML中,使用<bean>
元素的scope
属性如scope="prototype"
来设置作用域。
使用会话和请求作用域
在Web应用中常用,如购物车应使用会话作用域,它和特定的用户关联。会话作用域的设置如:
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES)
public ShopCar shopCar() {
return new ShopCar();
}
会话作用域的bean,对于某个会话而言是单例的,不同会话之间却相互独立。Spring不会将会话作用域的bean直接注入到使用它的bean中,而是注入一个代理,当使用者调用代理bean的方法时,代理会对其进行懒解析并将调用委托给会话作用域真正的bean。
注意上面的proxyMode
属性,值为ScopedProxyMode.INTERFACES
表示这个代理要实现bean的接口,并将调用委托给bean。当bean只是一个具体类时(没有返回接口),值应该为ScopedProxyMode.TARGET_CLASS
,表示使用CGLib生成基于类的代理。
在XML中生成scope代理
基于类的代理
相当于前面的ScopedProxyMode.TARGET_CLASS
:
<bean id="shopCar" class="org.sb.ShopCar" scope="session">
<aop:scoped-proxy/>
</bean>
基于接口的代理
相当于前面的ScopedProxyMode.INTERFACES
,这里要显式关闭”基于类”:
<bean id="shopCar" class="org.sb.ShopCar" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
运行时注入
不论是用JavaConfig还是用XML,之前学的都是硬编码的注入。很多时候会希望避免硬编码,使之在运行时再确定,Spring提供几种运行时求值的方式:
- 使用Environment
- 属性占位符
- Spring表达式语言(SpEL)
格式分别是:
- env.getXXX(“key”,…)
- “${…}”
- “#{…}”
使用Environment
Environment经常用来处理外部值,整个流程就两部:
- [1]声明属性源
- [2]通过Spring的
Environment
来检索属性
例如:
@Configuration
@PropertySource("classpath:dev/lzh.properties")//声明属性源
public class LzhConfig {
@Autowired
Environment env;//自动注入,这个的bean也不用写
@Bean
public Student student() {
//从Environment中检索属性值
return new Student(env.getProperty("lzh.age", Integer.class), env.getProperty("lzh.name"));
}
}
点进getProperty
可以看见一些常用的方法:
//判断在环境中是否存在某个key
boolean containsProperty(String var1);
//获取字面值,如果找不到相应的key则返回null
String getProperty(String var1);
//获取字面值,如果找不到相应的key则返回var2
String getProperty(String var1, String var2);
//获取字面值,并根据第二个参数(基本类型的包装类class)转换成基本类型值,找不到key返回null
<T> T getProperty(String var1, Class<T> var2);
//获取字面值,并根据第二个参数转换成基本类型值,找不到key返回var3
<T> T getProperty(String var1, Class<T> var2, T var3);
使用属性占位符
为了使用占位符,必须配置一个PropertySourcesPlaceholderConfigurer
,它能够基于Environment及其属性源解析占位符。
配置
如果是在JavaConfig中,可以手动添加这个bean:
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
属性源可以在配置类上使用@PropertySource
导入。
如果是在XML中,可以直接添加这个bean:
<context:property-placeholder/>
属性源可以直接指出:
<context:property-placeholder location="classpath:dev/lzh.properties"/>
使用
在XML中直接代替使用那些字面量的地方:
<bean class="org.lzh.model.Book" c:_0="${book.id}" c:_1="${book.name}"/>
在@Value
注解内配合占位符注入字面量,如在构造器参数上添加:
public Book(@Value("${book.id}") String title, @Value("${book.name}") int id) {
this.title = title;
this.id = id;
}
使用SpEL
Spring表达式语言——SpEL具有很多强大的功能:
- 使用bean的id引用bean
- 调用方法和访问对象的属性
- 对值进行算数、关系和逻辑运算
- 正则表达式匹配
- 集合操作
书上讲的很详细,这个东西很强大,但是要用好还是蛮复杂的,而且SpEL毕竟只是String类型的值,测试起来很困难。所以尽可能还是让表达式简洁一些,不要用太复杂的SpEL。
这里只记录一下实现和上面的属性占位符例子类似的功能,使用systemProperties
对象可以引用系统属性:
public Book(@Value("#{systemProperties['book.id']}") String title, @Value("#{systemProperties['book.name']}") int id) {
this.title = title;
this.id = id;
}
XML中的使用:
<bean class="org.lzh.model.Book" c:_0="#{systemProperties['book.id']}" c:_1="#{systemProperties['book.name']}"/>
书上说的”系统属性”指的是什么,我暂时还不清楚,好像不是从那个properties文件里读了(那个文件里没有感知到这里的使用,但是却可以感知到占位符)。
SpEL的功能远比属性占位符要强,而且IDEA能够感知SpEL,这样开发起来就舒服很多。关于SpEL的具体使用,有空再单独好好学一下。