Note/Spring实战/5

本文深入讲解Spring框架的高级配置技巧,包括使用Profile适应不同环境、条件化bean声明、解决自动装配歧义、bean作用域设置及运行时值注入等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇:Note4

前言:上一篇讲了 Spring 是如何装配 bean 的。在这一篇将会进一步的讲 Spring 装配 bean 的一些更高级的技巧,包括:

> Spring profile

> 条件化的 bean 声明

> 自动装配与歧义性

> bean 的作用域

> Spring 表达式语言

1. 条件化创建 bean 之 profile(开发环境与profile)

软件开发的时候,需要根据不同的环境(dev/qa/prod)来决定该使用哪种配置或环境相关的做法。

一种最笨的方法就是将不同的配置放在单独的配置类(或XML文件)中,然后在构建阶段(可能会使用Maven的profiles)确定要将哪一个配置编译到可部署的应用中。
这种方式的问题在于要为每种环境重新构建应用。当从dev环境迁移到qa环境时,重新构建没什么(就算出了bug也可以继续解决);然而当从qa环境迁移到prod环境时,重新构建并不能确保不会引入bug。

Spring提供了一种解决方案(profile)并不需要重新构建。
Spring提供的方案与构建时的方案差别不大,它需要根据环境来决定该创建哪个bean和不创建哪个bean。但它并不是在构建的时候做出的决策,而是在运行的时候自动确定。这样做的结果就是同一个部署单元(可能会是WAR文件)能够适用于所有的环境,没有必要重新构建。

Spring 3.1 引入了 bean profile 功能;要使用 profile,首先要指定某些 bean 属于哪个 profile(使用 @Profile 注解);然后在将应用部署到每个环境时,要确保对应的 profile 处于激活(active)状态。

1.1 在JavaConfig中配置profile

@Configuartion
@Profile("dev")    //可以同时用在多个环境上:@Profile(value={"dev","prod"})
public class DevConfig{
    @Bean
    public Resource source(){
        return new DevSource();
    }
}

在 Spring 3.1 中,只能在类级别上使用 @Profile 注解。而从 Spring 3.2 开始,可以在方法级别上使用 @Profile 注解。

@Configuartion
public class AllConfig{
    @Bean
    @Profile("dev")
    public Resource source(){
        return new DevSource();
    }

    @Bean
    @Profile("prod")
    public Resource source(){
        return new ProdSource();
    }
}

被声明在一个 profile 中的 bean(即,加了@Profile注解的bean),只有当它所指定的 profile 激活时,相应的 bean 才会被创建。而那些没有指定 profile 的 bean 始终都会被创建,与哪个 profile 激活与否没有关系。

1.2 在XML中配置profile

为每个环境单独创建一个 XML 配置,通过 <beans> 元素的 profile 属性,在 XML 中配置 profile 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:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="    
        http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context  
        http://www.springframework.org/schema/context/spring-context.xsd"
        profile="dev">

    <bean id="source" class="com.test.DevSource"/>
    
</beans>

所有环境的 XML 配置都会放到部署单元之中(如WAR文件),但只有 profile 属性与当前激活的 profile 相匹配的配置文件才会被用到。

还可以在 <beans> 中嵌套 <beans>,将所有环境的 profile bean 定义放在一个 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:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="    
        http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context  
        http://www.springframework.org/schema/context/spring-context.xsd">

    <beans profile="dev">
        <bean id="source" class="com.test.DevSource"/>
    </beans>

    <beans profile="prod">
        <bean id="source" class="com.test.ProdSource"/>
    </beans>
</beans>

这种配置方式和为环境单独进行XML配置效果是一样的,在运行的时候创建哪些 bean,取决于激活的是哪个 profile。

1.3 激活 profile

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

spring.profiles.default

spring.profiles.active

如果设置了 spring.profiles.active 则会按照该属性值来确定哪个 profile 是激活的,如果没有设置 spring.profiles.active 则会根据默认值 spring.profiles.active 来确定。如果两个都没有设置的话,那就没有激活的 profile,所以只会创建那些没有定义在 profile 中的 bean。

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

作为 DispatcherServlet 的初始化参数;

作为 Web 应用的上下文参数;

作为 JNDI 条目;

作为环境变量;

作为 JVM 的系统属性;

在集成测试类上,使用 @ActiveProfiles 注解设置。

1.3.1 作为 DispatcherServlet 的初始化参数

