Spring中bean的高级装配:Profile、条件化bean、自动装配的歧义性以及bean的作用域

一、环境与profile

在软件开发时,有一个很大的挑战就是将应用程序从一种环境迁移到另一种环境中。在开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移也无法工作。数据库配置,加密算法已经与外部系统的集成时跨环境部署时会发生变化的几个典型例子。
在数据库配置方面,在开发环境中我们可能会使用嵌入式数据库,并且能够加载测试数据;在生产环境中,可能更希望使用JNDI从容器中获取一个DataSource;而在QA环境中,我们可能更希望配置为Commons DBCP连接池。而上述的三个方法相同的仅限于都生成类型为DataSource的bean,仅此而已。

1、配置profile bean

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

1.1、在Java配置中,配置profile bean

使用@Profile注解指定某个bean属于哪一个profile,在3.1版本中@Profile注解只能用在类上面,而在3,2之后@Profile注解也可以用在方法上面,因此可以将不同的bean的声明放在同一个配置类中。下面举个简单例子说明一下。

需要注意的是,尽管每个DataSource bean都被声明在一个profile中,并且只有当规定的profile被激活时,相应的bean才会被创建,但是可能会有其他的bean并没有声明在profile范围内。没有被指定的bean始终都会被创建,和激活那个profile无关。

1.2、激活profile

Spring在确定哪个profile处于激活状态时,会检查两个独立的属性:spring.profiles.active和spring.profiles.default两个属性。active优先级高于default,当active没有设置的话,才会检查default。有多个方式来设置这两个属性:

  • 作为DispatcherServlet的初始化参数;
  • 作为web应用的上下文参数;
  • 作为JDNI条目;
  • 作为环境变量;
  • 在集成测试环境类上使用ActiveProfiles注解设置

具体使用哪一种或几种需要根据自己情况,在用到上述方式时,还得参考更加详细的资料。不过在Spring4.0之后引入了一个更加通用的实现条件化bean的定义,在这个机制中,条件完全由开发者自己确定。有关Profile机制的代码调试好久总是出问题,我放弃了!!!!以后果断选择条件化的bean!!!!

二、条件化的bean

假如你想要一个或多个bean在你想要的时候才会被创建,一种方法是可以使用之前提到过的Profile机制来实现,这里来说一种更加通用的机制即使用@Conditional注解,它可以用到类上,也可以用到带有@Bean注解的方法上。如果给定的条件计算结果为true,则创建该bean,否则不创建。 来看以下例子:

//三好学生
@Component
@Qualifier("top_ten")
public class SanHaoStudent implements Student { 
    private String name;
    private double score;

    public SanHaoStudent(String name, double score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public void showInfo() {
        System.out.println("----------三好学生---------");
        System.out.println("姓名:"+name+" "+"分数:"+score);
    }
}
//优秀学生
@Component
@Qualifier("top_twenty")
public class YouXiuStudent implements Student{
    private String name;
    private double score;

    public YouXiuStudent(String name, double score) {
        this.name = name;
        this.score = score;
    }
    @Override
    public void showInfo() {
        System.out.println("----------优秀学生---------");
        System.out.println("姓名:"+name+" "+"分数:"+score);
    }
}

再来看一下配置类:

@Configuration
public class JavaConfig {
    @Bean
    @Qualifier("top_ten")
    @Conditional(ScoresTopTen.class)
    public Student sanHaoStudent(){
        return new SanHaoStudent("xiaoshuang",99);
    }

    @Bean
    @Qualifier("top_twenty")
    @Conditional(ScoresTopTwenty.class)
    public Student youXiuStudent(){
        return new YouXiuStudent(env.getProperty("yafneg",88);
    }
}

注意在每个带有@Bean的方法上都有一个@Conditional注解,注解里面的类即为条件类,它们均实现了Condition接口,该接口中有一个matches方法,当该方法返回true时,对应得bean才会被创建。Condition接口内容如下:

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

再来看看两个条件类的具体实现:

public class ScoresTopTen implements Condition {
    double score = 99;
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return score >= 90;
    }
}
public class ScoresTopTwenty implements Condition {
    double score = 80;
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return score>=90;
    }
}

