profile
为不同环境,选择最为合适的配置,且无需重新构建
@Profile
指定某个bean属于哪个profile
在将应用部署到每个环境时,一起确保对应的profile处于激活(active)状态
- 类级别 after version 3.1
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig(){
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:scema.sql")
.addScript("classpath:test-data.sql")
.build();
}
上述代码告诉Spring这个配置类中的bean只有在dev
profile激活时才会创建,否则忽略。
- 方法级别 after version 3.2
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import javax.activation.DataSource;
/**
* @author yanzy
* @date 2019/2/20 下午5:03
* @description
*/
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource embeddedDataSource() {
return (DataSource) new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:scema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Bean
@Profile("prod")
public DataSource jndiDataSource(){
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
上述例子中,两个环境中对应的注入数据源的bean放在同一个配置类中,在不同的profile被激活时,会创建对应的bean,该配置类中其他为标记@profile的方法,无论在什么环境下都创建。
XML profile
在<beans>
元素中使用profile,告知Spring框架此时激活的环境,需要创建的bean
还可以采用元素嵌套的方案,将多环境配置放到同一个XML文件中
激活Profile
Spring根据
- spring.profiles.active
- spring.profiles.default
两个独立属性确定哪个profile处于激活状态;
若上述两个属性都没设置,则未激活任何profile。
设置方法:
- 作为DispatcherServlet的初始化参数
- 作为应用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用
@ActiveProfiles
注解设置
一般可以将spring.profiles.default设置为开发环境的profile,并使用DispatcherServlet参数的方式进行设置,如在web应用中,web.xml中可以如下设置:
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframwork.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
上述方法设置spring.profiles.default,所有开发人员都能从版本控制软件获取应用程序源码,并使用开发环境的设置运行代码,无需额外配置
部署到QA、生产或其他环境时,负责部署到人根据情况使用系统属性
、环境变量
或JNDI
设置spring.profiles.active即可。
可以同时激活多个profile,以,
分隔。
集成测试时,@ActiveProfiles
可以用来指定运行测试时要激活的profile。
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("dev")
public class DemoApplicationTests {
@Test
public void contextLoads() {
}
}
* Conditional
用在带有@Bean注解的方法上;给定条件计算结果为true,创建bean,否则不创建。
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
@Configuration
public class TestConfig {
@Bean
@Conditional(TestCondition.class)
public Object testConfig(){
return new Object();
}
}
class TestCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("test");
}
}
上述代码中,定义了一个仅在环境变量中存在"test"属性时才会创建的bean。我们可以看出,@Conditional中给定了一个Class,指明了条件TestCondition。
指明条件的类可以是任意实现了
Condition
接口的类,只需实现matches(ConditionContext context, AnnotatedTypeMetadata metadata)
方法即可。
ConditionContext
ConditionContext可以做到一下几点:
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至查探bean的属性;
- 借助getEnviroment()返回的Enviroment检查环境变量是否存在及它的值是什么;
- 读取并查探getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在;
AnnotatedTypeMetadata
AnnotatedTypeMetadata可以检查带有@Bean注解的方法上还有其他什么注解:
- isAnnotated()判断带有@Bean的方法是否还有其他注解;
- 其他的getAnnotationAttributes、getAllAnnotationAttributes方法,可获取其他注解的属性;
@Profile从Spring4开始,基于@Conditional和Condition实现。ProfileCondition作为Condition的实现类,用于做出决策。
处理自动装配歧义性
不仅有一个bean匹配自动装配所需结果
eg:
若同一个接口I的三个不同实现类ImplA、ImplB、ImplC都是用@Component注解,注入为Bean,在使用@Autowired自动装配I时,没有唯一、无歧义的可选值,Spring会抛出NoUniqueBeanDefinitionException异常。
标示首选Bean(@Primary)
使用@Primary
来表达首选方案,可与@Component共用在组件扫描的bean上,也可以与@Bean共用在配置类中。
XML中可以在属性中,添加primary配置
<bean id="iceCream" class="com.desserteater.IceCream" primary="true"/>
⚠️注意:若标示了两个以上首选bean,则标示失效。
限定自动装配的bean
Spring限定符@Qualifier
能够在所有可选的bean上进行缩小范围的操作。
面向bean ID的限定符
@Qualifier
是使用限定符的主要方式,可以与@Autowired和@Inject协同使用,在注入时指定想要注入进去的是哪个bean。
@Bean
@Qualify
public class IceCream implement Dessert{
}
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
其中,Qualifier中设置的属性值(限定符)是想要注入的bean的ID
。所有使用@Component注解声明的类都会创建为bean且默认ID为首字母小写的类名
若@Qualifier未给定限定符,默认给一个与bean的ID相同的限定符。
使用默认限定符在bean进行重构,类名发生改变后,会导致@Qualifier中设置的限定符失效。此时,可以选择自定义限定符,将指定的限定符与要注入的bean名称解耦。
面向特性的的限定符
@Bean
@Qualifier("cold")
public class IceCream{
}
自定义限定符
面向特性的限定符比默认的基于bean ID的限定符更好一些,但是当有多个bean都具备相同特性时,依然会有歧义性的问题,我们可能会想到添加多个@Qualifier注解去解决这个问题
⚠️
@Bean
@Qualifier("cold")
@Qualifier("cream")
public class IceCream implement Dessert{
}
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
但这种做法存在一个问题:
java不允许在同一个条目上重复出现相同类型的多个注解
所以上述方式会导致java编译器报错。
我们可以使用创建自定义注解的方式解决这个问题。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
}
@Bean
@Cold
@Creamy
public class IceCream implement Dessert{
}
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
使用这种方法,相比于使用原始@Qualifier并借助String类型来指定限定符,也更为类型安全。
bean的作用域
- 单例(Singleton):默认,在整个应用中,只创建bean的一个声音
- 原型(Prototype):每次注入或通过Spring应用上下文获取的时候,都会创建一个新的bean实例
- 会话(Session):Web应用中,为每个会话创建一个bean实例
- 请求(Request):Web应用中,为每个请求创建一个bean实例
@Scope
基本使用
上述四种作用域都可以使用@Bean或@Component加@Scope的注解组合来进行声明
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{
...
}
这里使用了ConfigurableBeanFactory类的SCOPE_PROTOTYPE设置了原型作用域,相比于@Scope(“prototype”)来说,此方法更加安全并不易出错。
在java配置类中声明,可选择和@Bean一起使用。
使用XML来配置的话,可如下配置
<bean id="notepad"
class="com.myapp.Notepad"
scope="prototype"/>
使用web相关作用域
web相关的作用域配置,可以使用
WebApplicationContext.SCOPE_SESSION
、WebApplicationContext.SCOPE_REQUEST
来声明。
此处存在一个问题,当要向一个单例的bean中注入web相关的bean时,单例bean会在应用上下文加载的时候创建,并试图将web bean注入进来。但web bean是会话/请求作用域的,此时并不存在;
另外,系统中会有多个web bean实例,此时并不想让Spring注入某个固定的web bean实例到单例bean中,我们希望所使用的web bean恰好是当前绘画对应的那一个。
要解决上面问题,可以使用proxyMode
属性,如下:
@Bean
@Scope(value="WebApplicationContext.SCOPE_SESSION", proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){...}
@Component
public class StoreService{
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart = shoppingCart;
}
}
此时,Spring不会将实际的ShoppingCart注入到StoreService中,而是会注入一个到ShoppingCart的代理,它会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给回话作用域内真正的ShoppingCart bean。即委托类实现ShoppingCart接口,并将调用委托给实现bean。
上述方法仅对接口有效果,若ShoppingCart是类而非接口时,要用CGLib来生成基于类的代理。此时,使用ScopedProxyMode.TARGET_CLASS
,表明要以生成类扩展方式创建代理。
XML中声明作用域代理
<bean id="cart" class="com.myapp.ShoppingCart scope="session">
<aop:scoped-proxy>
</bean>
">
使用Spring aop命名空间的元素<aop:scoped-proxy>
与proxyMode的属性功能相同。但是其默认使用CGLib创建目标类的代理,我们可以将proxy-target-class属性设置为false,要求它生成基于接口的代理:
<aop:scoped-proxy proxy-target-class="false">
要使用上述标签,需要在XML配置中声明Spring的aop命名空间:
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
运行时注入
属性占位符(Property placeholder)
@PropertySource和Environment
- @PropertySource和Environment
处理外部值最简单的方式:
指定属性源,通过Spring的Environment来检索属性
@Configuration
@PropertySource("classpath:/com/myapp/app.properties")
public class ExpressiveConfig{
@Autowired
Environment env;
@Bean
public BlankDisc disc(){
return new BlankDisc(
env.getProperty("disc.title"),
env.getProperty("disc.artist")
);
}
}
上例中的Environment
还包含以下方法:
- String getProperty(String key, String defaultValue)指定属性不存在时的默认值;
- T getProperty(String key, Class type)转换获取到的属性类型;
- T getProperty(String key, Class type, T defaultValue)转换获取到的属性类型并指定默认值;
- String getRequiredProperty(String key)指定必须定义的属性,若无,抛出;IllegalStateException
- containsProperty(String key)检查属性是否存在;
- Class getPropertyAsClass(String key, Class clazz)属性解析为类;
- String[] getActiveProfiles()返回激活profile名称的数组;
- String[] getDefaultProfiles()返回默认profile的数组;
- boolean acceptsProfiles(String… profiles)environment支持给定profile,返回true;
属性占位符
形式: ${...}
使用:
- 依赖组件扫描和自动装配使用的方式
@Value("${...}")
- XML中使用的方式
<bean id="sgtPeppers"
class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}"/>
为了使用占位符,我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer
bean,后者可以基于Spring Environment及其属性源来解析占位符,所以3.1后推荐使用。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
使用XML配置,上述bean Spring context命名空间中的context:propertyplaceholder元素会为你生成。
解析外部属性能够将值的处理推迟到运行时。
Spring表达式语言(SpEL)
基本内容
Spring3引入
在其将值装配到bean属性和构造器参数的过程中,所使用的表达式会在运行时计算得到值。
特性:
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算数、关系和逻辑运算
- 正则表达式匹配
- 集合操作
SpEL表达式需要放到#{...}
中
#{T(System).currentTimeMills()}
上式最终结果是计算表达式的那一刻当前时间的毫秒数。
T()表达式会将java.lang.System视为java中对应的类型。
- 表达式可以引用其他bean或其他bean的属性
#{sgtPeppers.artist}
- 表达式可以通过systemProperties对象引用系统属性
#{systemProperties['disc.title']}
bean装配时使用SpEL
- 通过组件扫描创建bean
注入属性和构造器参数时,可以使用@Value
public BlanckDisc(
@Value("#{systemProperties['disc.title']}") String title
){
this.title = title;
}
XML配置中,可以将SpEL表达式传入或的value属性中,或作为p-或c-命名空间条目的值。
<bean id="sgtPeppers"
class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}">
基础表达式
-
表示字面值
浮点
#{3.14159}
科学计数法
#{9.87E4}
String类型
#{'hello'}
Boolean类型
#{true}
-
引用bean、属性和方法
通过ID引用其它bean,可以使用SpEL将一个bean装配到另外一个bean的属性中,此时bean ID作为SpEL的表达式,在下例中,就是sgtPeppers。
#{sgtPeppers}
引用sgtPeppers的artist属性
#{sgtPeppers.artist}
调用方法
#{sgtPeppers.selectArtist()}
对于被调用方法的返回值,同样可以调用它的方法
#{artistSelector.selectArtist().toUpperCase()}
防止空指针异常,使用类型安全的运算符
#{artistSelector.selectArtist()?.toUpperCase()}
-
表达式中使用类型
依赖T()
这个关键运算符来访问类作用域的方法和常量,如,表达Java的Math类
#{T(java.lang.Math)}
运算结果会是一个Class对象,其真正的价值在于其可以访问目标类型的静态方法和常量
#{T(java.lang.Math).PI}
#{T(java.lang.Math).random()}
SpEL运算符
运算符类型 | 运算符 |
---|---|
算数运算 | +、-、*、/、%、^ |
比较运算 | <、>、==、<=、>=、lt、gt、eq、le、ge |
逻辑运算 | and、 or、 not、 | |
条件运算 | ?:(ternary)、?:(Elvis) |
#{2 * T(java.lang.Math).PI * circle.radius}
String类型+
操作
#{disc.title + 'by' + disc.artist}
比较操作符(符号形式和文本形式一样)
#{counter.total == 100}
或#{counter.total eq 100}
三元运算
#{scoreboard.score > 1000 ? "Winner" : "Loser"}
Elvis为判断结果是否为null的三元计算,命名灵感来自?像猫王的头发???
#{disc.title?:'Rattle and Hum'}
计算正则表达式
通过matches
运算符支持表达式中的模式匹配。
左边参数为String类型的文本,右边参数为正则表达式,返回Boolean类型的值。
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
上式为判断一个字符串是否包含有效的邮件地址。
计算集合
- 引用列表中的一个元素
#{jukebox.songs[4].title}
集合来源于id为jukebox的bean
丰富版
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
[]
还可以从String中获取一个字符
#{'This is a test'[3]}
- 查询运算符
.?[]
获取jukebox中artist属性为Aerosmith的所有歌曲
#{jukebox.songs.?[artist eq 'Aerosmith']}
.^[]
在集合中查询第一个匹配项
#{jukebox.songs.^[artiest eq 'Aerosmith']}
.$[]
在集合中查询最后一个匹配项
#{jukebox.songs.$[artiest eq 'Aerosmith']}
.![]
投影运算,从集合的每个成员中选择特定的属性放到另一个集合中
#{jukebox.songs.![title]}