为 Servlet 设置默认的 profile

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" ...>
    <servlet>
        <servlet-name>ProfileServlet</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>

    <servlet-mapping>
        <servlet-name>ProfileServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

1.3.2 作为 Web 应用的上下文参数

为上下文设置默认 profile

//web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" ...>
	<context-param>
		<param-name>spring.profiles.default</param-name>
		<param-value>dev</param-value>
	</context-param>
</web-app>

1.3.3 在集成测试类上,使用 @ActiveProfiles 注解设置

JUnit测试中激活 profile

使用 @ActiveProfiles 注解激活 profile。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest{
	...
}

2. 条件化创建 bean 之 @Conditional 注解

Spring 4.0 提供了一种更为通用的机制来实现条件化的bean定义,即 @Conditional 注解。@Conditional 注解对条件的判断相对于 @profile 注解能够达到更细微的级别(毕竟 @profile 注解只是判断环境)。

@Conditional 注解可以用到带有 @Bean 注解的方法(JavaConfig中的方法)上。

2.1 示例

假设现在有这样一个要求:当存在环境变量 “water” 时,才创建 Fish 这个 bean。

带有 @Bean 注解的方法:

@Configuration
public class TestConfig{
    @Bean
    @Conditional(WaterExistsCondition.class)
    public Fish getFish(){
        return new Fish();
    }
}

条件类:

public class WaterExistsCondition implements Condition{
    public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata){
        Environment env = context.getEnvironment();
        return env.containsProperty("water");
    }
}

给定的条件计算结果(即 matches()方法的返回值)如果为 true,则会创建这个 bean,否则忽略这个 bean。

设置给 @Conditional 注解的类可以是任意 Conditional 接口的实现类。

//Conditional 接口
public interface Condition{
    boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata);
}

2.2 ConditionContext 和 AnnotatedTypeMetadata 接口

Condition 接口的 matches() 方法有两个参数,参数的类型分别是 ConditionContext 和 AnnotatedTypeMetadata。

这两个接口提供的方法所能实现的功能确定(限制)了 Condition 条件判断的范围。

2.2.1 ConditionContext接口

public interface ConditionContext{
	BeanDefinitionRegistry getRegistry();
	ConfigurableListableBeanFactory getBeanFactory();
	Environment getEnvironment();
	ResourceLoader getResourceLoader();
	ClassLoader getClassLoader();
}

(1)借助 getRegistry() 方法返回的 BeanDefinitionRegistry 实例可以用来检查 bean 的定义。
(2)借助 getBeanFactory() 方法返回的 BeanDefinitionRegistry 实例可以用来检查 bean 是否存在,甚至探查 bean 的属性。
(3)借助 getEnvironment() 方法返回的 Environment  实例可以用来检查环境变量是否存在,以及环境变量的值是什么。
(4)借助 getResourceLoader() 方法返回的 ResourceLoader 实例可以用来加载资源,加载到的资源可以被读取和探查。
(5)借助 getClassLoader() 方法返回的 ClassLoader 实例可以用来加载并检查类是否存在。

2.2.2 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);
}

(1)借助 isAnnotated(String annotationType) 方法我们能够判断带有 @Bean 注解的方法上是否还有其他特定的注解。

(2)借助其他4个方法,我们能够检查带有 @Bean 的方法上其他注解的属性。

2.3 Spring4 中 @Profile 注解的实现

既然讲到了 @Conditional 注解,就很有必了解了一下 Spring4 中的 @Profile 注解。在 Spring4 中,@Profile 注解使用了 @Conditional 注解和 Condition 接口进行了重构。

@Profile 注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile{
	String[] value();
}

ProfileCondition 条件类:

class ProfileCondition implements Condition{
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata){
		if(context.getEnvironment() != null){
			MultiValueMap<String,Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
			if(attrs != null){
				for(Object value : attrs.get("value")){
					if(context.getEnvironment().acceptsProfiles(((String[]) value))){
						return ture;
					}
				}
				return false;
			}
		}
		return true;
	}
}

启动 Spring 应用时,ConditionContext 对象和 AnnotatedTypeMetadata 对象的部分值:

ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。 借助该信息,它会明确地检查value属性,该属性包含了bean的profile名称。然后,它根据通过ConditionContext得到的Environment来检查[借助acceptsProfiles()方法]该profile是否处于激活状态。