两个条件类里面的具体内容不重要,仅仅是为了说明问题。很显然第一个返回的时true,第二个返回的为false。如果代码不出错的情况下,此时应该会创建SanHaoStudent bean,而不会创建YouXiuStudent bean。

测试代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class Test {
    @org.junit.Test
    public void test(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
        Student student1 = (SanHaoStudent)context.getBean("sanHaoStudent");
        student1.showInfo();
        try {
            Student student2 = (YouXiuStudent)context.getBean("youXiuStudent");
            student2.showInfo();
        }catch (Exception e){
            System.out.println("YouXiuStudent未创建");
        }
    }
}
运行结果如下:
----------三好学生---------
姓名:xiaoshuang 分数:99.0
YouXiuStudent未创建

从上述结果可知,的确只创建了SanHaoStudent bean,而没有创建YouXiuStudent bean。因此Spring的这个机制可以根据需要而创建需要的bean,而对于那些目前来说没用的bean可以先不创建,这样可以大大提高代码性能,减少对内存的消耗。

三、解决Spring中自动装配的歧义性

在自动装配中有时候一个接口可能有多个实现类,则在自动注入bean时,Spring不能明确知道要注入哪个bean,因此可能会发生异常。空口无凭,代码为例:

现有一个甜点(Dessert)接口,它有三个实现类,IceCream类,Cookie类和Cake类。

IceCream类

@Component
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜欢吃冰淇淋!");
    }
}

Cookie 类

@Component
public class Cookie implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜欢吃曲奇!");
    }
}

Cake 类

@Component
public class Cake implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜欢吃饼干!");
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class DessertTest {
    private Dessert dessert;

    @Autowired
    public void setDessert(Dessert dessert) {//使用setter方法注入
        this.dessert = dessert;
    }

    @Test
    public void favoriteDessert(){
        dessert.getFavorite();
    }
}

运行程序,默默地等待。扣个鼻屎,玩会手机,擡头一看,我草咋那么多红色日志。你没有猜错,出错啦!异常如下:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
No qualifying bean of type 'com.tyf.day5.Demo1.Dessert' available: 
expected single matching bean but found 3: cake,cookie,iceCream

上述异常的大致意思就是在注入的过程中并有找到Desert类型唯一的bean,而是发现了三个即cake,cookie,iceCream。而在Spring自动注入时必须指明要注入哪个bean,这个得必须由开发者来指定,Spring容器是分不清到底应该注入哪个。解决歧义性的方式有三种:使用@Primary注解、使用@Qualifier注解对bean重命名以及使用自定义限定符注解(推荐使用)。

1、@Primary注解

存在多个相同类型的bean时,可以使用@Primary注解来标明哪一个bean为首选bean,以此来解决自动注入的歧义性。由于个人比较喜欢吃冰淇淋,因此我将IceCream作为首选bean,做法如下:

@Component
@Primary
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜欢吃冰淇淋!");
    }
}

做法很简单,即在某个类上直接添加@Primary注解即可,其他代码均不变。但是这种做法的局限性很大,如果有多个首选的bean,那么使用该注解就很难办到了。

2、@Qualifier注解

使用@Qualifier注解的做法就是为每个bean都起一个别名,然后在注入某个bean时再来指定要注入的bean,这样做的目的是为了缩小可选bean的范围,最终能够达到一个bean满足规定的限制条件。做法如下:

IceCream类:
@Component
@Qualifier("cold")
public class IceCream implements Dessert {
    @Override
    public void getFavorite() {
        System.out.println("最喜欢吃冰淇淋!");
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class DessertTest {
    private Dessert dessert;

    @Autowired
    @Qualifier("cold")
    public void setDessert(Dessert dessert) {//使用setter方法注入
        this.dessert = dessert;
    }

    @Test
    public void favoriteDessert(){
        dessert.getFavorite();
    }
}
结果如下:
最喜欢吃冰淇淋!

**

注意:当使用自定义的@Qualifier的值时,最佳实践是为bean选择特征性或描述性的术语,而不是随便使用名字。

**

3、使用自定义的限定符注解

由于Java中不允许在同一个条目上重复出现相同类型的注解,因此当出现具有相同特征的bean时,很难使用@Qualifier来限定唯一的bean。虽然不能使用相同类型的注解,但是可以使用不同类型的啊,因此可以自定义限定符注解。

首先来看一下如何来自定义注解:

@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Treate {
}

上述代码实现了一个名为Treat的注解,由于它带有@Qualifier注解,因此这就是一个限定符注解。以同样的方式再定义@Cool注解和@CanCode注解。

假如现在有两个人,他们都很帅,但是一个是程序员,一个是医生。他们的特征都是很帅,因此只用Cool这个特征不能分清他们,还得在使用注解加以区分。详细代码如下:

医生类:

@Component
@Cool
@Treate  //自定义限定符注解
public class Doctor implements People {
    @Override
    public void career() {
        System.out.println("我是一名医生");
    }
}

程序员类:

@Component
@Cool
@CanCode
public class Programmer implements People{
    @Override
    public void career() {
        System.out.println("我是一个程序员");
    }
}

测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JavaConfig.class)
public class Test {
    private People people;
    @Autowired
    @Cool
    @Treate

    public void setPeople(People people) {
        this.people = people;
    }

    @org.junit.Test
    public void test(){
        people.career();
    }
}
运行结果:
我是一名医生

比较推荐第三种方法,因为无论有多少个重复特征,只要不是同一个东西都能够再加特征来加以区分,因此今后尽量使用该方法来解决歧义性。

四、bean的作用域

Spring定义了下面四种bean的作用域:

(1)单例(Singleton):在整个应用中只创建bean的一个实例;
(2)原型(Prototyoe):在每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例;
(3)会话(Session):在web应用中,为每个回话创建一个bean实例;
(4)请求(Request):在web应用中为每个请求创建一个bean实例。

1、单例(Singleton)和原型(Prototype)

单例是Spring默认的作用域,但对于容易变的类型,单例并不合适。若要选择其他作用域,就需要使用@Scope注解,它可以和@Component以及@Bean注解一起使用。

例如将NotePad bean的作用域设置为原型,在组建扫描来发现和声明bean时,可以直接和@Component一起使用。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class NotePad{
    //省略
}

如上所示即为将NotePad的作用域设置为原型,同样还可以在Java配置文件中和@Bean一起使用或者在XML文件中配置,不再赘述。

2、会话(Session)和请求(Request)

会话和请求作用域一般都用于web应用中,电商平台中的购物车是一个典型的会话作用域的bean。如果购物车是单例的,那么在整个应用中所有用户都使用一个bean;如果购物车是原型的,则在某处向购物车添加商品后,在应用的另一处就可能不可用了,因为该bean时原型的。购物车和用户有很强的关联性,每个用户一个购物车,因此会话作用域的购物车更合适。设置某个bean为Session作用域的做法如下:

@Component
@Scope(value = WebApplicationContext.SCOPE_PROTOTYPE,proxyMode = ScopedProxyMode.INTERFACE)
public interface ShopCar{
    //省略
}

上述设置会让Spring容器创建多个ShopCar bean的实例,但是对于一个会话来说它只会创建一个实例,在当前会话相关的操作中,这个bean相当于还是单例的。

需要注意的是proxyMode 属性设置为ScopedProxyMode.INTERFACE,这个属性解决了将会话作用域的bean注入到单例bean中引发的问题。先来看一个proxyMode 的应用场景。

@Component
public class StoreService{
    private ShopCart shopCart;
    @Autorwired
    public void setShopCart(ShopCart shopCart){
        this.ShopCart = shopCart;
    }
}

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

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

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

ScopedProxyMode.INTERFACE表明代理要实现这个代理要实现ShopCart接口,并将调用委托给实现bean。如果ShopCart是一个类的话,Spring没有办法创建基于接口的代理,此时必须使用CGLib来生成基于类的代理,做法即为将proxyMode设置为ScopedProxyMode.TARGET_CLASS。

**

注:请求作用域的bean会面临相同的装配问题

**

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章