美文网首页Spring Framework学习
Spring IOC学习(02)Bean的高级装配

Spring IOC学习(02)Bean的高级装配

作者: 郭艺宾 | 来源:发表于2020-03-04 20:35 被阅读0次

内容概览


    1. 环境与profile
    1. 条件化的bean
    1. 自动装配的歧义性
    1. Bean的作用域
    1. 运行时值注入
    1. 总结

1. 环境与profile


在项目开发的过程中,不可避免的一个问题是项目需要在不同的环境之间运行与切换,比如最经典的例子,通常大部分中小型的项目分为开发(dev),测试(test),生产(prod)三个环境,这三个环境中,配置的数据库,缓存,常量或者秘钥等内容非常有可能是不同的,环境的切换通常非常有可能引发非预期的问题,详细大家都深有体会。

这种多环境的问题,参数布局最合理的方案就是,将不同环境的参数配置到不同的互不影响的文件中,然后在一个总的配置中引用指定环境的配置,这样进行环境切换时,只需要修改指定引用即可,所幸Spring在这方面的配置上给了非常简洁的方案。只要使用哪一个环境的配置,就激活哪一个就可以。

下面通过非常简单的代码来看一个多环境的例子,首先来看JavaConfig配置类中配置多环境,我们首先来配置一个bean,这个bean属于开发环境:

上面的配置类中使用了一个注解@Profile,这个注解就是配置环境的注解,这个注解需要一个参数,参数值就是环境的名字,上面配置的就是开发环境的bean,下面来看测试环境:

注意注解@Profile现在是配置在类级别上的,它表示整个类中的bean都是属于这个环境下创建,其实该注解还可以加在方法级别上,这样就能将多个环境的bean放在同一个配置类中,

注意,指定环境的bean会在该环境被激活时才创建,那些没有指定环境的bean就表示是否创建与激活哪个环境都没有关系,所以始终都会被创建。

上面是在Java配置类中配置,下面来看在xml文件中配置环境,首先看一个整个文件都为开发环境的xml:

注意,我们在开始的beans标签的结尾加了一个 profile="dev" ,这样就能指定为dev环境。测试环境也是一样:

上面两个xml都是整体文件属于一个环境,其实还可以在同一个文件中配置多个环境的bean,只需要在根元素beans下面继续嵌套beans标签即可:

上面的例子在JavaConfig和xml两种文件中通过两种方式定义不同的项目运行环境中的bean,那么问题来了,如何激活并使用某个profile呢?

Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default,通过单词意思大概也可以理解这两个配置的含义,一个是主动激活,一个是默认值,如果设置了spring.profiles.active属性的话,那么它设置的值就是被激活的值,但是如果没有设置spring.profiles.active,Spring将会查找spring.profiles.default的值。如果两个都没有设置,那就表示没有任何环境被激活,因此只会创建那些没有指定profile的bean。有多种方式来设置这两种属性:

  • 作为 DispatcherServlet的初始化参数
  • 作为Web应用的上下文参数
  • 作为JNDI条目
  • 作为环境变量
  • 作为JVM的系统属性
  • 在集成测试类上,使用@ActiveProfiles注解设置

举个栗子,比如使用DispatcherServlet的参数将spring.profiles.default的值设置为dev开发环境,在Servlet上下文中进行设置(为了兼顾到ContextLoaderListener)。例如在web应用中,在web.xml文件中的设置如下:

这样设置在开发环境中,就能直接启动激活dev环境,不需要额外的配置。当应用程序部署到生产环境或者其它环境时,运维人员可以根据情况使用JVM系统属性或者配置环境变量或者JNDI设置等方式设置spring.profile.active即可。这样spring.profiles.default的值就无所谓了,不影响生产环境。系统会优先使用spring.profiles.active的值。

上面spring.profiles.*两个参数中,profiles都是复数形式,表示可以同时激活多个profile,以逗号间隔,当然设置的多个环境要没有冲突,否则没有意义。

最后来看下在测试的时候配置环境,只需要使用@ActiveProfiles注解即可,参数是一个或者多个环境名:

2. 条件化的bean


前面讨论的profile其实就是一种条件化创建,在满足一定条件,即当前环境处于激活状态,那么某些bean就会被创建。关于条件化创建bean,还可以使用注解 @Conditional 来实现。首先我们来定义一个普通类:

然后就可以将此类配置成一个bean,但是是有条件的,如果实现这个条件逻辑呢?我们需要创建一个条件类 User013Condition ,此类需要实现 Condition 接口:

这个接口需要我们实现一个 matches方法,这个方法返回true,就表示满足条件,可以创建bean,返回false就不能创建。matches方法很简单,但是功能强大,它有两个参数,第一个是ConditionContext类型参数,第二个是AnnotatedTypeMetadata类型参数。先来看第一个,ConditionContext是一个接口,我们来看一下这个接口:

其中:

  • getRegistry() 方法返回的 BeanDefinitionRegistry 可以检查bean的定义
  • getBeanFactory() 方法返回的 ConfigurableListableBeanFactory 可以检查bean是否存在,甚至探查bean的属性
  • getEnvironment() 方法返回的 Environment 可以检查环境变量是否存在,以及它的值是什么
  • getResourceLoader() 方法 可以读取并探查 ResourceLoader 所加载的资源
  • getClassLoader()方法 返回的 ClassLoader 可以加载并检查类是否存在

通过上面几个方法可以看到,ConditionContext参数十分强大,几乎可以获取spring项目中大部分的项目信息。第二个AnnotatedTypeMetadata类型参数能够让我们检查带有@Bean注解的方法上还有什么其它注解,AnnotatedTypeMetadata也是一个接口,具体内容如下:

其中借助 isAnnotated 方法,我们能够判断带有@Bean注解的方法是不是还有其它特定的注解,借助其它几个方法,我们能够检查带有@Bean注解的方法上其它注解的属性。我们来看一下@Profile:

可以发现,这个注解的功能也是通过@Conditional注解实现的,这是spring4版本对@Profile进行了重构,我们来看一下具体实现逻辑:

可以看到这这个类中,通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性,借助该信息,它会明确检查value值,该属性包含了bean的profile名称。然后它根据ConditionContext得到Environment 来检查该profile (借助acceptsProfiles方法) 是否处于激活状态。

下面我们来实现一个自己的逻辑,检查环境变量中是否含有ioc,如果有,就创建该bean,

然后再配置类中配置Bean:

3. 自动装配的歧义性


前面的内容我们已经讨论过自动化装配,不过自动装配有歧义性,如果自动装配时,不止有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。

举个栗子,我们使用自动装配:

然后,写一个接口,这个接口有两个实现类,都是bean,

当我们把这个bean装配到其它bean中时,却没有指明是哪个具体bean,而是使用接口类型:

我们来调用:

这时候,Spring无法做出选择,别无他法,只好宣布失败,跑出NoUniqueBeanDefinitionException异常:

这只是一个简单的例子,实际开发中,歧义性确实非常罕见,不过确实是一个问题。当确实发生歧义性时,Spring提供了多种可选方案来解决这样的问题,你可以将多个可选bean中的某一个设置为首选(primary)bean,或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有唯一一个。

来看一下第一种方案,使用primary来标记一个首选的bean,可以使用@Primary注解配合@Component注解使用,将其中一个bean标志位首选:

这是自动装配的情况,如果是显式配置,可以和@Bean注解配合使用,标记在方法上来设置首选bean,如果是xml配置,可以在bean标签内增加一个属性 primary="true" 来标记哪个是首选bean。这样在运行的时候,Spring就不会有歧义性。但是要注意首选bean不能标记多个,标记多个和没有标记效果一样,都会产生歧义。

上面是第一种方案,更加强大的是第二种方案,使用限定符。设置首选bean的局限性在于,无法将可选方案的范围限定到唯一一个无歧义性的选项中。它只能标识一个优先的可选方案,当首先bean的数量超过一个时,我们并没有其它方法进一步缩小可选范围。相反,限定符能够在所有可选方案中进一步缩小范围,最终能够达到只有一个bean满足所规定的限定条件,如果将现有的所有限定符使用上依然存在歧义性,那么你可以继续使用更多的限定符来继续缩小范围。