3. 解决自动装配Bean时同时匹配到多个Bean的问题

在前面提到过,自动装配 Bean 时可以使用 @Autowired 注解自动去匹配 Spring 容器中的 Bean,@Autowired 注解默认按照类型去匹配,如果没有匹配到 Bean 或者匹配到多个 Bean 都会报错。

示例:

//自动装配Bean
@Autowired
private Animal animal;

//Dog.java
@Component
public class Dog implements Animal{...}

//Cat.java
@Component
public class Cat implements Animal{...}

那么如何解决匹配到多个 Bean 的问题呢?

3.1 方案一:标识首选的 bean(@Primary 注解)

//自动装配Bean
@Autowired
private Animal animal;

//Dog.java
@Component
@Primary
public class Dog implements Animal{...}

//Cat.java
@Component
public class Cat implements Animal{...}

在上述代码中,在Dog上添加了 @Primary 注解。这样,当匹配到多个 bean 时,会优先采用这个 bean。

【###】@Primary 注解也可以用在以 JavaConfig 方式声明 bean 的场景中:

@Configuration
public class AnimalConfig{
    @Bean
    @Primary
    public Dog dog(){
        return new Dog();
    }
}

【###】@Primary 注解也可以用在以 XML 配置方式声明 bean 的场景中:

<!-- 指定 primary 属性值为 true -->
<bean id="dog" class="com.animal.Dog" primary="true" />

3.2 方案二:使用限定符注解(@Quailfier)限定自动装配的 bean

方案一还不是最好的,因为当标识了多个相同的首选 bean 的时候,问题又回到了最初的状态。这时可以使用 @Quailfier 注解来限定自动装配的 bean。

@Quailfier 注解默认的限定符是 bean 的 ID。

@Autowired
@Quailfier("dog")
private Animal animal;

上述代码中,@Quailfier 注解中指定的 "dog" 就是 bean 的 ID。

【###】看似问题好像解决了,但可能会有意外的情况发生。

比如,当 bean 是使用 @Component 注解声明的,且默认以类名(首字母小写)作为 bean 的 ID。此时一些正常。

@Component
public class Dog implements Animal{...}

但某个时候,类名因为某些原因重构了:

@Component
public class MyDog implements Animal{...}

那么继续使用方案二最初的代码是无法匹配到原先那个 bean 的。那么,怎么解决这个问题呢?

方法一:在声明 bean 的时候指定 bean 的 ID

@Component("dog")
public class Dog implements Animal{...}

这样,就算改变了类名也不会使 bean 的 ID 发生改变。

方法二:声明 bean 时创建自定义的限定符

@Component
@Quailfier("dog")    //自定义的限定符
public class Dog implements Animal{...}

@Quailfier 注解也可以用在以 JavaConfig 方式声明 bean 的场景中:

@Configuration
public class AnimalConfig{
    @Bean
    @Quailfier("dog")
    public Dog dog(){
        return new Dog();
    }
}

这个自定义的限定符并不是与类名耦合在一起的,所以重构类名也不会有影响。

3.3 自定义限定符注解

如果使用上述方法二来解决问题,有可能会再次出现之前那种无语的情况。那就是自定义了多个相同的限定符。

然后可能你会为了解决这种问题再套上一个 @Quailfier 注解:

//错误方案
@Component
@Quailfier("my")
@Quailfier("dog")
public class MyDog implements Animal{...}

@Component
@Quailfier("other")
@Quailfier("dog")
public class OtherDog implements Animal{...}

@Autowired
@Quailfier("my")
@Quailfier("dog")
private Animal animal;

注意:这个问题是建立在使用了上面的方法二来解决问题,并且重构了类名的基础上的。不能想当然的认为可以直接使用@Quailfier("myDog") 来代替原来的 @Quailfier("dog"),这样无法保证代码的兼容性。

可以确定,上述方案是不成立的。因为在 Java 中不允许在同一个条目上重复出现相同类型的多个注解,这时候可以考虑自定义限定符注解。【声明】Java 8允许出现重复的注解,只要这个注解本身在定义的时候带有 @Repeatable 注解就可以。不过,Spring的@Qualifier 注解并没有在定义时添加 @Repeatable 注解。

自定义限定符注解:

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Quailfier
public @interface Dog{ }

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Quailfier
public @interface My{ }

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Quailfier
public @interface Other{ }

使用自定义限定符注解:

@Component
@My
@Dog
public class Dog implements Animal{...}

@Autowired
@My
@Dog
private Animal animal;

4. bean 的作用域

默认情况下,Spring 上下文中所有的 bean 都是以单例(singleton)的形式创建的。即,不管一个 bean 被注入到其他 bean 中多少次,每次注入的都是同一个实例。

但有些时候,你可能并不想要以单例的形式创建 bean。所以,Spring定义了多种作用域,可以基于这些作用域创建bean:

单例(Singleton) : 在整个应用中, 只创建bean的一个实例。

原型(Prototype) : 每次注入或者通过Spring应用上下文获取的时候, 都会创建一个新的bean实例。

会话(Session) : 在Web应用中, 为每个会话创建一个bean实例。

请求(Rquest) : 在Web应用中, 为每个请求创建一个bean实例。

4.1 使用 @Scope 注解来声明 bean 的作用域

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)	//或 @Scope("prototype")
public class Dog{...}

在 JavaConfig 中使用 @Scope 注解: 

@Configuration
public class AnimalConfig{	
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)	//或 @Scope("prototype")
    public Dog dog(){
        return new Dog();
    }
}

在 XML 配置使用 scope:

<bean id="dog" class="com.animal.Dog" scope="prototype"/>

指定代理模式(在下面4.2),使用 Spring aop 命名空间的一个新元素:<aop:scoped-proxy>

<?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:aop="http://www.springframework.org/schema/aop"  
    xsi:schemaLocation="    
        http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 默认情况,使用 CGLib 创建目标类的代理 -->
    <bean id="car" class="com.test.ShoppingCart" scope="session"/>
        <aop:scoped-proxy />
    </bean>

    <!-- 如果要生成基于接口的代理,将 proxy-target-class 属性设置为 false -->
    <bean id="car" class="com.test.ShoppingCart" scope="session"/>
        <aop:scoped-proxy proxy-target-class="false" />        
    </bean>
</beans>

4.2 会话和请求作用域使用场景示例

在电子商务应用中,购物车如果是原型作用域,那么在应用中某个地方往购物车中添加商品,在另一个地方可能就是另一个新的购物车了。所以,就购物车场景来说,会话作用域是最合适的。购物车使用会话作用域后,它将与用户进行关联。

指定会话作用域:

@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.INTERFACES)
public interface ShoppingCart{...}

这会告诉 Spring 为 Web 应用中的每个会话创建一个 ShoppingCart。

【###】@Scope 同时还有一个 proxyMode 属性, 它被设置成了 ScopedProxyMode.INTERFACES。

这个属性是干嘛的呢?因为当将会话或请求作用域的 bean 注入到单例 bean 中会有两个问题

问题1:当要将会话对象注入到单例的 bean(Spring默认创建的就是单例的 bean)中时,因为单例的 bean 是在 Spring 启动的时候创建的(在前);而会话对象是直到用户进入系统,创建了会话之后,才会创建会话实例(在后);所以在在创建单例 bean 的时候是无法注入会话对象的。

问题2:系统中将会有多个会话对象(每个用户对应一个),在将会话对象注入到单例 bean 中时,我们希望的注入的会话对象是当前登录用户所对应的那个。

如何解决?

代理模式:Spring会生成一个会话对象的代理类,这个代理类会暴露与会话对象相同的方法,在 Spring 启动创建单例 bean 时将这个代理类对象注入进去,但是当单例 bean 调用会话对象的某个方法时,该代理类会将方法的调用委托给真正的会话对象方法。


【###】两种生成代理类的方式

在上述例子中,如果 ShoppingCart  是接口的话,则 proxyMode 属性值是 ScopedProxyMode.INTERFACES。如果 ShoppingCart 是一个具体的类,Spring 就无法创建基于接口的代理了;这时,需要使用 CGLib 来生成基于类的代理;此时 proxyMode 属性值应该指定为 ScopedProxyMode.TARGET_CLASS,表明要以生成目标类扩展的方式创建代理。

@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.INTERFACES)
public class ShoppingCart{...}

5. 运行时的值注入(属性占位符 和 Spring表达式语言)

当讨论依赖注入的时候, 我们通常所讨论的是将一个bean引用注入到另一个bean的属性或构造器参数中。 它通常来讲指的是将一个对象与另一个对象进行关联。但是bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。

