Spring学习笔记-高级装配

本章内容:

  • Spring profile
  • 条件化的bean声明
  • 自动装配与歧义性
  • bean的作用域
  • Spring表达式语言

3.1 环境与profile

在软件开发的不同阶段需要不同的环境和配置。

1
2
3
4
5
6
7
@Bean(destroyMethod = "shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.addScript("classpath:ch3.sql")
.addScript("classpath:ch3.1.sql")
.build();
}

为了适应环境更换的需求,可以将所需要的所有的配置类配置到每个bean中,然后在构建阶段选择需要使用的bean,但是从开发环境切换到生产环境时可能会发生问题。

3.1.1 配置profile bean

Spring为此种场景提供了profile功能。

使用profile注解来声明在合适的阶段使用合适的bean。将所有的bean整理到一个profile中,确保在需要的时候active相应的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.ch3;

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.sql.DataSource;

/**
* @author zhulongkun20@163.com
* @since 2018/11/24 下午1:00
*/
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:test.sql")
.addScript("classpath:test1.sql")
.build();
}

@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean =
new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jndi/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}

虽然所有的bean都被声明在一个profile里,但是只有当指定的profile被激活时,相应的bean才会被创建,没有指定profile的bean始终都会被创建,与激活的profile没有关系。

在XML中配置profile:

可以通过beans元素的profile属性,在xml中配置profile。

1
2
3
4
5
6
7
8
9
10
11
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"
profile="dev">

<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:test.sql"/>
<jdbc:script location="classpath:test1.sql"/>
</jdbc:embedded-database>
</beans>

只有profile属性与当前激活的profile相匹配的配置文件才会被用到。

重复使用beans属性指定多个profile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">

<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:test.sql"/>
<jdbc:script location="classpath:test1.sql"/>
</jdbc:embedded-database>
</beans>

<beans profile="prod">
<jee:jndi-lookup jndi-name="jdbc/MyDatabase" id="dataSource" resource-ref="true" proxy-interface="javax.sql.DataSource"/>
</beans>
</beans>

虽然id都一样,类型都是javax.sql.dataSource,但是只会创建指定profile的bean。

3.1.2 激活profile

Spring在确定处于激活状态的profile时,依赖于两个独立的属性:

  • spring.profiles.active
  • spring.profiles.default

优先级从上到下,如果spring.profiles.active没有设置,则看spring.profiles.default,否则只会创建没有定义在profiles中的bean。

有多种方式设置这两个属性:

  • 作为DispatcherServlet的初始化参数
  • 作为web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM属性
  • 在集成测试类上使用@ActiveProfiles属性

在web.xml配置文件中设置默认的profile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>

<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.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>

可以同时激活多个profile,以逗号分隔。

使用profile进行测试:

Spring提供了@ActiveProfiles注解,用来指定测试时使用的profile。

1
2
3
4
5
6
@RunWith(SpringJunit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {

}

3.2 条件化的bean

需求:

  1. 希望一个或多个bean只有在类路径下包含某个特定的库时才创建
  2. 希望某个bean在特定的bean声明之后再创建

Spring 4引入了@Conditional注解,只有条件计算结果为true才会创建bean,否则不创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ch3;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* @author zhulongkun20@163.com
* @since 2018/11/24 下午2:07
*/
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
return environment.containsProperty("magic");
}
}

1
2
3
4
5
@Bean
@Conditional(MagicExistsCondition.class) //条件化创建bean
public MagicBean magicBean() {
return new MagicBean();
}

ConditionContext接口:

1
2
3
4
5
6
7
8
9
10
11
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();

ConfigurableListableBeanFactory getBeanFactory();

Environment getEnvironment();

ResourceLoader getResourceLoader();

ClassLoader getClassLoader();
}
  • getRegistry:根据返回值可以检查bean定义
  • getEnvirnment:检查环境变量
  • getResourceLoader:读取加载的资源
  • getClassLoader:加载并检查类是否存在

AnnotatedTypeMetadata接口:

1
2
3
4
5
6
7
8
9
10
11
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String var1);

Map<String, Object> getAnnotationAttributes(String var1);

Map<String, Object> getAnnotationAttributes(String var1, boolean var2);

MultiValueMap<String, Object> getAllAnnotationAttributes(String var1);

MultiValueMap<String, Object> getAllAnnotationAttributes(String var1, boolean var2);
}

3.3 处理启动装配的歧义性

仅有一个bean匹配所需结果时,自动装配才是有效的,如果有多个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数和方法参数。

Spring提供的解决方案:

  • 将可选bean中的其中一个声明为首选(primary)
  • 使用限定符(qualifier)缩小可选范围

3.3.1 标示首选的bean

将其中一个可选的bean声明为首选可以避免自动装配的歧义性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

