第三章 高级装配
* Spring profile
* 条件化的bean声明
* 自动装配与歧义性
* bean的作用域
* Spring表达式语言
配置profile bean
在3.1版本中, Spring引入了bean profile的功能。 要使用profile, 你首先要将所有不同的bean定义整理到一个或多个profile之中, 在将应用部署
到每个环境时, 要确保对应的profile处于激活(active) 的状态。
在Java配置中, 可以使用@Profile注解指定某个bean属于哪一个profile。 例如, 在配置类中, 嵌入式数据库的DataSource可能会配置成如
下所示
package com.myapp; import javax.activation.DataSource; 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; @Configuration @Profile("dev") public class DevelopmentProfileConfig { @Bean(destroyMethod = "shutdown") public DataSource dataSource() { return (DataSource) new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } }
package com.myapp; import javax.activation.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.jndi.JndiObjectFactoryBean; @Configuration @Profile("prod") public class ProductionProfileConfig { @Bean public DataSource dataSource() { JndiObjectFactoryBean jndiObjectFactory = new JndiObjectFactoryBean(); jndiObjectFactory.setJndiName("jdbc/myDS"); jndiObjectFactory.setResourceRef(true); jndiObjectFactory.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactory.getObject(); } }
在Spring 3.1中, 只能在类级别上使用@Profile注解。 不过, 从Spring 3.2开始, 你也可以在方法级别上使用@Profile注解, 与@Bean注解一同使用。 这样的话, 就能将这两个bean的声明放到同一个配置类之中, 如下所示:
@Profile注解基于激活的profile实现bean的装配
package com.myapp; import javax.activation.DataSource; 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; @Configuration public class DatasourceConfig { @Bean(destroyMethod = "shutdown") @Profile("dev") public DataSource embbededDataSource() { return (DataSource) new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } @Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactory = new JndiObjectFactoryBean(); jndiObjectFactory.setJndiName("jdbc/myDS"); jndiObjectFactory.setResourceRef(true); jndiObjectFactory.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactory.getObject(); } }
在XML中配置profile
我们也可以通过<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: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>
重复使用元素来指定多个profile
<?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/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 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="prod"> <jee:jndi-lookup id="dataSource" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="java.sql.DataSource" /> </beans> </beans>
激活profile
Spring在确定哪个profile处于激活状态时, 需要依赖两个独立的属性: spring.profiles.active和spring.profiles.default。 如果设置了spring.profiles.active属性的话,
那么它的值就会用来确定哪个profile是激活的。 但如果没有设置spring.profiles.active属性的话, 那Spring将会查找spring.profiles.default的值。 如果spring.profiles.active和spring.profiles.default均没有设置的话, 那就没有激活的profile, 因此只会创建那些没有定义在
profile中的bean。
有多种方式来设置这两个属性:
作为DispatcherServlet的初始化参数;
作为Web应用的上下文参数;
作为JNDI条目;
作为环境变量;
作为JVM的系统属性;
在集成测试类上, 使用@ActiveProfiles注解设置。
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" > <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/root-context.xml</param-value> </context-param> <!-- 为上下文设置默认的profile --> <context-param> <param-name>spring.profiles.default</param-name> <param-value>dev</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <!-- 为Servlet设置默认的profile --> <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> </web-app>
Spring提供了@ActiveProfiles注解, 我们可以使用它来指定运行测试时要激活哪个profile。
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = PersistenceTestConfig.class) @ActiveProfiles("dev") public class DatasourceTest { ... }
条件化的bean
package com.magic; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class magicConfigure { @Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); } }
可以看到, @Conditional中给定了一个Class, 它指明了条件——在本例中, 也就是MagicExistsCondition。 @Conditional将会
通过Condition接口进行条件对比:
public interface Condition { /** * Determine if the condition matches. * @param context the condition context * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class} * or {@link org.springframework.core.type.MethodMetadata method} being checked. * @return {@code true} if the condition matches and the component can be registered * or {@code false} to veto registration. */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
设置给@Conditional的类可以是任意实现了Condition接口的类型。 可以看出来, 这个接口实现起来很简单直接, 只需提
供matches()方法的实现即可。 如果matches()方法返回true, 那么就会创建带有@Conditional注解的bean。 如果matches()方法返
回false, 将不会创建这些bean。
在本例中, 我们需要创建Condition的实现并根据环境中是否存在magic属性来做出决策。 程序清单3.5展现
了MagicExistsCondition, 这是完成该功能的Condition实现类:
package com.magic; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; public class MagicExistsCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
MagicExistsCondition中只是使用了ConditionContext得到的Environment, 但Condition实现的考量因素可能会比这更
多。 matches()方法会得到ConditionContext和AnnotatedTypeMetadata对象用来做出决策。
ConditionContext是一个接口, 大致如下所示 :
public interface ConditionContext { BeanDefinitionRegistry getRegistry(); ConfigurableListableBeanFactory getBeanFactory(); Environment getEnvironment(); ResourceLoader getResourceLoader(); ClassLoader getClassLoader(); }
通过ConditionContext, 我们可以做到如下几点:
借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在, 甚至探查bean的属性;
借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。 像ConditionContext一
样, AnnotatedTypeMetadata也是一个接口。 它如下所示 :
package org.springframework.core.type; import java.util.Map; import org.springframework.util.MultiValueMap; public interface AnnotatedTypeMetadata { boolean isAnnotated(String annotationName); Map<String, Object> getAnnotationAttributes(String annotationName); Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName); MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString); }
借助isAnnotated()方法, 我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。 借助其他的那些方法, 我们能够检
查@Bean注解的方法上其他注解的属性。
非常有意思的是, 从Spring 4开始, @Profile注解进行了重构, 使其基于@Conditional和Condition实现。 作为如何使
用@Conditional和Condition的例子, 我们来看一下在Spring 4中, @Profile是如何实现的。
@Profile注解如下所示:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); }
注意:@Profile本身也使用了@Conditional注解, 并且引用ProfileCondition作为Condition实现。 如下所示, ProfileCondition实现了Condition接口, 并且在做出决策的过程中, 考虑到
了ConditionContext和AnnotatedTypeMetadata中的多个因素。
package org.springframework.context.annotation; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.MultiValueMap; class ProfileCondition implements Condition { @Override 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 true; } } return false; } } return true; } }
我们可以看到, ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。 借助该信息, 它会明确地检
查value属性, 该属性包含了bean的profile名称。 然后, 它根据通过ConditionContext得到的Environment来检查[借
助acceptsProfiles()方法] 该profile是否处于激活状态。
处理自动装配的歧义性
为了阐述自动装配的歧义性, 假设我们使用@Autowired注解标注了setDessert()方法 ;
@Autowired public void setDessert(Dessert dessert) { this.dessert = dessert; }
在本例中, Dessert是一个接口, 并且有三个类实现了这个接口, 分别为Cake、 Cookies和IceCream:
@Component public class Cake implements Dessert @Component public class Cookies implements Dessert @Component public class IceCream implements Dessert
因为这三个实现均使用了@Component注解, 在组件扫描的时候, 能够发现它们并将其创建为Spring应用上下文里面的bean。 然后, 当Spring
试图自动装配setDessert()中的Dessert参数时, 它并没有唯一、 无歧义的可选值。 在从多种甜点中做出选择时, 尽管大多数人并不会有
什么困难, 但是Spring却无法做出选择。 Spring此时别无他法, 只好宣告失败并抛出异常。 更精确地讲, Spring会抛
出NoUniqueBeanDefinitionException:
当确实发生歧义性的时候, Spring提供了多种可选方案来解决这样的问题。 你可以将可选bean中的某一个设为首选(primary) 的
bean, 或者使用限定符(qualifier) 来帮助Spring将可选的bean的范围缩小到只有一个
标示首选的bean
通过将其中一个可选的bean设置为首选(primary) bean能够避免自动装配时的歧义性。 当遇到歧义性的时候, Spring将
会使用首选的bean, 而不是其他可选的bean。
@Component @Primary public class IceCream implements Dessert
如果你使用XML配置bean的话, 同样可以实现这样的功能。 <bean>元素有一个primary属性用来指定首选的bean:
<bean id="iceCream" class="com.condition.IceCream" primary="true"/>
@Component @Primary public class Cake implements Dessert
现在, 有两个首选的Dessert bean: Cake和IceCream。 这带来了新的歧义性问题。 就像Spring无法从多个可选的bean中做出选择一样,
它也无法从多个首选的bean中做出选择。 显然, 如果不止一个bean被设置成了首选bean, 那实际上也就是没有首选bean了。
就解决歧义性问题而言, 限定符是一种更为强大的机制, 下面就将对其进行介绍。
限定自动装配的bean
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。 它只能标示一个优先的可选方案。 当首选
bean的数量超过一个时, 我们并没有其他的方法进一步缩小可选范围。
@Qualifier注解是使用限定符的主要方式。 它可以与@Autowired和@Inject协同使用, 在注入的时候指定想要注入进去的是哪个bean。
例如, 我们想要确保要将IceCream注入到setDessert()之中:
@Autowired @Qualifier("iceCream") public void setDessert(Dessert dessert) { this.dessert = dessert; }
实际上, 还有一点需要补充一下。 更准确地讲, @Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。 如
果没有指定其他的限定符的话, 所有的bean都会给定一个默认的限定符, 这个限定符与bean的ID相同。 因此, 框架会将具有“iceCream”限定符
的bean注入到setDessert()方法中。 这恰巧就是ID为iceCream的bean, 它是IceCream类在组件扫描的时候创建的。
基于默认的bean ID作为限定符是非常简单的, 但这有可能会引入一些问题。 如果你重构了IceCream类, 将其重命名为Gelato的话, 那此时会
发生什么情况呢? 如果这样的话, bean的ID和默认的限定符会变为gelato, 这就无法匹配setDessert()方法中的限定符。 自动装配会失
败。
创建自定义的限定符
我们可以为bean设置自己的限定符, 而不是依赖于将bean ID作为限定符。 在这里所需要做的就是在bean声明上添加@Qualifier注解。 例
如, 它可以与@Component组合使用, 如下所示:
@Component @Qualifier("cold") public class IceCream implements Dessert
在这种情况下, cold限定符分配给了IceCreambean。 因为它没有耦合类名, 因此你可以随意重构IceCream的类名, 而不必担心会破坏自动
装配。 在注入的地方, 只要引用cold限定符就可以了:
@Autowired @Qualifier("cold") public void setDessert(Dessert dessert) { this.dessert = dessert; }
可能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解。 Cookies类大致就会如下所示:
@Component @Qualifier("cold") @Qualifier("fruity") public class Cookies implements Dessert
IceCream类同样也可能再添加另外一个@Qualifier注解:
@Component @Qualifier("cold") @Qualifier("creamy") public class IceCream implements Dessert
@Autowired @Qualifier("cold") @Qualifier("creamy") public void setDessert(Dessert dessert) { this.dessert = dessert; }
这里只有一个小问题: Java不允许在同一个条目上重复出现相同类型的多个注解。 [1]如果你试图这样做的话, 编译器会提示错误。 在这里, 使
用@Qualifier注解并没有办法(至少没有直接的办法) 将自动装配的可选bean缩小范围至仅有一个可选的bean。
但是, 我们可以创建自定义的限定符注解, 借助这样的注解来表达bean所希望限定的特性。 这里所需要做的就是创建一个注解, 它本身要使
用@Qualifier注解来标注。 这样我们将不再使用@Qualifier("cold"), 而是使用自定义的@Cold注解, 该注解的定义如下所示:
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Cold {}
同样, 你可以创建一个新的@Creamy注解来代替@Qualifier("creamy"):
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Creamy {}
当你不想用@Qualifier注解的时候, 可以类似地创建@Soft、 @Crispy和@Fruity。 通过在定义时添加@Qualifier注解, 它们就具有
了@Qualifier注解的特性。 它们本身实际上就成为了限定符注解。
现在, 我们可以重新看一下IceCream, 并为其添加@Cold和@Creamy注解, 如下所示:
@Component @Cold @Creamy public class Cookies implements Dessert
bean的作用域
Spring定义了多种作用域, 可以基于这些作用域创建bean, 包括:
单例(Singleton) : 在整个应用中, 只创建bean的一个实例。
原型(Prototype) : 每次注入或者通过Spring应用上下文获取的时候, 都会创建一个新的bean实例。
会话(Session) : 在Web应用中, 为每个会话创建一个bean实例。
请求(Rquest) : 在Web应用中, 为每个请求创建一个bean实例 。
单例是默认的作用域, 但是正如之前所述, 对于易变的类型, 这并不合适。 如果选择其他的作用域, 要使用@Scope注解, 它可以
与@Component或@Bean一起使用。
例如, 如果你使用组件扫描来发现和声明bean, 那么你可以在bean的类上使用@Scope注解, 将其声明为原型bean:
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class Notepad {
这里, 使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。 你当然也可以使
用@Scope("prototype"), 但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
如果你想在Java配置中将Notepad声明为原型bean, 那么可以组合使用@Scope和@Bean来指定所需的作用域:
@Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Notepad notePad() { return new Notepad(); }
同样, 如果你使用XML来配置bean的话, 可以使用<bean>元素的scope属性来设置作用域:
<bean id="notepad" class="com.scope.Notepad" scope="prototype"/>
不管你使用哪种方式来声明原型作用域, 每次注入或从Spring应用上下文中检索该bean的时候, 都会创建新的实例。 这样所导致的结果就是每
次操作都能得到自己的Notepad实例。
使用会话和请求作用域
在Web应用中, 如果能够实例化在会话和请求范围内共享的bean, 那将是非常有价值的事情。 例如, 在典型的电子商务应用中, 可能会有一个
bean代表用户的购物车。 如果购物车是单例的话, 那么将会导致所有的用户都会向同一个购物车中添加商品。 另一方面, 如果购物车是原型作
用域的, 那么在应用中某一个地方往购物车中添加商品, 在应用的另外一个地方可能就不可用了, 因为在这里注入的是另外一个原型作用域的
购物车。
就购物车bean来说, 会话作用域是最为合适的, 因为它与给定的用户关联性最大。 要指定会话作用域, 我们可以使用@Scope注解, 它的使用
方式与指定原型作用域是相同的:
@Component @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.INTERFACES) public class ShoppingCart {...}
这里, 我们将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session) 。 这会告诉Spring为Web应用
中的每个会话创建一个ShoppingCart。 这会创建多个ShoppingCart bean的实例, 但是对于给定的会话只会创建一个实例, 在当前会话
相关的操作中, 这个bean实际上相当于单例的。
要注意的是, @Scope同时还有一个proxyMode属性, 它被设置成了ScopedProxyMode.INTERFACES。 这个属性解决了将会话或请求作
用域的bean注入到单例bean中所遇到的问题。 在描述proxyMode属性之前, 我们先来看一下proxyMode所解决问题的场景。
假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中, 如下所示:
@Component public class StoreService {
@Autowired public void setShoppingCart(ShoppingCart shoppingCart) { this.shoppingCart = shoppingCart; } ... }
因为StoreService是一个单例的bean, 会在Spring应用上下文加载的时候创建。 当它创建的时候, Spring会试图将ShoppingCart bean
注入到setShoppingCart()方法中。 但是ShoppingCart bean是会话作用域的, 此时并不存在。 直到某个用户进入系统, 创建了会话之
后, 才会出现ShoppingCart实例。
另外, 系统中将会有多个ShoppingCart实例: 每个用户一个。 我们并不想让Spring注入某个固定的ShoppingCart实例
到StoreService中。 我们希望的是当StoreService处理购物车功能时, 它所使用的ShoppingCart实例恰好是当前会话所对应的那一
个。
Spring并不会将实际的ShoppingCart bean注入到StoreService中, Spring会注入一个到ShoppingCart bean的代理, 如图3.1所示。 这个
代理会暴露与ShoppingCart相同的方法, 所以StoreService会认为它就是一个购物车。 但是, 当StoreService调
用ShoppingCart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
现在, 我们带着对这个作用域的理解, 讨论一下proxyMode属性。 如配置所示, proxyMode属性被设置成
了ScopedProxyMode.INTERFACES, 这表明这个代理要实现ShoppingCart接口, 并将调用委托给实现bean。
如果ShoppingCart是接口而不是类的话, 这是可以的(也是最为理想的代理模式) 。 但如果ShoppingCart是一个具体的类的话, Spring
就没有办法创建基于接口的代理了。 此时, 它必须使用CGLib来生成基于类的代理。 所以, 如果bean类型是具体类的话, 我们必须要
将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS, 以此来表明要以生成目标类扩展的方式创建代理。
尽管我主要关注了会话作用域, 但是请求作用域的bean会面临相同的装配问题。 因此, 请求作用域的bean应该也以作用域代理的方式进行注
入 。
在XML中声明作用域代理
如果你需要使用XML来声明会话或请求作用域的bean, 那么就不能使用@Scope注解及其proxyMode属性了。 <bean>元素的scope属性能够
设置bean的作用域, 但是该怎样指定代理模式呢?
要设置代理模式, 我们需要使用Spring aop命名空间的一个新元素:
<bean id="notepad" class="com.scope.session.ShoppingCart" scope="session"> <aop:scoped-proxy/> </bean>
<aop:scoped-proxy>是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。 它会告诉Spring为bean创建一个作用域代
理。 默认情况下, 它会使用CGLib创建目标类的代理。 但是我们也可以将proxy-target-class属性设置为false, 进而要求它生成基于接
口的代理:
<bean id="notepad" class="com.scope.session.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class="false"/> </bean>
运行时值注入
Spring提供了两种在运行时求值的方式:
属性占位符(Property placeholder)。
Spring表达式语言(SpEL)。
注入外部的值
在Spring中, 处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。 例如, 下面程序清单展现了一个基本的
Spring配置类, 它使用外部的属性来装配BlankDisc bean。
package soundsystem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.env.Environment; @Configuration @PropertySource("classpath:/com/soundsystem/app.properties") public class ExpressiveCongig { @Autowired private Environment env; @Bean public BlankDisc disc() { return new BlankDisc( env.getProperty("disc.title"), env.getProperty("disc.artist")); } }
深入学习Spring的Environment
当我们去了解Environment的时候会发现, 程序清单3.7所示的getProperty()方法并不是获取属性值的唯一方法, getProperty()方
法有四个重载的变种形式:
String getProperty(String key); String getProperty(String key, String defaultValue); <T> T getProperty(String key, Class<T> targetType); <T> T getProperty(String key, Class<T> targetType, T defaultValue);
前两种形式的getProperty()方法都会返回String类型的值。
剩下的两种getProperty()方法与前面的两种非常类似, 但是它们不会将所有的值都视为String类型。
int connectionCount = env.getProperty("db.connection.count", Integer.class, 10);
Environment还提供了几个与属性相关的方法, 如果你在使用getProperty()方法的时候没有指定默认值, 并且这个属性没有定义的话,
获取到的值是null。 如果你希望这个属性必须要定义, 那么可以使用getRequiredProperty()方法, 如下所示:
@Bean public BlankDisc disc() { return new BlankDisc( env.getRequiredProperty("disc.title"), env.getRequiredProperty("disc.artist")); }
在这里, 如果disc.title或disc.artist属性没有定义的话, 将会抛出IllegalStateException异常。
如果想检查一下某个属性是否存在的话, 那么可以调用Environment的containsProperty()方法:
boolean titleExists = env.containsProperty("disc.title");
最后, 如果想将属性解析为类的话, 可以使用getPropertyAsClass()方法:
Class<CompactDisc> adClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
除了属性相关的功能以外, Environment还提供了一些方法来检查哪些profile处于激活状态:
String[] getActiveProfiles(): 返回激活profile名称的数组;
String[] getDefaultProfiles(): 返回默认profile名称的数组;
boolean acceptsProfiles(String... profiles): 如果environment支持给定profile的话, 就返回true。
解析属性占位符
Spring一直支持将属性定义到外部的属性的文件中, 并使用占位符值将其插入到Spring bean中。 在Spring装配中, 占位符的形式为使用“${
... }”包装的属性名称。 作为样例, 我们可以在XML中按照如下的方式解析BlankDisc构造器参数:
<bean id="sgPeppers" class="soundsystem.SgtPeppers" c:title="${disc.title}" c:artist="${disc.artist}" /> </bean>
可以看到, title构造器参数所给定的值是从一个属性中解析得到的, 这个属性的名称为disc.title。 artist参数装配的是名为
disc.artist的属性值。 按照这种方式, XML配置没有使用任何硬编码的值, 它的值是从配置文件以外的一个源中解析得到的。 (我们稍后
会讨论这些属性是如何解析的。 )
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话, 那么就没有指定占位符的配置文件或类了。 在这种情况下, 我们可以使
用@Value注解, 它的使用方式与@Autowired注解非常相似。 比如, 在BlankDisc类中, 构造器可以改成如下所示:
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist) { this.title = title; this.artist = artist; }
为了使用占位符, 我们必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer
bean。 从Spring 3.1开始, 推荐使用PropertySourcesPlaceholderConfigurer, 因为它能够基于Spring Environment及其属性源来
解析占位符。
如下的@Bean方法在Java中配置了PropertySourcesPlaceholderConfigurer:
@Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigure() { return new PropertySourcesPlaceholderConfigurer(); }
如果你想使用XML配置的话, Spring context命名空间中的<context:propertyplaceholder>元素将会为你生
成PropertySourcesPlaceholderConfigurer bean:
<context:property-placeholder/>
解析外部属性能够将值的处理推迟到运行时, 但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。 而Spring表达
式语言提供了一种更通用的方式在运行时计算所要注入的值。
使用Spring表达式语言进行装配
SpEL拥有很多特性, 包括:
使用bean的ID来引用bean;
调用方法和访问对象的属性;
对值进行算术、 关系和逻辑运算;
正则表达式匹配;
集合操作。
SpEL样例
需要了解的第一件事情就是SpEL表达式要放到“#{ ... }”之中, 这与属性占位符有些类似, 属性占位符需要放到“${ ... }”之中。 下面所
展现的可能是最简单的SpEL表达式了:
#{1}
除去“#{ ... }”标记之后, 剩下的就是SpEL表达式体了, 也就是一个数字常量。 这个表达式的计算结果就是数字1, 这恐怕并不会让你感到
丝毫惊讶。
当然, 在实际的应用程序中, 我们可能并不会使用这么简单的表达式。 我们可能会使用更加有意思的表达式, 如:
#{T{System}.currentTimeMillis()}它的最终结果是计算表达式的那一刻当前时间的毫秒数。 T()表达式会将java.lang.System视为Java中对应的类型, 因此可以调用
其static修饰的currentTimeMillis()方法。
SpEL表达式也可以引用其他的bean或其他bean的属性。 例如, 如下的表达式会计算得到ID为sgtPeppers的bean的artist属性:
#{sgtPeppers.artist}
我们还可以通过systemProperties对象引用系统属性:
#{systemProperties["disc.title"]}
如果通过组件扫描创建bean的话, 在注入属性和构造器参数时, 我们可以使用@Value注解, 这与之前看到的属性占位符非常类似。 不过, 在
这里我们所使用的不是占位符表达式, 而是SpEL表达式。 例如, 下面的样例展现了BlankDisc, 它会从系统属性中获取专辑名称和艺术家的
名字:
public BlankDisc(@Value("#{systemProperties[disc.title]}") String title, @Value("#{systemProperties[disc.artist]}") String artist) { this.title = title; this.artist = artist; }
在XML配置中, 你可以将SpEL表达式传入<property>或<constructor-arg>的value属性中, 或者将其作为p-命名空间或c-命名空间条
目的值。 例如, 在如下BlankDisc bean的XML声明中, 构造器参数就是通过SpEL表达式设置的:
<bean id="sgPeppers" class="soundsystem.SgtPeppers" c:title="#{systemProperties[disc.title]}" c:artist="#{systemProperties[disc.artist]]}" /> </bean>
表示字面值
我们在前面已经看到了一个使用SpEL来表示整数字面量的样例。 它实际上还可以用来表示浮点数、 String值以及Boolean值。
下面的SpEL表达式样例所表示的就是浮点值:
#{3.14159}
数值还可以使用科学记数法的方式进行表示。 如下面的表达式计算得到的值就是98,700:
#{9.87E4}
SpEL表达式也可以用来计算String类型的字面值, 如:
#{'Hello'}
最后, 字面值true和false的计算结果就是它们对应的Boolean类型的值。 例如:
#{false}
引用bean、 属性和方法
SpEL所能做的另外一件基础的事情就是通过ID引用其他的bean。 例如, 你可以使用SpEL将一个bean装配到另外一个bean的属性中, 此时要
使用bean ID作为SpEL表达式(在本例中, 也就是sgtPeppers) :
#{sgtPeppers}
现在, 假设我们想在一个表达式中引用sgtPeppers的artist属性:
#{sgtPeppers.artist}
表达式主体的第一部分引用了一个ID为sgtPeppers的bean, 分割符之后是对artist属性的引用。
除了引用bean的属性, 我们还可以调用bean上的方法。 例如, 假设有另外一个bean, 它的ID为artistSelector, 我们可以在SpEL表达式
中按照如下的方式来调用bean的selectArtist()方法:
#{artistSelector.selectArtist()}
对于被调用方法的返回值来说, 我们同样可以调用它的方法。 例如, 如果selectArtist()方法返回的是一个String, 那么可以调
用toUpperCase()将整个艺术家的名字改为大写字母形式:
#{artistSelector.selectArtist().toUpperCase()}
如果selectArtist()的返回值不是null的话, 这没有什么问题。 为了避免出现NullPointerException, 我们可以使用类型安全的运
算符:
#{artistSelector.selectArtist()?.toUpperCase()}
与之前只是使用点号(.) 来访问toUpperCase()方法不同, 现在我们使用了“?.”运算符。 这个运算符能够在访问它右边的内容之前, 确保它
所对应的元素不是null。 所以, 如果selectArtist()的返回值是null的话, 那么SpEL将不会调用toUpperCase()方法。 表达式的返
回值会是null。
在表达式中使用类型
如果要在SpEL中访问类作用域的方法和常量的话, 要依赖T()这个关键的运算符。 例如, 为了在SpEL中表达Java的Math类, 需要按照如下
的方式使用T()运算符:
T{java.lang.Math}
这里所示的T()运算符的结果会是一个Class对象, 代表了java.lang.Math。 如果需要的话, 我们甚至可以将其装配到一个Class类型的
bean属性中。 但是T()运算符的真正价值在于它能够访问目标类型的静态方法和常量。
例如, 假如你需要将PI值装配到bean属性中。 如下的SpEL就能完成该任务:
T{java.lang.Math}.PI
与之类似, 我们可以调用T()运算符所得到类型的静态方法。 我们已经看到了通过T()调用System.currentTimeMillis()。 如下的这个
样例会计算得到一个0到1之间的随机数:
T{java.lang.Math}.random()
SpEL运算符
SpEL提供了多个运算符, 这些运算符可以用在SpEL表达式的值上。 表3.1概述了这些运算符。