在 Note4 的 3.2.2 和 3.3.2 中演示了字面量的装配方法,尽管实现了这个需求,但字面量被硬编码了。我们有时想要避免使用这种硬编码方式,而是想让这些值在运行时再确定。

为了实现这些功能, Spring提供了两种在运行时求值的方式:

> 属性占位符(Property placeholder) 

> Spring表达式语言(SpEL) 

5.1 方式1:使用 Spring 的 Enviroment 检索属性

先声明属性源再通过 Spring 的 Environment 来检索属性。

【示例】

定义属性文件(属性源):

-- /com/test/test.properties
test.title=This is a test
test.content=This is a content

使用 Spring 的 Enviroment 检索属性:

@Configuration
@PropertySource("classpath:/com/test/test.properties")
public class TestConfig{
    @Autowired
    Environment env;

    @Bean
    public Test test(){
        return new Test(env.getProperty("test.title"),env.getProperty("test.content"));
    }
}

【详解 Environment】

Environment 接口源码:

package org.springframework.core.env;
public interface PropertyResolver {
    //检查某个属性是否存在
    boolean containsProperty(String key);

    //通过key获取属性值,没有获取到返回null
    String getProperty(String key);
    //通过key获取属性值,如果没获取到,返回一个默认值
    String getProperty(String key, String defaultValue);

    //通过key获取属性值,并将值转换为一个指定的类型后再返回,没有获取到返回null
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);	

    //属性解析为类,例如属性值为:com.test.Test,则解析为Test类
    @Deprecated
    <T> Class<T> getPropertyAsClass(String key, Class<T> targetType);

    //通过key获取属性值,没有获取到抛出IllegalStateException异常
    String getRequiredProperty(String key) throws IllegalStateException;	
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    String resolvePlaceholders(String text);	
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

package org.springframework.core.env;
public interface Environment extends PropertyResolver {
    //返回激活profile名称的数组
    String[] getActiveProfiles();
    //返回默认profile名称的数组
    String[] getDefaultProfiles();
    /**如果environment支持给定profile的话, 就返回true。
       在bean创建之前, 使用acceptsProfiles()方法来确保
       给定bean所需的profile处于激活状态。
    **/
    boolean acceptsProfiles(String... profiles);
}

5.2 方式二:使用属性占位符

占位符的形式为使用 “${... }” 包装的属性名称。

<bean id="cat" class="com.test.Cat">
    <constructor-arg value="${cat.name}"/>
</bean>

如果是使用依赖于组件扫描和自动装配来创建和初始化应用组件的话,可以使用@Value注解,它的使用方式与@Autowired注解非常相似。

@Component
public class Cat{
    private String name;

    public Cat(@Value("${cat.name}") String name){
        this.name = name;
    }
    ...
}

【###】配置 PropertyPlaceholderConfigurer

为了使用占位符, 我们必须要配置一个PropertyPlaceholderConfigurer bean 或 PropertySourcesPlaceholderConfigurer bean。 从Spring 3.1开始, 推荐使用 PropertySourcesPlaceholderConfigurer, 因为它能够基于 Spring Environment 及其属性源来解析占位符。

JavaConfig 形式的配置:

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
	return new PropertySourcesPlaceholderConfigurer();
}

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:aop="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="    
        http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/aop/spring-context.xsd">
	
	<context:property-placeholder />
</beans>

<context:propertyplaceholder> 元素是 Spring context 命名空间中的。

5.3 方式三:使用Spring表达式语言

Spring 3 引入了Spring表达式语言(Spring Expression Language,SpEL) , 它能够以一种强大和简洁的方式将值装配到 bean属性和构造器参数中, 在这个过程中所使用的表达式会在运行时计算得到值。

SpEL拥有很多特性, 包括:

> 使用bean的ID来引用bean

> 调用方法和访问对象的属性

> 对值进行算术、关系和逻辑运算

> 正则表达式匹配

> 集合操作

5.3.1 SpEL 语法规则和示例

【语法规则】

SpEL表达式要放到 “#{ ... }” 之中。

【示例】

(1)引用字面量

可以包括:整数、浮点数、String值、Boolean值,数值还可以使用科学计数法进行表示。

//整数
#{1}

//浮点数
#{3.14159}

//科学计数法,98700
#{9.87E4}

//String
#{'Hello'}

//Boolean
#{false}

可能你会觉得在 SpEL 中使用字面量没意义(还不如直接写字面量呢)。但是,在 SpEL 中使用字面量的通常是为了与其他简单表达式组合成更复杂的表达式。