@Component
@Primary
public class IceCream implements Dessert {
//...
}

@Bean
@Primary
public Dessert dessert() {
return new IceCream();
}

xml配置:

1
<bean id="iceCream" class="com.test.dessert.IceCream" primary="true"/>

3.3.2 限定自动装配的bean

设置首选bean的局限性在于 @Primary无法将可选方案范围限定到一个无歧义性的选项中 ,当首选bean的数量超过一个时,无法进一步缩小限定范围。

@Qualifier注解是使用限定符的主要方式,与@Autowired协同使用,在注入时指定要注入的bean。

1
2
3
4
5
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

@Qualifier注解的参数就是想要注入的bean的id,所有使用@Component注解的类都会创建为bean,且id为首字母小写的类名。

基于默认id作为限定符是简单的,但是当类名被更改之后会使限定符失效。

创建自定义的限定符:

可以设置自己的限定符,而不依赖于bean id作为限定符。

1
2
3
4
5
@Component
@Qualifier("cold")
public class IceCream implements Dessert {

}

此时cold限定符分配给了IceCream bean,只需要在合适的地方引入cold限定符即可自动装配。

1
2
3
4
5
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}

此时类限定名的变更不会影响到自动装配。但是当应用中出现同名的注解@Qualifier(“cold”)时,歧义性又会再次出现。

这时需要多个@Qualifier注解来进一步缩小限定范围。


3.4 bean的作用域

默认情况下,Spring应用上下文中的所有bean都是以单例模式创建的。不管给定的bean被注入到其他bean多少次,每次注入的都是同一个实例。

如果一个类是可变(mutable)的,那么对其进行重用时可能会遇到意想不到的问题。

Spring定义的bean作用域:

  • 单例(Singleton):在整个应用中,只创建一个bean;
  • 原型(Prototype):每次注入或者通过上下文获取bean时都创建一个新的bean;
  • 会话(Session):在Web应用中,为每个回话创建一个bean;
  • 请求(Request):在Web应用中,为每个请求创建一个bean。

@Scope注解:

用来指定bean的作用域:

1
2
3
4
5
6
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//或者 @Scope("prototype")
public class Notepad {
//something
}

XML配置:

1
<bean id="notepad" class="com.app.Notepad" scope="prototype" />

3.5 运行时值注入

Spring提供了两种运行时求值的方式:

  • 属性占位符(Property placeholder);
  • Spring表达式语言(S片EL)。
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@PropertySource("classpath:/com/soundsys/app.properties")
public class ExpressiveConfig {
@Autowired
Environment env;

@Bean
public BlankDisc disc() {
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}

Spring的Environment:

getProperty()方法的四种重载方式:

  • String getProperty(String key);
  • String getProperty(String key, String defaultValue);
  • T getProperty(String key, Class type);
  • T getProperty(String key, Class type, T defaultValue);

使用重载形式的getProperty()方法可以避免类型转换:

1
int connectionCount = env.getProperty("db.connection.count", Integer.class, 10);

Environment常见方法:

  • boolean containsProperty(String property);
  • String[] getActiveProfiles();
  • String[] getDefaultProfiles();
  • boolean acceptsProfiles(String… profiles)。

解析属性占位符:

Spring支持将属性定义到外部的属性文件中,并使用占位符将其值插入到Spring bean中。在Spring装配中,占位符的形式为使用 “${…}” 的形式包装的属性名称。

1
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />

使用组件扫描和自动装配时:

1
2
3
4
public BlankDisc(@Value("${disc.title}" String title, @Value("${disc.artist}") String artist) {
this.title = title;
this.artist = artist;
}

SpEL表达式语言:

将表达式语言放到 “#{…}” 之中。

  • “#{1 + 1}”
  • “#{T(System).currentMillis()}”
  • “#{sgtPeppers.artist}”
  • “#{false}”
  • “#{artistSelector.selectArtists().toUpperCase()}”

SpEL运算符:

运算符类型 运算符
算术运算符 +、-、*、/、%、
比较运算符 <、>、==、<=、>=、lt、gt、eq、le、ge
逻辑运算符 and、or、not、|
条件运算符 ?:(ternary)、?:()
正则表达式 matches

计算正则表达式:

1
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}

计算集合:

1
2
3
4
5
#{jukebox.songs[4].title}
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
#{jukebox.songs.?[artist eq 'Aerosmith']} //.?[]得到集合的一个子集
#{jukebox.songs.^[artist eq 'Areosmith']} //.^[]查询集合中的第一个匹配项
#{jukebox.songs.$[artist eq 'Areosmith'].![title]} //.$[]查询集合中的最后一个匹配项,.![]从集合的每个成员中选择特定的属性放到另外一个集合中