@Qualifier注解是使用限定符的主要方式,它可以与@Autowire等注入注解协同使用,在注入的时候指定想要使用的是哪个bean,例如:

这是使用限定符最简单的例子,@Qualifier注解的参数就是所选bean的id,我们前面讨论过,每个bean都有一个默认的id,就是类名,不过首字母要变为小写。

其实更准确的来说,@Qualifier注解的参数是一个String类型的限定符,每个bean都会指定一个限定符,如果没有显式指定,那么这个限定符和bean的id相同。

在没有显式的指明限定符的时候,会存在问题,如果类名修改了,那么限定符会跟着修改,所以更好的办法是手动指明一个限定符,这样就不会受其它地方的修改产生影响。例如:

同样,使用注解@Qualifier指定限定符还可以配置在带有@Bean注解的方法上。不过如果限定符有重复的情况,比如:

当然这种情况是极少的,遇到这种情况我们可以增加限定符来继续缩小范围:

报错了,这时候遇到了一个问题,Java不允许在同一个条目上重复出现相同类型的多个注解,这里可以采取一个折中的办法,就是自定义注解,比如根据 @Qualifier("nums"),@Qualifier("one"),@Qualifier("two")三个标识,我们可以自定义下面三个注解:

这样我们就可以把新定义的注解标记在原来有@Qualifier的地方:

通过自定义的限定符注解,我们就可以同时使用多个限定符,不会再报错了。相对于原来的@Qualifier注解,自定义限定符注解类型更加安全。

4. Bean的作用域


在默认情况下,Spring应用上下文中所有的bean都是以单例(singleton)的形式创建的。也就是说不管一个bean被注入到其它bean多少次,每次所注入的都是同一个实例,当然大多数情况下面,单例bean是理想的方案。

但是有的时候,某些bean是有一定状态的,它是易变的,因此重用就是不安全的,再使用单例就不合适了。在这方面,Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

  • 单例(Singletone):在整个应用中,只创建一个bean的实例
  • 原型(Prototype):每次注入或者通过Spring上下文获取时,都会创建一下新的bean实例
  • 会话(Session):在web应用中,为每个会话创建一个bean实例
  • 请求(Request):在web应用中,为每个请求创建一个bean实例

单例是默认的作用域,对于不适合单例的要选择其它作用域,需要使用@Scope注解,它可以与@Component或@Bean一起使用在类或者方法上。例如将一个类声明为原型bean:

这里使用 ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域,当然,也可以直接在注解里面写成:@Scope("prototype"),但是字面量毕竟容易出错,使用常量更加安全。同样,在配置类中显式配置bean也可以使用:

同样,在xml配置中,可以使用bean元素的scope属性来配置:

再来看一个购物车的例子。在电商网站上,每个用户都有一个自己的购物车,这个购物车的bean不能设置为单例作用域,否则全网站只有一个购物车肯定不合适,也不能设置为原型作用域,否则每次使用都会创建一个新的购物车,以前加进去的都没了,同样的道理,配置成请求作用域也会存在这个问题。所以,最合适的是会话作用域,因为它与用户的关联性最大,要指定会话作用域,我们可以使用@Scope注解,使用方式是相同的:

这里的WebApplicationContext类需要引入spring-web依赖。这样就会在web应用中为每个会话创建一个购物车实例。也就是对于同一个会话,购物车相当于单例。

@Scope注解还有一个属性 proxyMode,它的值在购物车中被设置成了 ScopedProxyMode.INTERFACES,这个属性解决了将会话或者请求作用域的bean注入到单例的bean中所遇到的问题,在讨论proxyMode之前,先来看一下它应用的场景,假设我们要讲购物车的bean注入到电商网站的bean中:

因为Store是一个单例的bean,会在Spring应用上下文加载时创建,当它创建时,Spring会试图将ShoppingCart注入到Store中,但是ShoppingCart是会话级别的bean,因此此时并不存在,直到第一个用户登录有了会话后才有ShoppingCart实例。

