《Spring 实战》笔记3:高级装配2

bean 的作用域

在默认情况下,Spring 应用上下文中所有 bean 都是作为以单例(singleton)的形式创建的。

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)
// @Scope("prototype")  // 使用 SCOPE_PROTOTYPE 常量更加安全并且不易出错
public class NotePad {
}

在 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.myapp.NotePad"
    scope="prototype"

使用会话和请求作用域

在 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中声明作用域代理

如果你需要使用 XML 来声明会话或请求作用域的 bean,那么就不能使用 @Scope 注解及其 ProxyMode 属性了 <bean> 元素能够设置 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-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>

运行时值注入

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

但 bean 装配的另一个方面指的是将一个值注入到 bean 的属性或构造器参数中。例如将专辑的名字装配到 BlankDisc bean 的构造器活 title 属性中:

@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:_tilte="Sgt, Pepper's Lonely Hearts Club Band"
    c:_artist="The Beatles" />

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

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

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

注入外部的值

在 Spring 中,处理外部值的最简单方式就是声明属性源,并通过 Spring 的 Enviroment 来检索属性。

// 一个基本的 Spring 配置类,它使用外部的属性来装配 BlankDisc bean。
@Configuration
@PropertySource("classpath:/com/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"));
    }
}

// app.properties
// disc.tilte=Sgt, Pepper's Lonely Hearts Club Band
// disc.artist=The Beatles

PropertyResolver

getProperty() 方法并不是获取属性值的唯一方法,getProperty() 方法有四个重载的变种形式

package org.springframework.core.env;

public interface PropertyResolver {
    // 获取属性值,不存在为 null
    String getProperty(String key);

    // 指定属性不存在的时候,会使用一个默认值
    String getProperty(String key, String defaultValue);

    // 与前面的两种非常类似,但是它们不会将所有的值都视为 String 类型
    <T> T getProperty(String key, Class<T> targetType);
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);

    // 检查一下某个属性是否存在
    boolean containsProperty(String key);

    // 如果属性不存在,则抛出异常
    String getRequiredProperty(String key) throws IllegalStateException;

    /**
     * Return the property value associated with the given key, converted to the given targetType (never {@code null}).
     * @throws IllegalStateException if the given key cannot be resolved
     */
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

    /**
     * 解析给定文本中的 $ {...} 占位符,将其替换为 {@link #getProperty} 解析的相应属性值。
     * 没有默认值的无法解决的占位符将被忽略并传递不变
     */
    String resolvePlaceholders(String text);

    // 同上,如果占位符解析不存在,则抛出异常
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}

Environment

public interface Environment extends PropertyResolver {

    // 返回激活 profile 名称的数组
    String[] getActiveProfiles();

    // 返回默认 profile 名称的数组
    String[] getDefaultProfiles();

    // 如果 environment 支持给定 profile 的话,就返回 true。
    boolean acceptsProfiles(String... profiles);
}

直接从 Environment 中检索属性是非常方便的,尤其是在 Java 配置中装配 bean 的时候。但是,Spring 也提供了通过占位符装配属性的方法,这些占位符的值会来源于一个属性源。

解析属性占位符

Spring 一直支持将属性定义到外部的属性配置文件中,并使用占位符值将其插入到 Spring bean 中,占位符的形式为使用 ${ ... } 包装的属性名称

<bean class="com.soundsystem.BlankDisc"
      c:_title="${disc.title}"
      c:_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, 从 Spring3.1 开始,推荐使用 PropertySourcesPlaceholderConfigurer ,因为它能够基于 Spirnig Environment 及其属性源来加载占位符。

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

如果使用 XML 配置,Spring Context 命名空间中的 <context:propertyplaceholder> 元素会为你生成 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: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 拥有的特性:

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

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

装配 bean 时使用 SpEL

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- 命名空间条目的值。

<bean id="sgtPeppers"
    class="soundsystem.BlankDisc"
    c:_tilte="#{systemProperties['disc.title']}"
    c:_artist="#{systemProperties['disc.artist']}" />

表示字面值

#{3.1415}
#{9.87E4}
#{'Hello'}
#{false}

引用 bean、属性和方法

#{sgtPeppers} // 通过 ID 引用其他的 bean
#{sgtPeppers.artist}  // 引用 sgtPeppers 的 artist 属性
#{artistSelector.selectArtist()} // 调用 bean 的 selectArtist() 方法
#{artistSelector.selectArtist().toUpperCase()}
#{artistSelector.selectArtist()?.toUpperCase()}  // 避免空指针
// 如果 selectArtist() 的返回值是 null 的话,
// 那么 SpEL 将不会调用 toUpperCase() 方法。表达式的返回值会是 null。

在表达式中使用类型

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

T(java.lang.Math)  // 一个 Class 对象
T(java.lang.Math).PI  // 访问目标类型的常量
T(java.lang.Math).random()  // 访问目标类型的静态方法

SpEL 运算符

运算符类型 运 算 符
算术运算 + 、 - 、 * 、 / 、 % 、 ^
比较运算 < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge
逻辑运算 and 、 or 、 not 、 `
条件运算 ?:(ternary) 、 ?:(Elvis)
正则表达式 matches
#{2 * T(java.lang.Math).PI * circle.radius}  // 计算 circle bean 中所定义圆的周长
#{T(java.lang.Math).PI * circle.radius ^ 2}  // ^ 用于乘方计算的运算符

// 三元运算符(ternary)
#{scoreboard.score > 1000 ? "Winner!" : "Loser"}
// 三元运算符的一个常见场景就是检查 null 值
#{disc.title ?: "Rattle and Hum"}  // 如果是 null 的话,那么表达式的计算结果就会是 Rattle and Hum

// 正则表达式
// matches 运算符对 String 类型的文本(作为左边参数)应用正则表达式(作为右边参数)。
// matches 的运算结果会返回一个 Boolean 类型的值:如果与正则表达式相匹配,则返回 true;否则返回 false。
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}

计算集合

#{jukebox.songs[4].title}  // 计算 songs 集合中第五个元素的 title 属性
#{jukebox.songs[T(java.long.Math).random() * jukebox.songs.size()].title}  // 随机选择一首歌
#{'This is a test'[3]}  // s

// 查询运算符 .?[]
#{jukebox.songs.?[artist eq 'Aerosmith']  // artist 属性为 Aerosmith 的所有歌曲
// .^[]  在集合中查询第一个匹配项
// .$[]  在集合中查询最后一个匹配项
#{jukebox.songs.^[artist eq 'Aerosmith']

// 投影运算符 .![],从集合的每个成员中选择特定的属性放到另外一个集合中
#{jukebox.songs.![title]  // 将 title 属性投影到一个新的 String 类型的集合中
// 投影操作可以与其他任意的 SpEL 运算符一起使用
#{jukebox.songs.?[artist eq 'Aerosmith'].![title]

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Spring 实战》笔记3:高级装配2

文章字数:3.7k

本文作者:Bin

发布时间:2019-07-02, 22:13:00

最后更新:2019-12-03, 14:11:14

原始链接:http://coolview.github.io/2019/07/02/Spring/%E3%80%8ASpring%20%E5%AE%9E%E6%88%98%E3%80%8B%E7%AC%94%E8%AE%B03%EF%BC%9A%E9%AB%98%E7%BA%A7%E8%A3%85%E9%85%8D2/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录