spring的高级装配

715 阅读13分钟

环境与profile

配置profile bean

Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。当然,在这个过程中需要根据环境决定该创建哪个bean和不 创建哪个bean。不过Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定。这样的结果就是同一个部署单元(可能会是 WAR文件)能够适用于所有的环境,没有必要进行重新构建。

在3.1版本中,Spring引入了bean profile的功能。要使用profile,你首先要将所有不同的bean定义整理到一个或多个profile之中,在将应用部署 到每个环境时,要确保对应的profile处于激活(active)的状态。

在javaConfig中配置profile

1.@Profile注解在类上
在Java配置中,可以使用@Profile注解指定某个bean属于哪一个profile。例如,在配置类中,嵌入式数据库的DataSource可能会配置成如 下所示:

说明:@Profile注解应用在了类级别上。它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建。如果dev profile没有激活的话,那么带有@Bean注解的方法都会被忽略掉

  1. 1.@Profile注解在方法上
    在Spring 3.1中,只能在类级别上使用@Profile注解。不过,从Spring 3.2开始,你也可以在方法级别上使用@Profile注解,与@Bean注解 一同使用。这样的话,就能将这两个bean的声明放到同一个配置类之中,如下所示:

注意:没有指定profile的bean始终都会被创建,与激活哪个profile没有关系

在XML中配置profile

1.分别制定多个profile(多个xml配置文件) 可以通过元素的profile属性,在XML中配置profile bean

2.同意制定多个profile(在一个xml配置文件中)
根元素中嵌套定义元素,而不是为每个环境都创建一个profile XML文件。这能够将所有的profile bean定义放到 同一个XML文件中:

激活profile

Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:spring.profiles.active和spring.profiles.default。
1.如果设置了spring.profiles.active属性的话,那么它的值就会用来确定哪个profile是激活的。
2.但如果没有设置spring.profiles.active属性的话,那Spring将会查找spring.profiles.default的值。
3.如果spring.profiles.active和spring.profiles.default均没有设置的话,那就没有激活的profile,因此只会创建那些没有定义在 profile中的bean。

有多种方式来设置这两个属性:

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

在spring.profiles.active和spring.profiles.default中,profile使用的都是复数形式。这意味着你可以 同时激活多个profile,这可以通过列出多个profile名称,并以逗号分隔来实现。当然,同时启用dev和prod profile可能也没有太大的意义,不 过你可以同时设置多个彼此不相关的profile。

使用profil进行测试
Spring提供了@ActiveProfiles注解,我们可以使用它来指定运行测试时要激活哪个profile。

条件化的bean

你可能希望:
1.一个或多个bean只有在应用的类路径下包含特定的库时才创建。
2.或者我们希望某个bean只有当另外某个特定的bean也声明了之后 才会创建。
3.还可能要求只有某个特定的环境变量设置之后,才会创建某个bean。

在Spring 4之前,很难实现这种级别的条件化配置,但是Spring4引入了一个新的@Conditional注解,它可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则的话,这个bean会被忽略。

@Conditional注解的使用

例子: 例如,假设有一个名为MagicBean的类,我们希望只有设置了magic环境属性的时候,Spring才会实例化这个类。如果环境中没有这个属 性,那么MagicBean将会被忽略。

@Conditional中给定了一个Class,它指明了条件MagicExistsCondition。@Conditional将会 通过Condition接口进行条件对比:
设置给@Conditional的类可以是任意实现了Condition接口的类型。可以看出来,这个接口实现起来很简单直接,只需提 供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean。如果matches()方法返 回false,将不会创建这些bean

2.创建Condition接口的实现(MagicExistsCondition)
在本例中,我们需要创建Condition的实现并根据环境中是否存在magic属性来做出决策

接口说明

ConditionContext:


通过ConditionContext,我们可以做到如下几点:

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

AnnotatedTypeMetadata接口:
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。


借助isAnnotated()方法,我们能够判断带有@Bean注解的方法是不是还有其他特定的注解。借助其他的那些方法,我们能够检 查@Bean注解的方法上其他注解的属性。

处理自动装配的歧义性

在spring自动装配时,如果不止一个bean匹配,此时就发生了歧义性。会引发spring抛出NoUniqueBeanDefinitionException异常:

解决方法:1.可选bean中的某一个设为首选(primary)的 bean,2.或者使用限定符(qualifier)来帮助Spring将可选的bean的范围缩小到只有一个bean。

标示首选的bean

在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将 会使用首选的bean,而不是其他可选的bean。

通过@Primary来表示首选,该注解的用法:
1、和@Component注解一起用在类上。

2、和@Bean注解一起用在方法上

3、如果用在配置文件,可以使用<bean元素的primary属性

注意:如果有标识了多个首选bean,那么就像Spring无法从多个可选的bean中做出选择一样, 它也无法从多个首选的bean中做出选择。

限定自动装配的bean

Spring的限定符能够在所有可选的bean上进行缩小范围的操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的 限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围。

@Qualifier注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。

@Qualifier注解所设置的参数就是想要注入的bean的ID,其实更准确的讲@Qualifier("iceCream")所引用的bean要具有String类型的“iceCream”作为限定符。如 果没有指定其他的限定符的话,所有的bean都会给定一个默认的限定符,这个限定符与bean的ID相同。