(2)引用 bean 、属性和方法

//引用ID为cat的bean
#{cat}

//引用cat的name属性
#{cat.name}

//引用cat的miao()方法
#{cat.miao()}

//方法链
#{cat.miao().toUpperCase()}

//避免空指针异常,如果miao()方法返回值为null的话,不再调用toUpperCase()方法,表达式返回值为null
#{cat.miao()?.toUpperCase()}

(3)调用类的静态方法和常量

如果要在SpEL中访问类作用域的方法和常量的话, 要依赖T()这个关键的运算符。T() 运算符的结果会是一个 Class 对象。

【注意】T()运算符要用在 SpEL 表达式中才有效。

#{T(java.lang.Math).PI}

#{T(java.lang.Math).random()}

(4)引用系统属性

//通过 systemProperties 对象引用系统属性
#{systemProperties['test.title']}

5.3.2 在bean装配的时候使用这些表达式

如果是通过组件扫描创建 bean 的话, 在注入属性和构造器参数时, 我们可以使用 @Value 注解, 这与之前看到的属性占位符非常类似。 不过, 在这里我们所使用的不是占位符表达式, 而是SpEL表达式。

public class Cat{
    private String name;

    public Cat(@Value("#{systemProperties['cat.name']}") String name){
        this.name = name;
    }
}

在 XML 配置中, 你可以将 SpEL 表达式传入 <property> 或 <constructor-arg> 的 value 属性中, 或者将其作为 p- 命名空间或 c- 命名空间条目的值。

<bean id="cat" class="com.test.Cat" c:_name="#{systemProperties['cat.name']}" />

5.3.3 SpEL运算符

SpEL提供了多个运算符, 这些运算符可以用在SpEL表达式的值上。

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

【注意】

当使用String类型的值时, “+”运算符执行的是连接操作, 与在Java中是一样的。

【示例】
(1)计算circle bean中所定义圆的周长(2πR)

#{2 * T(java.lang.Math).PI * circle.radius}

(2)计算circle bean中所定义圆的面积(πR²)

#{T(java.lang.Math).PI * circle.radius ^ 2}

(3)String类型的值拼接

#{'my ' +  cat.name}

(4)比较运算符有两种形式: 符号形式和文本形式(等效的)

//符号形式
#{cat.age == 2}

//文本形式
#{cat.age eq 2}

(5)条件运算符

//三元运算符(ternary),与 Java 中的三元运算符类似
#{cat.age > 3 ? "big cat" : "litter cat"}

//Elvis运算符,通常用来检查null值, 并用一个默认值来替代null
//注意:? 和 : 之间没有其他内容
#{cat.name ?: 'hello kitty'}

(6)计算正则表达式

matches运算符对 String 类型的文本(作为左边参数) 应用正则表达式(作为右边参数) 。 matches 的运算结果会返回一个Boolean类型的值,如果与正则表达式相匹配, 则返回true, 否则返回false。

//简单的验证邮箱地址的有效性
#{cat.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}

(7)计算集合

> 获取指定索引位置的集合元素([ ] 运算符):

#{cat.songs[3]}    //索引从 0 开始

> 从String中获取一个字符([ ] 运算符):

#{'this is a cat'[1]}    //表达式值为:h

> 对集合进行过滤,得到集合的一个子集(.?[ ] 运算符):

#{cat.songs.?[author eq 'kitty']}

把这只小猫收藏的所有歌曲中作者是 "kitty" 的歌曲放到一个新的集合中(author是歌曲的属性)。

> SpEL 还提供了另外两个查询运算符: “.^[ ]” 和 “.$[ ]”, 它们分别用来在集合中查询第一个匹配项和最后一个匹配项。

>投影运算符(.![ ])

它会从集合的每个成员中选择特定的属性放到另外一个集合中。

#{cat.songs.![title]}

比如, 假设我们不想要歌曲对象的集合, 而是所有歌曲名称的集合。 上面的表达式会将 title 属性投影到一个新的 String 类型的集合中。

5.3.4 建议

在动态注入值到 Spring bean 时, SpEL 是一种很便利和强大的方式。 我们有时会忍不住编写很复杂的表达式。但是 SpEL 毕竟只是 String 类型的值, 可能测试起来很困难。 鉴于这一点,建议尽可能让表达式保持简洁,这样测试不会是什么大问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值