如果你有幸能看到。
之前,我们已经看到了如何使用自动装配让Spirng完全负责将bean引用注入到构造函数和属性中,自动装配能够提供很大的帮助,因为它会减少装配应用程序组件时所需的显示配置的数量。
不过仅有一个bean匹配所需的结果时,自动装配才是有效的。如果不仅一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器或方法参数
为了阐述自动装配的歧义性,假设我们提供@Autowired注解标注了setDessert方法
@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应用上下文里面的bean,然后, 当Spring试图自动装配的setDessert()中的Dessert参数时,它们并没有唯一、无歧义的可选值。
Spring此时别无选择,只好宣告失败并抛出异常,更准确的将。Spring会抛出: NoUniqueBeanDefinitionException
当Spring发生歧义时,Spring提供了多种可选方案来解决这样的问题。你可以将可选bean的某一个设为首选(primary)的bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。
在声明bean的时候,通过将一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其它可选的bean。
假设冰激凌就是你最喜欢的甜点,在Spring中,可以通过@Primary来表达最喜欢的方案。 @Primary
能够与 @Componnet
组合用在组件扫描的bean上,也可以与 @Bean
组合用在Java配置的声明中。
@Component @Primary public class IceCream implements Dessert {...}
或者你通过JavaConfig显示配置地声明IceCream,
@Bean @Primary public Dessert IceCream() { return new IceCream(); }
如果你喜欢使用XML配置bean的话,同样可以实现这样的功能。
<bean id="ceCream" class="com.guo.IceCream" primary="true"/>
如果你标注了两个或者多个首选bean,那么就无法工作了。
@Component @Primary public class Cake implements Dessert { ...}
设置首选bean的局限性在于@Primary无法将可选方案的范围限定到唯一一个无歧义的选项中。它只能表示一个优先的可选方案。
Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小范围。
@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。例如,我们确保要将IceCream注入到setDessert()之中。
@Autowired @Qualifier("iceCream") public void setDessert(Dessert dessert) { this.dessert = dessert; }
@Qualifier("iceCream")指向的是组件扫描时所创建的bean,并且这个bean是IceCream的实例。更具体一点:@Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。没有没有则和ID一样。
基于默认的bean ID作为限定符是非常简单的,但这有可能会引入一些问题。如果你重构了IceCrean类,将其重名为“Gelato”的话,那此时会发生什么情况?如果是这样的话,bean的默认ID和默认的限定符会变为gelato,这就无法匹配setDessert()方法中的限定符,自动装配会失败。
这里的问题在于setDessert()方法上所指定的限定符与要注入的bean的名称是紧耦合的。对类名称的任意改动都会导致限定符失败。
我们可以为bean设置自己的限定符,而不是依赖于将ID作为限定符。在这里所需要做的就是在bean声明上加@Qualifier注解。
@Component @Qualifier("cold") public class IceCream implements Dessert {...}
在这种情况下,cold限定符分配了IceCream bean。因为它没耦合类名,因此你可以随意重构IceCream,而不必担心会破坏自动装配。
在注入的地方,只要引用cold限定符就可以了
@Autowired @Qualifier("cold") public void setDessert(Dessert dessert) { this.dessert = dessert; }
值得一提的是,当通过Java配置显式定义bean的时候i@Qualifier也可以与@Bean注解一起。
@Bean @Qualifier public Dessert dessert () { return new IceCream(); }
当使用自定义的@Qualifier值时,最佳实践是为bean选择特征性或描述性的术语,而不是使用随意的名字。
面向特性的限定符要比基于bean ID的限定符更好一些。但是如果多个bean都具备这个相同的特性的话,这种做法也会出现问题。
这里只有一个小问题: Java不允许在同一个条目上重复出现相同类型的多个注解。如果你试图这样做的话,编译器将会出错。
但是我们可以创建 自定义的注解,借助这样的注解来表达bean所希望限定的特性。这里需要做的就是创建一个注解,它本身要使用@Qualifier注解来标注。
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface cold {}
当你不想用 @Qualifier注解的时候,可以类似的创建@Sort、@Crispy和@Fruity。通过在定义时添加@Qualifier注解。它就具有了@Qualifier注解的特性。
现在我们重新看一下IceCream,并为其添加@Cold和@Creamy注解
@Component @Clod @Creamy public class IceCream implements Dessert {...}
类似的,Popsicle类可以添加@Cold和@Fruity注解
@Component @cold @Fruity public class Popsicle implements Dessert {...}
最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选的范围缩小到只有一个bean满足需求。
为了得到IceCream bean 和 setDessert()方法可以这样使用注解:
@Autowired @Cold @Creamy public void setDessert(Dessert dessert) { this.dessert = dessert; }
通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有Java编译器的限制或错误,与此同时,相对于原始的@Qualifier并借助于String类型来指定限定符,自定义的注解也是类型安全的。
在本节和前节中,我们讨论了几种通过自定义注解扩展Spring的方式, 为了创建自定义的条件化注解,我们建议一个新的注解并在这个注解上添加了Conditional,为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上添加了@Qualifer。这种技术可以用到很多Spring注解中,从而能够将他们组合在一起形成特定目标的自定义注解。
默认情况下,Spring应用上下文中所有的bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多次,每次所注入的都是同一个bean。
在大多数情况下,单例bean是很理想的方案,初始化和垃圾回收对象实例所带来的成本只有一些小规模任务。在这些任务中,让对象保持无状态并且在应用中反复使用这些对象可能并不合理。
有时候可能发现,你所使用的类是异变的(mutable),它们会保持一些状态,因此重复使用时不安全的。在这种情况下将class声明为单例的bean就不是什么好主意了。因为会污染对象,稍后重用的时候会出现意想不到的问题。
Spring定义了多种作用域可以基于这些作用域创建bean,包括:
单例是默认的作用域,但是正如之前所述,对于异变的类型,这并不适合。如果要选择其他作用域,要使用@Scope注解,它可以与@Component或@Bean一起使用。
如果你使用组件扫描来发现bean和生命bean,那么你可以在bean的类上使用@Scope注解,并将其声明为原型bean
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class NotePad { }
在这里,使用 ConfigurableBeanFactory
类的 SCOPE_PROTOTYPE
常量设置了原型作用域。你当然可以使用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。
如果你想在JavaConfig中将NotePad声明为原型bean,那么可以组合使用@Scope和@Bean来指定所需的作用域
@Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public NotePad notepad { return new NotePad(); }
如果你想使用XMl来配置bean的话,你可以使用bean元素的scope属性来设置作用域
<bean id="notepad" class="com.guo.myapp.NotePad" scope="prototype"
不管你使用哪种方式来声明作用域,每次注入或从Spirng应用上下文中检索该bean的时候,都会创建新的实例。这样导致的结果就是每次操作都能得到自己的NotePad实例。
在Web应用中,如果能够实例化在会话和请求范围内共享的bean,那将是非常有价值的事。例如:在典型的电子商务中,可能会有一个bean代表用户的购物车,如果这个购物车是单例的话,那么 将导致所有的用户都会像同一个购物车中添加商品。另一方面,如果购物车是原型作用域,那么在应用中某一个地方往购物车添加商品,在应用的另外一个地方可能就不可用了。因为这里注入的是另外一个原型作用域的购物车。 就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大,要指定会话作用域,我们可以使用@Scope注解,它的使用方式和原型作用域是相同的。
@Component @Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES) public ShoppingCart cart() {...}
这里我们将value设置成 WebapplicationConext.SCOPE.SESSION
。这会告诉Spring为Web应用中的每个会话创建一个ShoppingCart。
需要注意跌是,@Scope同时还有另外一个ProxyMode属性,它被设置成了 ScopeProxyMode.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注入到SetShoppingCart方法中,但是ShoppingCart是会话作用域的,此时并不存在。直到用户进入系统,创建了会话之后,才会出现ShoppingCart实例。
另外系统中将会有多个实例:每个用户一个。 我们并不想让Spirng注入到某个固定的ShoppingCart实例到StoreService中,我们希望的是当StoreService处理购物车的时候,他所用使用的ShoppingCart实例恰好是当前会话所对应的一个。
Spring并不会将实例的ShoppingCart bean注入到StoreService,Spring会注入一个到ShoppingCart的代理。这个代理会暴露于ShoppingCart相同的方法。所以StoreService就会认为他是一个购物车。
但是当StoreService调用ShoppingCart的方法方法时,代理会对其进行解析,并将调用委托给会话作用域内真正的ShoppingCart。
现在我们带着这个 作用域的理解,讨论一下ProxyMode属性,如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean
如果ShoppingCart是接口,而不是类的话,这是可以的,但
如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了,此时,它必须使用CGLIB来生成基于类的代理。所以,如果bean类型是具体的类的话,我们必须要将ProxyMode属性设置为 ScopedProxyMOde.TARGET_CLASS
.以此来表明要以生成目标类 扩展的方法创建代理。
尽管,我主要关注量会话作用域,但是请求作用域的bean会面临相同的装配问题,因此,请求作用域的bean应该也以作用域代理的方式进行注入
如果你需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其ProxyMode属性了元素能够设置bean的作用域,但是该怎样设置代理模式呢?
要使用代理模式,我们需要使用Spring aop命名空间的一个新元素:
<?xml version="1.0" encoding="UTF-8"?> <bean id="cart" class="com.guo.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy/> </bean>
aop:scoped-proxy 是与@Scope注解的proxyMode属性功能相同的SpringXML配置元素,它会告诉Spring为bean创建一个作用域代理。默认情况下,它会使用CGLIB创建目标的代理。但是我们可以将proxy-target-class的属性设置为false,进而要求它生成基于接口的代理。
<?xml version="1.0" encoding="UTF-8"?> <bean id="cart" class="com.guo.myapp.ShoppingCart" scope="session"> <aop:scoped-proxy proxy-target-class = "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/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"> </beans>
Spring高级配置的另一个可选方案:Spring表达式语言(Spring Expression Language)
当讨论依赖注入的时候,我们通常讨论的是将一个bean引入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另一个对象关联起来
但bean装配的另一个方面指的是将一个值注入到bean的属性或构造器参数中。
有时候硬编码是可行的,但有时候我们可能会希望避免硬编码。而是让这些值在运行时在确定,为了实现这些功能,Spring提供了运行时求值的方式:
这两种技术的用法是类似的,不过他们的目的和行为是有所差别的。
在Spring中,处理外部值的最简单方式就是声明属性源,并通过Spring的Enviroment来检索属性。
一个基本的Spring配置类,他使用外部的属性来装配BlankDisc bean。
@Configuration @PropertySource("classpath:/com/guo/soundsystem/app.properties") public class EnvironmentConfig { @Autowired private Environment env; @Bean public BlankDisc blankDisc() { return new BlankDisc( env.getProperty("disc.title"), env.getProperty("disc.artist")); } }
@PropertySource 引用了类路径中一个名为app.properties的文件
这个属性文件加载到Spring的Environment中,同时blackDisc()方法中,会创建一个新的BlankDisc,它的构造参数是从属性文件中获取的,而这是通过getProperty()实现的。
getProperty() 方法并不是获取属性值的唯一方法,getProperty()方法有四个重载的变种形式
package org.springframework.core.env; /** * Interface for resolving properties against any underlying source. * * @author Chris Beams * @since 3.1 * @see Environment * @see PropertySourcesPropertyResolver */ public interface PropertyResolver { 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类型的值,但是你可以稍微对@Bean方法修改一些,这样在指定属性不存在的时候,会使用一个默认值。
@Bean public BlankDisc blankDisc() { return new BlankDisc( env.getProperty("disc.title","guo go go"), env.getProperty("disc.artist","UU")); }
剩下的两种getProperty()方法与前面的两种非常类似,但是他们不会将所有的值都视为String类型。假设你要获取的值所代表的连接池中所维持的连接数量,如果我们从属性文件中得到的是一个String类型的值,那么在使用之前还需要将其转化为Interge类型,但是如果使用重载的形式,就能非常便利的解决这个问题。
int connectionCount = env.getProperty("db.connection.count",Interge.class,30);
Environment还提供了几个与属性相关的方法,如果你在使用getProperty()方法的时候没有默认值,并且这个属性没有定义的话,获取到的值是null,如果你希望这个属性必须定义,那么可以使用getRequiredProperty(),
@Bean public BlankDisc blankDisc() { return new BlankDisc( env.getRequiredProperty("disc.title"), env.getRequiredProperty("disc.artist")); }
在这里,如果disc.title或者disc属性没有定义的话,将会抛出 lllegalStateException
异常
如果想要检查一个元素是否存在的话,可以调用Envrionment的contaiinsProperty()方法
boolean titleeExists = env.containsProperty("disc.title");
如果想将属性解析为类的话,可以使用getPropertyAsClass()方法
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class",CompactDisc.class);
除了属性的功能外,Environment还提供 一些方法来检查哪些Profile处于激活状态
public interface Environment extends PropertyResolver { String[] getActiveProfiles(); //返回激活profile名称的数组 String[] getDefaultProfiles(); //返回默认profile名称的数组 boolean acceptsProfiles(String... profiles); //如果environment支持给定的profile,则返回true }
直接从Environment中检索属性是非常方便的,尤其是在Java配置中装配bean的时候,但是Spring也提供了通过占位符装配属性的方式,这些占位符的值会来源于一个属性源。
Spring一直支持将属性定义到外部的属性配置文件中,并使用占位符值将其插入到Spring bean中,
<bean class="com.guo.soundsystem.BlankDisc" c:_0 = "${disc.title}" c:_1 = "${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, 从Spring3.1开始,推荐使用propertySourcesPlaceholderConfigurer,因为它能够基于Spirnig Environment 及其属性源来加载占位符。
@Bean public static propertyplaceholderConfigurer placeholderConfigurer() { return new propertyplaceholderConfigurer(); }
如果你想使用XML配置的话,Spring Context命名空间中的 context:propertyplaceholder 元素会为你生成
<?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:c="http://www.springframework.org/schema/c" 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-4.0.xsd"> <context:property-placeholder location="com/soundsystem/app.properties" /> </beans>
解析外部属性能够将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。而Spring表达式语言提供了一种更为通用的方式在运行时计算所要注入的值。
Spring 3引入了Spring表达式语言(SpringExpression Langguage SpEL),它能够以一种强大和简洁的方式将值装配到bean的属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算到值。。
SpEL拥有的特性:
SpEL表达式要放要 #{...}
之中这与属性占位符有些类似,属性占位符需要放到${...}之中
#{T(System).currentTimeMillis()} //计算表达式的那一刻当前时间的毫秒值。 #{systemProperties['disc.title']} //引用其他bean和其他bean的属性 #{3.141519} //表示浮点值 #{artistSelector.selectArtist()} //除了引用bean,还可以调用方法 如果要在SpEL中访问类的作用域的方法和常量,需要依赖T()这个关键运算符。
在动态注入值到Spring bean时,SpEL是一种很遍历和强大的方式。
1、学习了Spring profile,解决了Spring bean 要跨各种部署环境的通用问题。Profile bean 是在运行时条件化创建bean的一种方式,但在Spring4中提供了@Conditional注解和SpringCondition接口的实现。
2、解决两种自动装配歧义的方法,首选bean以及限定符。
3、Spring嫩那个狗让bean以单例,原型、会话、请求作用域的方式来创建。
4、简单的学习了SpEl,它能够在运行时计算要注入的bean属性的值。
依赖注入能够将组件以及协作的其他组件解耦,AOP有利于将应用组件与跨多个组件的任务进行解耦。