另外,系统中将会有多个ShoppingCart实例,因为每个用户一个,所以并不应该注入某个固定的ShoppingCart实例到Store中,最好Store处理购物车业务时,恰好是当前用户对应的那一个。

因此Spring并不会将实际的ShoppingCart实例注入到Store中,Spring会注入一个ShoppingCart的bean的代理,

如图,这个代理会暴露与ShoppingCart相同的方法,所以Store会认为它就是一个购物车,但是当Store调用ShoppingCart的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的 ShoppingCart 的bean。

现在我们可以来讨论一下proxyMode属性,ShoppingCart的proxyMode属性被设置成了 ScopedProxyMode.INTERFACES,这个属性值表明,这个代理要实现ShoppingCart接口,并将调用委托给实际实现这个接口的bean。

如果ShoppingCart是接口而不是具体类,这样是可以的,也是最理想的模式。但是如果ShoppingCart是一个具体类的话,Spring就没有办法创建基于接口的代理了,此时,必须使用 CGLib 来生成基于类的代理,所以如果bean类型是具体类的话,我们必须要将proxyMode的值设置为 ScopedProxyMode.TARGET_CLASS,以此表示要为类创建代理,所以代码应该改为:

上面我们主要讨论的会话作用域,不过请求作用域也会面临相同的问题,因此请求作用域的bean应该也以作用域代理的方式进行注入。

上面讨论的是Java配置类和自动装配的方式,在xml中配置会话作用域和请求作用域的bean,需要使用spring aop命名空间的一个新元素(需要声明aop命名空间,后面讨论aop会具体学习):

<aop:scoped-proxy/>是与@Scope注解的proxyMode属性功能相同的Spring xml配置元素。它会告诉Spring bean创建一个作用域代理,默认情况下,它会使用 CGLib创建目标类的代理,如果想要求生成基于接口的代理,需要设置 proxy-target-class="false" ,

5. 运行时值注入


Spring 的ioc中,依赖注入就是将一个bean的引用注入到另一个bean的属性或者方法参数中。本质上说就是将一个对象与另一个对象进行关联。不过前面的例子中,也有不少是直接在创建bean的时候赋值的,比如:

这种是采用硬编码的方式,直接给bean赋的初始值。有时候是可以的,但是很多时候,我们更希望避免硬编码,让这些属性或参数的值在运行时再确定,Spring提供了两种方式实现了运行时值注入的功能:

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

这两种的用法有点类似,不过目的和行为所有差别,其中属性占位符方式较为简单,先来看一下这种方法。

Spring中,处理外部值最简单的方式就是声明属性源,并通过Spring的Environment来检索属性。举个栗子,首先创建一个属性文件:

然后看配置:

代码中,引入配置文件app.properties 使用了注解 @PropertySource, 这个属性文件的内容会加载到Spring的Environment中,然后就可以从这里检索出属性。在配置方法中可以看到,属性的获取时通过 getProperty 方法实现的。下面来具体了解一下 Environment。

调用Environment方法的时候发现,getProperty并不是获取属性的唯一方法:

getProperty方法有四个重载 :

  • String getProperty(String key)
  • String getProperty(String key, String defaultValue)
  • T getProperty(String key, Class<T> type)
  • T getProperty(String key, Class<T> type, T defaultValue)

前两种方法都是返回String类型,第一种方法我们已经使用过了,第二种方法多了一个参数,默认值。也就是在获取不到指定属性的值的时候,直接返回一个默认值。剩下的两种getProperty方法与前面两种非常类似,但是它可以对返回类型进行设置,不一定是String类型。例如我们想要的值只是一个数量,这样就能直接获取对应的类型,而不必获取String,然后在进行类型转换:

Environment还提供了几个与属性相关的方法,如果你在使用 getProperty方法的时候,没有指定默认值,并且这个属性也没有定义的话,获取到的值是null,如果你希望这个属性必须要定义,那么可以使用getRequiredProperty方法:

在这里,如果属性 "app.user.age" 没有定义,会抛出 IllegalStateException 异常。如果想检查一下属性是否存在可以使用 containsProperty 方法:

Environment还提供了一些方法来检查哪些profile处于激活状态:

  • String[] getActiveProfiles() 返回激活profile名称的数组
  • String[] getDefaultProfiles() 返回默认profile名称的数组
  • boolean acceptsProfiles(String... profiles) 如果Environment支持给定profile的话,就返回true

在前面的讨论条件化创建bean的例子中,我们提到了 环境的的注解 @Profile 的底层实现使用的就是条件化创建,条件逻辑类是 ProfileCondition :

可以看到这个类中就使用了 acceptsProfiles 方法。Environment的方法不经常使用,但是一定要了解。直接从Environment中获取属性是非常方便的,尤其是在使用Java配置类的时候,不过Spring也提供了通过占位符装配属性的方法,占位符的值来源于一个属性源。在Spring装配中,占位符的形式为 "${...}" 包装的属性名称。

在Java配置类中,可以使用@Value注解来配置属性,例如:

或者直接放在参数中:

这样配置bean的时候,可以直接使用:

再来看xml配置的例子,首先引入配置文件,需要使用元素标签 context:property-placeholder ,然后引用属性值:

Spring 表达式语言(Spring Expression Language, SpEL)提供了另外一种更为通用的在运行时注入值。它能够以一种强大和简洁的方式将值装配到bean属性和方法参数中,在这个过程中表达式会在运行时计算得到值。使用SpEL可以实现非常不错的装配效果。SpEL拥有很多特性,包括:

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

而且SpEL还能够应用在依赖注入以外的地方(例如,Spring Security)。首先来看SpEL的几个例子,以及如何将其注入到bean中。

需要了解的第一件事就是SpEL表达式要放到 "#{...}" 之中,这与属性占位符有点类似,属性占位符需要放到 "${...}" 之中。下面来看一个最简单的SpEL表达式:#{1} ,大括号内部的就是实际的SpEL表达式体了。这里是一个数字常量 1 ,很明显,这个表达式的结果就是 1 。当然实际的表达式肯定更复杂一些。再来看一个例子: #{T(System).currentTimeMillis()} ,它的最终计算结果是获取那一刻的时间毫秒数。T()表达式会将java.lang.System视为Java中对应的类型,因此,可以调用它内部的静态方法 currentTimeMillis() ,举个栗子:

SpEL表达式也可以引用其它的bean或其它bean的属性,例如:#{role.titles} ,这个表达式会计算得到id为role的bean的titles属性,代码示例如下(role0202方法):

我们还可以通过systemProperties对象引用系统属性,比如获取操作系统的名字:

同时,我们可以自定义自己的配置文件的属性对象,然后以同样的方式引用:

上面是几个在配置类中的基本使用的例子,同样,在xml配置文件中使用方式类似,如获取系统属性:

获取用户自定义属性:

前面是几个简单的样例,下面来看一下SpEL所支持的基础表达式。前面已经看到一个表示常量的例子:#{1} ,同样,表示浮点值就是: #{3.14} ,表示科学计数法就是 : #{9.87E4} ,表示乘以10的四次方,就是 98700 ,也可以直接表示String类型的值: #{'Jack'} ,也可以表示boolean类型的值: #{true} 。当然单独包含一个字母量的值其实意义不大,但是由多个字面量、常量和变量组成的表达式是非常有用的。

前面的例子中,有对bean属性的引用 : #{role.titles} ,不仅是属性,还可以调用bean的方法: #{role.say()} ,还可以连续调用: #{role.say().toString()} ,不过如果 say() 方法的返回值为null就会报异常,为了避免这个问题,我们可以使用安全运算符: #{role.say()?.toString()} ,就是在无法确定的 say() 方法后面加个?,这样如果 say() 方法返回null ,就不会继续调用后面的 toString() 方法,表达式的结果直接返回 null 。

前面的例子中,有获取时间戳的例子: #{T(System).currentTimeMillis()} ,在SpEL表达式中,可以直接使用类型。如果要访问类的静态方法和常量的话,需要依赖 T() 这个关键的运算符,例如,使用Math类就需要像下面这样写: #{T(java.lang.Math)} ,这里的 T() 运算符的结果是一个class对象,这个运算符的真正价值就在于,它能访问类的静态方法和常量。例如,装配的时候需要常量 PI ,可以这样写: #{T(java.lang.Math).PI} ,获取一个随机数,就需要调用方法,可以这样写,#{T(java.lang.Math).random()} 。

SpEL提供了很多运算符,这些运算符可以运用在表达式上:

举个例子: #{2T(java.lang.Math).PIcircle.radius} ,这个例子展示了将简单的表达式组合为复杂的表达式,它的计算结果显而易见,就是圆的周长。 #{T(java.lang.Math).PI*circle.radius^2} 就是圆的面积,"^" 是用于乘方计算的运算符。

当使用String类型的值时 “+” 符号就表示连接操作: #{'hello ' + 'world'} ,与Java代码中是一样的,SpEL还提供了比较运算符,有符号和文本两种形式:

  • 1.#{user.age == 18} 与 #{user.age eq 18} 等价
  • 2.#{user.age > 18} 与 #{user.age gt 18} 等价
  • 3.#{user.age < 18} 与 #{user.age lt 18} 等价
  • 4.#{user.age >= 18} 与 #{user.age ge 18} 等价
  • 5.#{user.age <= 18} 与 #{user.age le 18} 等价

计算结果是boolean类型 true或者false。SpEL还提供了三元运算符,与Java代码中的很类似,比如判断年龄,大于18就是成人:#{user.age >= 18 ? 'man' : 'child'} 。三元运算符常见的一种场景就是检查null值,如果是null就用一个默认的值来代替null值,下面会判断name是否为null,如果是null就返回 "Jack" :

  • .#{user.name ?: 'Jack'}

当处理文本时,有时候检查文本是否匹配某种规则是非常有用的,比如正则表达式。SpEL通过matches运算符支持表达式中的模式匹配。matches运算符对String类型的文本(作为左边参数)进行匹配正则表达式(作为右边参数)。如果匹配就返回true,反之返回false。假设我们想检查一个问题是否是邮件地址:

  • .#{user.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.com'}

除了正则表达式,SpEL最令人惊叹的技巧是操作集合和数组。比如:#{role.users[2].title} 表示引用一个元素。比如: #{role.users[T(java.lang.Math).random()*role.users.size()].title} 表示随机获取一个人的title。

"[]"运算符用来从集合或者数组中按照索引获取元素。不仅如此,它还可以从字符串中获取一个字符,比如获取第五个:#{'hello'[4]} ,结果就是 "e" .

SpEL还提供了查询运算符(.?[]),它会用来对集合进行过滤,得到集合的一个子集。假设,要获取角色中,所有年龄大于18岁的用户:#{role.users .?[age > 18]} ,可以看到查询运算符的方括号中有另一个表达式,这个表达式结果为true,这个元素就会放到结果列表中。

SpEL还提供了投影运算符(.![]),它从集合的每个成员中选择特定的属性放到另外一个集合中。假设我们要获取所有用户的名字:#{role.users .![name]} ,实际上,投影的操作可以与其它任意的运算符一起使用,比如将查询运算符与投影一起使用:#{role.users .?[age>18] .![name]} ,表示所有成人的名字。

SpEL还有很多例子,不过要注意,写SpEL的时候,不要过于复杂,否则维护和测试起来都很麻烦。简单直接就可以。

6. 总结


这篇内容是上一篇的延续,学习了很多spring ioc的高级技巧,完善了spring ioc的整个知识结构 ,为后面的内容学习 spring aop做好准备。

代码地址:https://gitee.com/blueses/spring-framework-demo
11-12:profile与环境配置
13:条件化的bean
14-17:自动装配的歧义性
18:bean的作用域
19-20:运行时值注入

相关文章

网友评论

    本文标题:Spring IOC学习(02)Bean的高级装配

    本文链接:https://www.haomeiwen.com/subject/uejbqhtx.html