Spring条件初始化资源

Aug 5, 2018


概述

在项目中会有依据一定条件来初始化Bean的需求,特别是在分Module的代码中,底层Module实现了对各种资源的封装,比如DB,Thrift,MQ,Http,调度任务等,但是上层的需求多种多样,比如有的Module只需要使用Thrift,但由于其依赖的底层Module封装了所有的资源,导致在上层Module运行时,除了Thrift,实际还初始化了DB,MQ,Http等资源,这不仅是种不必要的浪费,有时还可能带来问题。因此,本文将主要探讨如何能依据一定的条件,按需初始化Bean,以避免不必要的浪费。

说明

范例项目通过Main函数启动项目,项目结构参考,其中Db.java,Http.java,Kafka.java代表三种不同的资源,它们在init时会打印日志:

.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── foo
    │   │               ├── Boot.java
    │   │               └── beans
    │   │                   ├── Db.java
    │   │                   ├── Http.java
    │   │                   └── Kafka.java
    │   └── resources
    │       ├── applicationContext.xml

applicationContext.xml文件内容为:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring基础配置 -->
<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"
       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/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 类增强代理(而不是基于java的接口代理) -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

</beans>

启动类Boot.java内容为:

package com.example.foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 启动类
 *
 * @author huanhuang
 */
public class Boot {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        context.start();
    }
}

资源代码以Db.java为例:

public class Db {

    public void init() {
        System.out.println("Db has bean initialized");
    }
}

方法一:Spring Profile

Profile的概念其实很常见,比如Maven也有,大家一般用Maven的Profile来管理不同环境的配置文件。但我自己之前有一个误区,以为Spring的Profile只能选者一种特定的Profile,但实际上它是支持同时指定多种Profile的,这就带来了很大的灵活性,下面介绍如何基于Spring Profile来条件初始化资源,先列下项目结构,主要多了一个配置文件enableProfiles.properties

├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── foo
    │   │               ├── Boot.java
    │   │               └── beans
    │   │                   ├── Db.java
    │   │                   ├── Http.java
    │   │                   └── Kafka.java
    │   └── resources
    │       ├── applicationContext.xml
    │       └── enableProfiles.properties

Profile配置

applicationContext.xml文件中为不同的资源指定了Profile:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring基础配置 -->
<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">

    <!-- 类增强代理(而不是基于java的接口代理) -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <beans profile="enableDb">
        <bean class="com.example.foo.beans.Db" init-method="init"/>
    </beans>

    <beans profile="enableHttp">
        <bean class="com.example.foo.beans.Http" init-method="init"/>
    </beans>

    <beans profile="enableKafka">
        <bean class="com.example.foo.beans.Kafka" init-method="init"/>
    </beans>

</beans>

启用特定的Profile

这里我们把启用的profile放在了一个enableProfiles.properties文件里,内容为:

enableProfiles=enableDb,enableKafka

表明我们只启用DbKafka资源,这里要注意的一点是,从结果来看,需要在Spring初始化之前,设置选用好Profile,Boot.java代码如下:

/**
 * 启动类
 *
 * @author huanhuang
 */
public class Boot {
    public static void main(String[] args) {
        try {
            Properties properties = PropertiesLoaderUtils
            .loadAllProperties("enableProfiles.properties");
            String enableProfiles = properties.getProperty("enableProfiles");
            System.out.println(enableProfiles);
            System.setProperty(
            AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, enableProfiles);
        } catch (IOException e) {
            e.printStackTrace();
        }
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        context.start();
    }
}

这段代码里我们首先手动读取了enableProfiles.properties文件,然后最关键的代码在于下面一行:

System.setProperty(AbstractEnvironment.ACTIVE_PROFILES_PROPERTY_NAME, enableProfiles);

这个设定了一个环境变量,实际对应是的是spring.profiles.active,最后运行结果如下:

Db has bean initialized
Kafka has bean initialized

符合预期,只有DbKafka被启用了,这种方式简单明了。

方法二:Spring Conditional

Spring的@Conditional注解也就是依据一定的条件去初始化Bean,实际上Spring 4之后,@Profile注解就是通过@Conditional来实现的,基于@Conditional注解,实际上想怎么初始化资源都可以,Profile实际知识它的一个Feature。这个工具非常强大,唯一的问题是貌似不支持XML。下面是使用这种方式的代码结构:

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── foo
│   │   │               ├── Boot.java
│   │   │               ├── beans
│   │   │               │   ├── Db.java
│   │   │               │   ├── Http.java
│   │   │               │   └── Kafka.java
│   │   │               └── config
│   │   │                   ├── Config.java
│   │   │                   ├── DbCondition.java
│   │   │                   ├── HttpCondition.java
│   │   │                   └── KafkaCondition.java
│   │   └── resources
│   │       └── applicationContext.xml

因为@Conditional是注解,所以Bean的初始化也就通过@Configuration来实现了,需要配置applicationContext.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Spring基础配置 -->
<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"
       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/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd 
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 类增强代理(而不是基于java的接口代理) -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <context:component-scan base-package="com.example.foo"/>
</beans>

Bean的初始化在Config.java中:

@Configuration
public class Config {

    @Bean(initMethod = "init")
    @Conditional(DbCondition.class)
    public Db db() {
        return new Db();
    }

    @Bean(initMethod = "init")
    @Conditional(HttpCondition.class)
    public Http http() {
        return new Http();
    }

    @Bean(initMethod = "init")
    @Conditional(KafkaCondition.class)
    public Kafka kafka() {
        return new Kafka();
    }
}

可以看到,我们可以给不同的资源使用不同的Condition,非常强大,当然自己得多写点代码,以DbCondition.class为例:

/**
 * 启用Db的条件
 *
 * @author huanhuang
 */
public class DbCondition implements Condition {

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }
}

需要继承Condition接口,然后这个方法里怎么匹配,自己想怎么写都可以,这里我只有HttpCondition.class的方法返回true,其它都是false,运行结果如下:

Http has bean initialized

总结

Profile实际是Conditional的一个Feature,如果没有更复杂的需求,Profile就够用了,对于业务代码来讲Profile更容易理解,Conditional更适合于框架的场景。


上一篇博客:折腾MacVim
下一篇博客:RabbitMQ 介绍