高级装配
环境与profile
配置profile bean
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。
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 new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
需要注意的是@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev 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.jndi.JndiObjectFactoryBean;
@Configuration
@Profile("prod")
public class ProductionProfileConfig {
@Bean
public DataSource dataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
只有prod profile激活的时候,才会创建对应的bean。
在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,可以在方法级别上使用@Profile注解,与@Bean注解 一同使用。
package com.myapp;
import javax.sql.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 embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.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();
}
}
尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile激活时,相应的bean才会被创建,但 是可能会有其他的bean并没有声明在一个给定的profile范围内。没有指定profile的bean始终都会被创建,与激活哪个profile没有关系。
在XML中配置profile
可以通过元素的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"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
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设置为prod,创建适用于生产环境的从JNDI获取的DataSource bean。同样,可以创建基于连接池定义 的DataSource bean,将其放在另外一个XML文件中,并标注为qaprofile。所有的配置文件都会放到部署单元之中(如WAR文件),但是只 有profile属性与当前激活profile相匹配的配置文件才会被用到。
可以在根元素中嵌套定义元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
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">
<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="qa">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:url="jdbc:h2:tcp://dbserver/~/test" p:driverClassName="org.h2.driver" p:username="sa" p:password="password" p:initialSize="20" p:maxActive="30" />
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
除了所有的bean定义到了同一个XML文件之中,这种配置方式与定义在单独的XML文件中的实际效果是一样的。这里有三个bean,类型都 是javax.sql.DataSource,并且ID都是dataSource。但是在运行时,只会创建一个bean,这取决于处于激活状态的是哪个profile。
激活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应用的上下文参数
- 作为JND条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上,使用@ActiveProfiles注解设置
在Web应用中设置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring-context*.xml</param-value>
</context-param>
<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>
<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.profiles.default,所有的开发人员都能从版本控制软件中获得应用程序源码,并使用开发环境的设置(如 嵌入式数据库)运行代码,而不需要任何额外的配置。
当应用程序部署到QA、生产或其他环境之中时,负责部署的人根据情况使用系统属性、环境变量或JNDI设置spring.profiles.active即 可。当设置spring.profiles.active以后,至于spring.profiles.default置成什么值就已经无所谓了;系统会优先使 用spring.profiles.active中所设置的profile。
在spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着可以 同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。
Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。在集成测试时,通常想要激活的是开发环境的 profile。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class})
@ActiveProfiles("dev")
public class PersistenceTest {
...
}
条件化的bean
假设有一个名为MagicBean的类,只有设置了magic属性的时候,Spring才会实例化这个类。如果环境中没有这个属性,MagicBean将会被忽略。
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean() {
return new MagicBean();
}
@Conditional中给定了一个Class,它指明了条件。@Conditional将会通过Condition接口进行条件对比。
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
设置给@Conditional的类可以是任意实现了Condition接口的类型,这个接口只需实现matches()方法实现即可,true会创建bean,false则不创建。
package com.habuma.restfun;
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是一个接口:
package org.springframework.context.annotation;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
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注解的方法上还有什么其他的注解。AnnotatedTypeMetadata也是一个接口:
package org.springframework.core.type;
import java.util.Map;
import org.springframework.util.MultiValueMap;
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);
}
借助isAnnotated()方法,能够判断带有@Bean注解的方法是否还有其他特定的注解。借助其他的哪些方法,能够检查@Bean注解的方法上其他注解的属性。
从Spring4开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.ConfigurableEnvironment;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
@Profile本身也使用了@Conditional注解,并且引用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
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
Dessert是一个接口,有三个类实现了这个接口。
@component
public class Cake implements Dessert{ ... }
@component
public class Cookies implements Dessert{ ... }
@component
public class IceCream implements Dessert{ ... }
上面的类均使用了@component注解,当Spring试图自动装配setDessert()中的Dessert参数时,他并没有唯一、无歧义的可选值。Spring会抛出NoUniqueBeanDefinitionException.
Spring提供多种方案解决这样的问题:
- 将可选bean中的某一个设为首选(primary)的bean
- 使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean
标示首选的bean
在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将 会使用首选的bean,而不是其他可选的bean。
@component
@Primary
public class IceCream implements Dessert{ ... }
或者通过java配置显式声明
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
使用XML配置,同样可以实现这样的功能。
<bean id="iceCream" class="com.desserteater.IceCream" primary="true" />
不管采用什么方式来标示首选bean,效果都是一样的,都是告诉Spring在遇到歧义性的时候要选择首选的bean。 但是,如果标示了两个或更多的首选bean,那么它就无法正常工作了。
限定自动装配的bean
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标示一个优先的可选方案。
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
@Qualifier注解所设置的参数就是想要注入的bean的ID。所有使用@Component注解声明的类都会创 建为bean,并且bean的ID为首字母变为小写的类名。因此,@Qualifier("iceCream")指向的是组件扫描时所创建的bean,并且这个 bean是IceCream类的实例。
更准确地讲,@Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。如 果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。因此,框架会将具有“iceCream”限定符 的bean注入到setDessert()方法中。这恰巧就是ID为iceCream的bean,它是IceCream类在组件扫描的时候创建的。
创建自定义的限定符
们可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。在这里所需要做的就是在bean声明上添加@Qualifier注解。
@component
@Qualifier("cold")
public class IceCream implements Dessert{ ... }
cold限定符分配给了IceCreambean。因为它没有耦合类名,因此可以随意重构IceCream的类名,而不必担心会破坏自动 装配。在注入的地方,只要引用cold限定符就可以了:
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
当通过Java配置显式定义bean的时候,@Qualifier也可以与@Bean注解一起使用:
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}
当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。
使用自定义的限定符注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都具备相同特性的话,这种做法也会出现问题。
@component
@Qualifier("cold")
public class Popsicle implements Dessert{ ... }
有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,再次遇到了歧义性的问题,需要使用更多的限定符来将可选范围限定到只有一个bean。
能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解
@component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert{ ... }
Popsicle类同样也能再添加另外一个@Qualifier注解:
@component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert{ ... }
在注入点中,我们可能会使用这样的方式来将范围缩小到IceCream:
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
这里只有一个小问题:Java不允许在同一个条目上重复出现相同类型的多个注解。
使 用@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 IceCream implements Dessert{ ... }
Popsicle类可以添加@Cold和@Fruity注解:
@component
@Cold
@Fruity
public class Popsicle implements Dessert{ ... }
在注入点,可以使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求:
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert){
this.dessert = dessert;
}
通过声明自定义的限定符注解,可以同时使用多个限定符,不会再有Java编译器的限制或错误。与此同时,相对于使用原始 的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。
bean的作用域
在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean 多少次,每次所注入的都是同一个实例。
在大多数情况下,单例bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无 状态并且在应用中反复重用这些对象可能并不合理。
有时候,可能会发现,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例 的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。
Spring定义了多用作用域,可以基于这些作用域创建bean:
- 单例(Singleton):在整个应用中,只创建bean的一个实例。
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域,但是正如之前所述,对于易变的类型,这并不合适。如果选择其他的作用域,要使用@Scope注解,它可以 与@Component或@Bean一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad { ... }
使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。你当然也可以使 用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
如果使用XML来配置bean的话,可以使用元素的scope属性来设置作用域:
<bean id="notepad" class="com.myapp.Notepad"
scope="prototype" />
使用会话和请求作用域
在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事情。例如,在典型的电子商务应用中,可能会有一个 bean代表用户的购物车。如果购物车是单例的话,那么将会导致所有的用户都会向同一个购物车中添加商品。另一方面,如果购物车是原型作 用域的,那么在应用中某一个地方往购物车中添加商品,在应用的另外一个地方可能就不可用了,因为在这里注入的是另外一个原型作用域的 购物车。
就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用 方式与指定原型作用域是相同的:
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() { ... }
将value设置成了WebApplicationContext中的SCOPE_SESSION常量(它的值是session)。这会告诉Spring为Web应用 中的每个会话创建一个ShoppingCart。这会创建多个ShoppingCart bean的实例,但是对于给定的会话只会创建一个实例,在当前会话 相关的操作中,这个bean实际上相当于单例的。
@Scope同时还有一个proxyMode属性,它被设置成了ScopedProxyMode.INTERFACES。这个属性解决了将会话或请求作 用域的bean注入到单例bean中所遇到的问题。
假设我们要将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的代理。
这个 代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。但是,当StoreService调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
如配置所示,proxyMode属性被设置成 了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。
如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring 就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要 将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。
尽管主要关注了会话作用域,但是请求作用域的bean会面临相同的装配问题。因此,请求作用域的bean应该也以作用域代理的方式进行注 入。
在XML中声明作用域代理
如果需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其proxyMode属性了。元素的scope属性能够设置bean的作用域,要设置代理模式,我们需要使用Spring aop命名空间的一个新元素:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>
<aop:scoped-proxy >是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接 口的代理:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target="false" />
</bean>
为了使用<aop:scoped-proxy >元素,我们必须在XML配置中声明Spring的aop命名空间:
<?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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
运行时值注入
bean装配的另外一个方面指的是将一个值注入到bean的属性或者构造器参数中。
@Bean
public CompactDisc sgtPeppers() {
return new BlankDisc("Sgt. Pepper's Lonely Hearts Club Band", "The Beatles");
}
尽管这实现了需求,也就是为BlankDisc bean设置title和artist,但它在实现的时候是将值硬编码在配置类中的。与之类似,如果使用 XML的话,那么值也会是硬编码的:
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="Sgt. Pepper's Lonely Hearts Club Band" c:_artist="The Beatles" />
有时候硬编码是可以的,但有的时候,我们可能会希望避免硬编码值,而是想让这些值在运行时再确定。为了实现这些功能,Spring提供了两 种在运行时求值的方式:
- 属性占位符(Property placeholder)
- Spring表达式语言(SpEL)
注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
package com.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 EnvironmentConfig {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title"),
env.getProperty("disc.artist"));
}
}
@PropertySource引用了类路径中一个名为app.properties的文件:
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
这个属性文件会加载到Spring的Environment中,稍后可以从这里检索属性。同时,在blankDisc()方法中,会创建一个新的BlankDisc,它的 构造器参数是从属性文件中获取的,而这是通过调用getProperty()实现的。
深入学习Spring的Environment
Environment的getProperty()方法并不是获取属性值的唯一方法,getProperty()方 法有四个重载的变种形式:
- String getProperty(String key);
- String getProperty(String key, String defaultValue);
- T getProperty(String key, Class targetType);
- T getProperty(String key, Class targetType, T defaultValue);
前两种形式的getProperty()方法都会返回String类型的值。看到了如何使用第一种getProperty()方法。但 是,可以稍微对@Bean方法进行一下修改,这样在指定属性不存在的时候,会使用一个默认值:
package com.soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class EnvironmentConfigWithDefaults {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));
}
}
剩下的两种getProperty()方法与前面的两种非常类似,但是它们不会将所有的值都视为String类型。例如,假设想要获取的值所代表的含义是连接池中所维持的连接数量。如果我们从属性文件中得到的是一个String类型的值,那么在使用之前还需要将其转换为Integer类型。但是,如果使用重载形式的getProperty()的话,就能非常便利地解决这个问题:
int connectionCount = env.getProperty("db.connection.count", Integer.class, 30);
Environment还提供了几个与属性相关的方法,如果你在使用getProperty()方法的时候没有指定默认值,并且这个属性没有定义的话, 获取到的值是null。如果你希望这个属性必须要定义,那么可以使用getRequiredProperty()方法,如下所示:
package com.soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class EnvironmentConfigWithRequiredProperties {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
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> cdClass = 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装配中,占位符的形式为使用“${ ... }”包装的属性名称
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="${disc.title}" c:_artist="${disc.artist}" />
title构造器参数所给定的值是从一个属性中解析得到的,这个属性的名称为disc.title。artist参数装配的是名为 disc.artist的属性值。按照这种方式,XML配置没有使用任何硬编码的值,它的值是从配置文件以外的一个源中解析得到的。
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。在这种情况下,我们可以使 用@Value注解,它的使用方式与@Autowired注解非常相似。
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
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
如果你想使用XML配置的话,Spring context命名空间中<context:propertySourcesPlaceholderConfigurer >的元素将会为你生成PropertySourcesPlaceholderConfigurer 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: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/context http://www.springframework.org/schema/aop/spring-context.xsd">
<context:property-placeholder />
</beans>
解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。
使用Spring表达式语言进行装配
以一种强大和简洁的方式将值装配到bean属性和构造器参 数中,在这个过程中所使用的表达式会在运行时计算得到值。
SpEL拥有很多特性,包括:
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算术、关系和逻辑运算
- 正则表达式匹配
- 集合操作
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表达式传入或的value属性中,或者将其作为p-命名空间或c-命名空间条目的值。
<bean id="sgtPeppers" class="soundsystem.BlankDisc" c:_title="#{systemProperties['disc.title']}" c:_artist="#{systemProperties['disc.artist']}" />
表示字面值
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()}
在表达式中使用类型
如果要在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运算符
| 运算符类型 | 运 算 符 |
|---|---|
| 算术运算 | +、-、 * 、/、%、^ |
| 比较运算 | < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge |
| 逻辑运算 | and 、 or 、 not 、│ |
| 条件运算 | ?: (ternary) 、 ?: (Elvis) |
| 正则表达式 | matches |
作为使用上述运算符的一个简单样例,我们看一下下面这个SpEL表达式:
#{2*T(java.lang.Math).PI*circle.radius}
还可以在表达式中使用乘方运算符(^)来计算圆的面积:
#{T(java.lang.Math).PI*circle.radius^2}
当使用String类型的值时,“+”运算符执行的是连接操作,与在Java中是一样的:
#{disc.title + 'by' + disc.artist}
SpEL同时还提供了比较运算符,用来在表达式中对值进行对比。比较运算符有两种形式:符号形式和文本形式。在大多数情 况下,符号运算符与对应的文本运算符作用是相同的,使用哪一种形式均可以。
#{counter.total == 100}
或者
#{counter.total eq 100}
表达式的计算结果是个Boolean值:如果counter.total等于100的话,为true,否则为false
SpEL还提供了三元运算符(ternary),它与Java中的三元运算符非常类似。例如,如下的表达式会判断如果scoreboard.score>1000的 话,计算结果为String类型的“Winner!”,否则的话,结果为Loser:
#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
三元运算符的一个常见场景就是检查null值,并用一个默认值来替代null。例如,如下的表达式会判断disc.title的值是不是null,如 果是null的话,那么表达式的计算结果就会是“Rattle and Hum”:
#{disc.title ?: "Rattle and Hum"}
这种表达式通常称为Elvis运算符。
计算正则表达式
当处理文本时,有时检查文本是否匹配某种模式是非常有用的。SpEL通过matches运算符支持表达式中的模式匹配。matches运算符对 String类型的文本(作为左边参数)应用正则表达式(作为右边参数)。matches的运算结果会返回一个Boolean类型的值:如果与正则表达式相匹配,则返回true;否则返回false。
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
计算集合
SpEL中最令人惊奇的一些技巧是与集合和数组相关的。最简单的事情可能就是引用列表中的一个元素了:
#{jukebox.songs[4].title}
这个表达式会计算songs集合中第五个(基于零开始)元素的title属性,这个集合来源于ID为jukebox bean。
为了让这个表达式更丰富一些,假设我们要从jukebox中随机选择一首歌:
#{jukebox.songs[T(java.lang.Math).random()*jukebos.songs.size()].title}
“[]”运算符用来从集合或数组中按照索引获取元素,实际上,它还可以从String中获取一个字符。比如:
#{'This is a test'[3]}
这个表达式引用了String中的第四个(基于零开始)字符,也就是“s”。
SpEL还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集。作为阐述的样例,假设你希望得到jukebox 中artist属性为Aerosmith的所有歌曲。如下的表达式就使用查询运算符得到了Aerosmith的所有歌曲:
#{jukebox.songs.?[artist eq 'Aerosmith']}
可以看到,选择运算符在它的方括号中接受另一个表达式。当SpEL迭代歌曲列表的时候,会对歌曲集合中的每一个条目计算这个表达式。如果表达式的计算结果为true的话,那么条目会放到新的集合中。否则的话,它就不会放到新集合中。在本例中,内部的表达式会检查歌曲 的artist属性是不是等于Aerosmith。
SpEL还提供了另外两个查询运算符:“.^[]”和“.$[]”,它们分别用来在集合中查询第一个匹配项和最后一个匹配项。例如,考虑下面的表达 式,它会查找列表中第一个artist属性为Aerosmith的歌曲:
#{jukebox.songs.^[artist eq 'Aerosmith']}
,SpEL还提供了投影运算符(.![]),它会从集合的每个成员中选择特定的属性放到另外一个集合中。作为样例,假设我们不想要歌曲对象的集合,而是所有歌曲名称的集合。如下的表达式会将title属性投影到一个新的String类型的集合中:
#{jukebox.songs.![title]}
投影操作可以与其他任意的SpEL运算符一起使用。比如,我们可以使用如下的表达式获得Aerosmith所有歌曲的名称列表:
#{jukebox.songs.?[artist eq 'Aerosmith'].![title]}