转载

Spring之旅第六站:处理自动装配的歧义性、SpEL

如果你有幸能看到。

  • 1、本文参考了《Spring 实战》重点内容,参考了GitHub上的代码
  • 2、本文只为记录作为以后参考,要想真正领悟Spring的强大,请看原书。
  • 3、在一次佩服老外,国外翻译过来的书,在GiuHub上大都有实例。看书的时候,跟着敲一遍,效果很好。
  • 4、代码和笔记在这里 GitHub ,对你有帮助的话,欢迎点赞。
  • 5、每个人的学习方式不一样,找到合适自己的就行。2018,加油。
  • 6、问候了下Java 8 In Action 的作者Mario Fusco,居然回复了。
  • 7、Spring In Action 、Spring Boot In Action的作者Craig Walls老忙了,没理睬。
  • 8、知其然,也要知其所以然。

之前,我们已经看到了如何使用自动装配让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。

3.3.1 表示首选的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 { ...}

就解决歧义性问题而言,限定符是一种更为强大的机制

3.2.2 限定自动装配的bean

设置首选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注解中,从而能够将他们组合在一起形成特定目标的自定义注解。

3.4 bean的作用域

默认情况下,Spring应用上下文中所有的bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多次,每次所注入的都是同一个bean。

在大多数情况下,单例bean是很理想的方案,初始化和垃圾回收对象实例所带来的成本只有一些小规模任务。在这些任务中,让对象保持无状态并且在应用中反复使用这些对象可能并不合理。

有时候可能发现,你所使用的类是异变的(mutable),它们会保持一些状态,因此重复使用时不安全的。在这种情况下将class声明为单例的bean就不是什么好主意了。因为会污染对象,稍后重用的时候会出现意想不到的问题。

Spring定义了多种作用域可以基于这些作用域创建bean,包括:

  • 单例(singleton):在整个应用中,只创建bean的一个实例
  • 原型(prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例
  • 会话(Session):在Web应用中,每个会话创建一个bean实例
  • 请求(Request):在Web应用中,为每个请求创建一个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实例。

3.4.1 使用会话和请求作用域

在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应该也以作用域代理的方式进行注入

3.4.2 在XML中声明作用域代理

如果你需要使用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)

3.5 运行时注入

当讨论依赖注入的时候,我们通常讨论的是将一个bean引入到另一个bean的属性或构造器参数中。它通常来讲指的是将一个对象与另一个对象关联起来

但bean装配的另一个方面指的是将一个值注入到bean的属性或构造器参数中。

有时候硬编码是可行的,但有时候我们可能会希望避免硬编码。而是让这些值在运行时在确定,为了实现这些功能,Spring提供了运行时求值的方式:

  • 属性占位符(Property placeholder)
  • Spring表达式语言(SpEL)

这两种技术的用法是类似的,不过他们的目的和行为是有所差别的。

3.5.1 注入外部的值

在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()实现的。

深入学习Spirng的Environment

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表达式语言提供了一种更为通用的方式在运行时计算所要注入的值。

3.5.2 使用Spring表达式语言进行装配

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

SpEL拥有的特性:

  • 使用bean的ID来引用bean
  • 调用方法和访问对象的属性
  • 对值进行算术、关系、逻辑运算
  • 正则表达式匹配
  • 集合操作

SpEL表达式要放要 #{...} 之中这与属性占位符有些类似,属性占位符需要放到${...}之中

#{T(System).currentTimeMillis()}    //计算表达式的那一刻当前时间的毫秒值。

#{systemProperties['disc.title']}   //引用其他bean和其他bean的属性

#{3.141519}                         //表示浮点值

#{artistSelector.selectArtist()}    //除了引用bean,还可以调用方法

如果要在SpEL中访问类的作用域的方法和常量,需要依赖T()这个关键运算符。

在动态注入值到Spring bean时,SpEL是一种很遍历和强大的方式。

3.6 小节

1、学习了Spring profile,解决了Spring bean 要跨各种部署环境的通用问题。Profile bean 是在运行时条件化创建bean的一种方式,但在Spring4中提供了@Conditional注解和SpringCondition接口的实现。

2、解决两种自动装配歧义的方法,首选bean以及限定符。

3、Spring嫩那个狗让bean以单例,原型、会话、请求作用域的方式来创建。

4、简单的学习了SpEl,它能够在运行时计算要注入的bean属性的值。

依赖注入能够将组件以及协作的其他组件解耦,AOP有利于将应用组件与跨多个组件的任务进行解耦。

原文  https://juejin.im/post/5a8efd555188257a666f1f18
正文到此结束
Loading...