Spring高级装配

Spring高级装配

由于前几天学SpringBoot自动装配时,发现自己的Spring的某些装配方式掌握的还不到位,因此这两天又看了几遍Spring高级装配。

Spring高级装配内容

  • Profile环境装配
  • Conditional条件装配
  • 处理装配时的歧义性
  • bean的作用域
    • 单例 - Singleton
    • 原型 - Prototype
    • 会话 - Session
    • 请求 - Request

1.Profile环境装配

  1. 配置

通常在开发中,从一个环境迁移到另一个环境往往会出现一些问题,最常见的就是开发环境下的数据库配置与生产环境下的数据库配置不是同一个的情况,而使用Spring从3.1版本起提供的@Profile功能,使用@Profile注解制定某个bean属于哪一个profile,只有在对应profile激活时,相应的bean才会被创建。下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class DataSourceConfig{

@Bean
@Profile("dev")//为开发环境指定装配的bean
public DataSource dataSource1(){
...
return dataSource1;
}

@Bean
@Profile("prod")//为生产环境指定装配的bean
public DataSource dataSource2(){
...
return dataSource2;
}
}

在这个例子中,只有当dev profile激活时,datasource1的bean会被创建,datasource2的bean就不会被创建,相反,当prod profile激活时,datasource2的bean会被创建,datasource1的bean就不会被创建。如果没有指定profile,那这个bean始终都会被创建,与profile无关。

profile也可以通过xml的方式配置,在这里就不说了(本文省略用xml配置方式)。

  1. 激活

Spring在确定哪个profile处于激活状态需要两个独立的属性:spring.profiles.activespring.profiles.default。顾名思义,如果设置了spring.profiles.active那么这个值就是当前确定激活的profile,但是如果spring.profiles.active没有设置,就根据spring.profiles.default的值确定激活的profile,如果spring.profiles.default也没有配置,那就没有激活的profile。

而这两个值的设置方式有多种:

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

2.Conditional条件装配

Spring4引入了一个新的@Conditional注解,他可以用到带有@Bean注解的方法上,如果给定的条件计算结果为true,则创建这个bean,反之则忽略这个bean。

例如,假设有个MagicBean的类,当设置了magic这个环境属性的时候,spring才会实例化这个类,如果环境属性中没有这个属性,则忽略这个类。下面是使用@Conditional实现的代码。

1
2
3
4
5
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}

我们给@Conditional中设置了一个Class–MagicExistsCondition,它实现了Condition接口,下面是这个接口的代码。

1
2
3
public interface Condition{
boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata);
}

当实现的matches()方法返回true时,则创建这个MagicBean,反之则忽略。下面是实现这个接口的代码。

1
2
3
4
5
6
public class MagicExistsCondition implements Condition{
public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata){
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}

在Spring4中,@Profile注解进行了重构,使其基于@ConditionalCondition实现。

3.处理自动装配的歧义性

在Spring的自动装配中,仅有一个bean匹配所需的结果时,自动装配才是有效的,当有多个bean能够匹配结果时,这种歧义性会阻碍Spring的自动装配。下面举一个例子。

1
2
3
4
@Autowired
public void setDessert(Dessert dessert){
this.dessert = dessert;
}

这是一个甜点的setter方法,其中Dessert是一个接口,并且有三个类实现了这个接口,分别是

1
2
3
4
5
6
7
8
@Component
public class Cake implements Dessert { ... }

@Component
public class Cookies implements Dessert { ... }

@Component
public class IceCream implements Dessert { ... }

因为这三个类都使用了@Component注解,因此都会被当做组件扫描,当spring在装配setDessert()时,就不知道装配哪一个,只好抛出异常。

Spring提供了多种可选方案来解决这样的问题。

  1. 表示首选的bean

我们可以在其中一个bean设置为首选,那么当spring同时扫描到这些可选的bean时,会使用首选的bean。例如将IceCream设置为首选。

1
2
3
@Component
@Primary
public class IceCream implements Dessert { ... }

但是如果标注了两个或者更多个首选bean,就无法正常工作了,因为spring依旧无法从两个首选的bean中选一个装配。

  1. 限定自动装配的bean

使用限定符是解决歧义性问题的一种更为强大的机制。可以在@Autowired协同使用,在注入的时候指明使用哪个bean。例如

1
2
3
4
5
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}

其中@Qualifier中制定的参数时想要注入的bean的ID。但是基于这种方式会导致当我未来将IceCream类更换名字时,这边将会报错,因为这个限定符与要注入的bean的名称时紧耦合的。

我们可以自定义限定符解决这个问题:

1
2
3
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }
1
2
3
4
5
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert;
}

还有一个问题,当我们未来又添加了一个甜品是

1
2
3
@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }

这是,一个限定符显然无法限定到一个可选的bean,这是我们可以用两个限定符来限制。

1
2
3
4
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert { ... }
1
2
3
4
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert { ... }

但是在java中又不允许在同一个条目上出现重复的同类型的多个注解(java8允许出现重复的注解,只要注解在定义时带有@Repeatable注解就可以,但是@Qualifier注解并没有在定义时添加@Repeatable注解),因此我们可以自定义注解在解决这个问题。

1
2
3
4
5
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold{}
1
2
3
4
5
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy{}

然后添加到指定的bean上就可以了

1
2
3
4
@Component
@Cold
@Creamy
public class IceCream implements Dessert { ... }

4.bean的作用域

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

其中单例时默认的作用域,若要选择其他的作用于,则需要使用@Scope注解, 可以与@Component@Bean一起使用。例如

1
2
3
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{ ... }
1
2
3
4
5
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad(){
return new Notepad();
}

也可以使用@Scope("prototype"),但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。也可以使用xml配置bean的scope,这里就不举例了。

会话、请求

在Web应用中,也经常遇到需要实例化在会话和请求范围内共享的bean,例如用户的购物车。如果是单例模式的话,会导致每个人都会向同一个购物车添加商品,而如果用原型作用域的,那么每次请求都重新创建一个购物车,那么上一次添加的东西就没有了。就购物车bean来说,会话作用域是最为合适的。例如

1
2
3
4
@Bean
@Scope(value=ConfigurableBeanFactory.SCOPE_SESSION,
proxyMode=ScopeProxyMode.INTERFACES)
public ShoppingCart cart(){ ... }

这里将作用域设为会话内共享,注意还有另一个参数proxyMode,它被设置成了ScopeProxyMode.INTERFACES,这个属性指明了这个类采用的代理模式是基于接口的代理,如果这个类没有实现接口而是一个具体的类的话,那么必须使用CGLib来生成基于类的代理,这时要将proxyMode设置成ScopeProxyMode.TARGET_CLASS

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×