创建自定义的限定符
可以为bean设置自己的限定符,而不是依赖于将beanID作为限定符(默认情况下bean的限定符和id一样)。要做的就是在bean声明上添加@Qualifier注解。
1.例 如,它可以与@Component组合使用。

在这种情况下,cold限定符分配给了IceCreambean。因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动 装配

2.当通过Java配置显式定义bean的时候,@Qualifier也可以与@Bean注解一起使用

使用:

在这种情况下,cold限定符分配给了IceCreambean。因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动 装配。在注入的地方,只要引用cold限定符就可以了。

使用自定义的限定符注解
面向特性的限定符要比基于bean ID的限定符更好一些。但是,如果多个bean都具备相同特性的话,这种做法也会出现问题。例如,如果引入 了这个新的Dessert bean,会发生什么情况呢:


现在我们有了两个带有“cold”限定符的甜点。在自动装配Dessert bean的时候,我们再次遇到了歧义性的问题,需要使用更多的限定 符来将可选范围限定到只有一个bean。

能想到的解决方案就是在注入点和bean定义的地方同时再添加另外一个@Qualifier注解。IceCream类大致就会如下所示

Popsicle类同样也可能再添加另外一个@Qualifier注解:
在注入点中,我们可能会使用这样的方式来将范围缩小到IceCream:

这里只有一个小问题:Java不允许在同一个条目上重复出现相同类型的多个注解。如果你试图这样做的话,编译器会提示错误。

解决方法:
可以创建自定义的限定符注解,借助这样的注解来表达bean所希望限定的特性。这里所需要做的就是创建一个注解,它本身要使 用@Qualifier注解来标注。
不再使用@Qualifier("cold"),而是使用自定义的@Cold注解

同样,可以创建一个新的@Creamy注解来代替@Qualifier("creamy"):
现在,我们可以重新看一下IceCream,并为其添加@Cold和@Creamy注解,如下所示:
类似地,Popsicle类可以添加@Cold和@Fruity注解:
最终,在注入点,我们使用必要的限定符注解进行任意组合,从而将可选范围缩小到只有一个bean满足需求。为了得到IceCream bean,setDessert()方法可以这样使用注解:

通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有Java编译器的限制或错误。

bean的作用域

在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:

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

选择作用域:
使用@Scope注解,可以与@Component或@Bean一起使用。
1.与@Component注解一起使用

说明:1.使用@Scope注解把bean声明为原型bean;2.使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。也可以使 用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。

2.使用@Scope和@Bean来指定所需的作用域:

3.如果你使用XML来配置bean的话,可以使用元素的scope属性来设置作用域:

不管你使用以上哪种方式来声明原型作用域,每次注入或通过Spring应用上下文中获取该bean的时候,都会创建新的实例

使用会话和请求作用域

例如,在典型的电子商务应用中,可能会有一个 bean代表用户的购物车就购物车bean来说,会话作用域是最为合适的,因为它与给定的用户关联性最大。要指定会话作用域,我们可以使用@Scope注解,它的使用 方式与指定原型作用域是相同的:

proxyMode属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。先来看一下proxyMode所解决问题的场景: 假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中,如下所示:

因为StoreService是一个单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCartbean注入到setShoppingCart()方法中。但是ShoppingCartbean是会话作用域的,此时并不存在。直到某个用户进入系统,创建了会话之后,才会出现ShoppingCart实例。

另外,系统中将会有多个ShoppingCart实例:每个用户一个。我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。我们希望的是当StoreService处理购物车功能时,它所使用的ShoppingCart实例恰好是当前会话所对应的那一 个。

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

现在,我们带着对这个作用域的理解,讨论一下proxyMode属性。如配置所示,proxyMode属性被设置成了ScopedProxyMode.INTERFACES,这表明这个代理要实现ShoppingCart接口,并将调用委托给实现bean。

如果ShoppingCart是接口而不是类的话,这是可以的(也是最为理想的代理模式)。但如果ShoppingCart是一个具体的类的话,Spring就没有办法创建基于接口的代理了。此时,它必须使用CGLib来生成基于类的代理。所以,如果bean类型是具体类的话,我们必须要将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

尽管我主要关注了会话作用域,但是请求作用域的bean会面临相同的装配问题。因此,请求作用域的bean应该也以作用域代理的方式进行注入。

在XML中声明作用域代理

如果你需要使用XML来声明会话或请求作用域的bean,那么就不能使用@Scope注解及其proxyMode属性了。<bean元素的scope属性能够设置bean的作用域,但是该怎样指定代理模式呢?
要设置代理模式,我们需要使用Spring aop命名空间的一个新元素:

<aop:scoped-proxy是与@Scope注解的proxyMode属性功能相同的Spring XML配置元素。它会告诉Spring为bean创建一个作用域代 理。默认情况下,它会使用CGLib创建目标类的代理。但是我们也可以将proxy-target-class属性设置为false,进而要求它生成基于接口的代理:
为了使用aop:scoped-proxy元素,我们必须在XML配置中声明Spring的aop命名空间: