spring web運行時根據不同profile的配置,選擇性的加載組件(bean)

        本文的實踐方案其實很久之前就已經實現了(其想法跟實踐從主要點來看都非常的簡潔),由於我一直想將此方案應用於企業應用中並形成一個比較基礎的應用框架,因此關於本方案的文檔化拖了很久都沒有形成。

        運行時加載組件其實是個老生常談的話題了。比較早的windows桌面應用(最新的桌面應用實現此功能應該更不是事兒了)中,無論是功能組件還是窗口組件,在項目方案定義完接口後,主框架利用LoadLibrary、FreeLibrary等,在運行時(runtime),由用戶從某位置選載到func/window.dll組件文件,然後加載到應用中,完成主框架、功能框架的對接以及功能的運行時添加。如下圖。

        由上面的思路,本文介紹的主題是:在spring web應用中,如何做到在啓動運行時,根據用戶設定的應用配置,讓spring框架選擇性的加載組件(或者創建bean)。

 

我把上面這段主題的“例子”單列出來:

        比如,你有個web應用項目,是個小型oa管理系統,一般來說,你在idea中使用spring mvc創建一個工程後,爲了實現信息持久化,你需要連接數據庫;爲了實現oa流程,你需要依賴activiti之類的bpm框架;爲了實現某些場景的定時任務(比如每月一次的加班審報郵件確認),你需要依賴quartz之類的調度框架;等等。無論這些功能框架有沒有類似springboot下面的starter,你都可以自己造一些與這些框架或者組件適配的***Configuration,然後使用@Configuration註解,在應用運行時,創建這些框架相關的bean。

        上面這裏的問題是,一旦你選擇了讓功能框架添加進來,那啓動應用後,按照基本的默認情況,這些bean是必然是被創建的,啓動之後數據庫相關的bean、相對龐大的activiti、時不時出現的quartz日誌(爲了調試,你總是會輸出一些東西吧?),將會通過控制檯告訴你他們還活着。

        然而,我們當前其實就是想做一個crud頁面的前後端聯試,看看頁面是否正常,根本不需要那些數據庫、流程、調度框架現身。

        spring當然已經提供了@Conditional註解來告訴我們可以選擇了,然而spring考慮的仍然是普適,在一向需要定製的企業應用中,如果不在已有的基礎上進行增強,多半是滿足不了業務需求的(對開發來說,我們這裏是需要專一性):我們仍然是希望於通過一個簡單的外部配置參數(不同profile)來修改應用的狀態,而不是修改pom.xml中的依賴或者修改類的實現。

 

        本文就是介紹如何在spring web中,通過增強spring以實現以上場景中的“專一”開發的(即若是你多人開發,一人前端,一人流程,一人調度,各自pull下來的都是同一份程序,僅本地配置文件的值不同)。由此引入的應用場景是:多人開發的便捷、配置位置更加鬆散……

 

        嚴格來說(或者也是很明顯的),由於當前解決的是“啓動運行時”,因此本文介紹的方案屬於半運行時狀態,與一開始的windows桌面應用的運行時的即時加載還是有區別的(即時加載ms也有很多實現了,框架也提供了運行時創建bean的能力)。

        另外,很多同學早已轉戰使用spring boot了,在spring boot裏,使用對應的技術點,也可以實現本文的“啓動運行時”選擇性加載組件(我其實也已經實現了相關的功能,但關於相關文檔的說明仍在拖拉中……)

        結束冗長的背景回憶、適用場景以及簡短的目標,我們來開始工程的實現相關。

1、環境

jdk:1.7

開發環境:idea 2016.3.4

spring版本:4.3.14.RELEASE

maven:3.3+

 

2、主要技術點

這是一個maven項目。

2.0 pom.xml的配置。分爲兩個部分:

一是關於<profiles>節,我們需要根據項目情況創建若干個<profile>,每個<profile>內,我們需要在<properties>節內定義一個屬性(比如mvn.profiles.active),這個屬性的值與當前<profile>的<id>一般是相同的(也可以不同),用來替換後續在工程src/main/resources/application-***.properties文件名中的“***”區域,這主要是爲了在使用打包命令mvn package -P<profile.id>時,結合之後pom.xml中<resources>段中的設置,進行資源文件的精準化打包;

二是關於<build>/<resources>節,我們主要是實現資源的精準化打包。結合上面對<profile>的配置來完成,具體在後面的工程中可查看。

2.1 從org.springframework.web.context.ContextLoaderListener類派生一個CustomContextLoaderListenerod類,主要進行定製化實現:完成對某個profile約定的諸多properties文件中環境變量的先加載(重載customizeContext方法)。這裏,先創建這些環境變量的原因是爲後續在@Conditional註解需要的Conditional派生類中可以使用。

CustomContextLoaderListenerod類中加載的properties文件,必需有一個約定好名稱的環境變量(比如本文示例工程中的PROP_ZBASE_MAIN_CONFIG_ROOT),來表示外部配置文件組的根路徑

*外部配置文件組:指對某個<profile>來說,根據業務需要,你總是要有一堆配置存在某個地方,這個文件組就是專門存這裏配置的,對本項目來說,雖然是文件組,但它仍然可能是存在數據庫或者遠端某個服務器上。

2.2 web.xml中添加2.1中定製的<listener>;

2.3 application.properties文件。在resources目錄下,創建一個默認的applications.properties屬性文件,此文件其實跟本文的主題沒什麼關係,總之,你可以設置一些與<profile>無關的屬性放在它裏面;

2.4 application-**.properties文件。這些文件是必需的,且與<profile>應該是對應的。因爲PROP_ZBASE_MAIN_CONFIG_ROOT代表的環境變量,應該在每個application-**.properties文件裏都設置一個,用來表示外部配置文件組的根路徑。

2.5 外部配置文件中的bean創建開關。其實是.properties文件中的一個鍵值對,正常來說,你需要在每個<profile>對的配置文件組裏都有這麼一個創建開關(當然值可以不同,鍵肯定是要相同的)

2.6 根據項目需要,實現一個或者若干個org.springframework.context.annotation.Condition接口(比如PropConfigurationCondition類實現了此接口)。接口的matches方法最簡單的形式是判斷上面2.5步中的鍵值對中的值,以返回true或者false;

2.7 在一個bean創建類(比如PropConfiguration類)上,使用@Configuration註解的同時,也使用@Conditional註解,後者的value設置爲2.5步中的PropConfigurationCondition類;

 

至此,關於根據不同<profile>的配置,選擇性的創建bean的步驟就介紹完了。

下面是驗證步驟的要點:

2.8 創建一個Controller,在其中創建一個成員變量,使用@Autowired標識此成員變量由spring 進行注入,然而,此處你需要注意註解@Autowired的required需要設置爲false

 

3、maven工程的主要實現

這一節裏,就是主要實現的各種貼貼貼,你完全可以繞過本節到最後的下載鏈接裏,直接下載工程。

3.1 在idea中,新建一個maven工程:

        我們這裏把工程的groupid設爲com.bn.zbase,artifactid設爲zbase-test-web。

        使用上面archetype生成的工程裏,爲我們生成了一個默認的老舊的工程結構(應該是還有別的archetype可以達到主動設置此效果了,然後我已經等不及了)。這裏,我直接貼出改造後的目錄結構:

3.2 pom.xml的修改

本文配套的項目僅是一個演示,因此僅包含了基本的依賴,以下給出與2.0節對應的配置,其餘的可下載工程後查看

<profiles>
    <profile>
      <id>dev</id>
      <properties>
        <mvn.profiles.active>dev</mvn.profiles.active>
      </properties>
    </profile>
    <profile>
      <id>sit</id>
      <properties>
        <mvn.profiles.active>sit</mvn.profiles.active>
      </properties>
    </profile>
  </profiles>
  <build>
    <finalName>zbase-test-web</finalName>
    <plugins>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.6</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <excludes>
          <exclude>application*.properties</exclude>
        </excludes>
      </resource>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
          <include>application.properties</include>
          <include>application-${mvn.profiles.active}.properties</include>
        </includes>
      </resource>
    </resources>
  </build>

3.3 CustomContextLoaderListener類的實現

主要是重載了ContextLoader類的customizeContext方法,以使用本地方法先加載必備的屬性到context的環境變量對象中:

public class CustomContextLoaderListener extends ContextLoaderListener {

    private static final String PROP_ZBASE_MAIN_CONFIG_ROOT = "zbase.main.config.root";

    @Override
    protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
        // 加載classpath下的配置
        String strConfigApplication = "classpath:application*.properties";
        loadCustomConfigFile(wac, strConfigApplication);

        String strZbaseMainConfigRoot = wac.getEnvironment().getProperty(PROP_ZBASE_MAIN_CONFIG_ROOT);
        if (strZbaseMainConfigRoot.endsWith("/")) {
            strZbaseMainConfigRoot = strZbaseMainConfigRoot.substring(0, strZbaseMainConfigRoot.length() - 1);
        }
        // 以下分開讀取的原因是:當使用“http://***/*.properties”來獲取遠程的配置文件時,你需要指定具的文件名,
        // 而不能由*.properties來讀取所有的文件
        // 關於如何實現*.properties文件組,而不是使用下面的示例,其實也可以通過多加一個間接層來實現
        String strConfig = strZbaseMainConfigRoot + "/app.properties";
        loadCustomConfigFile(wac, strConfig);
        strConfig = strZbaseMainConfigRoot + "/app2.properties";
        loadCustomConfigFile(wac, strConfig);
        strConfig = strZbaseMainConfigRoot + "/biz.properties";
        loadCustomConfigFile(wac, strConfig);

        super.customizeContext(sc, wac);
    }

    /**
     * 加載配置文件
     * @param wac
     * @param strConfigFilePath
     */
    private void loadCustomConfigFile(ConfigurableWebApplicationContext wac, String strConfigFilePath) {

        List<Resource> listResources = new ArrayList<Resource>();
        ResourcePatternResolver objResolver = new PathMatchingResourcePatternResolver();
        try {
            listResources.addAll(Arrays.asList(objResolver.getResources(strConfigFilePath)));

        } catch (IOException e) {
            e.printStackTrace();
        }
        Resource[] locations = new Resource[listResources.size()];
        locations = listResources.toArray(locations);
        for (Resource item : locations) {
            ResourcePropertySource objRes = null;
            try {
                objRes = new ResourcePropertySource(item.getFilename(), item);
                wac.getEnvironment().getPropertySources().addLast(objRes);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

3.4 web.xml中的配置

添加一個<listener>節:

    <listener>
        <listener-class>com.bn.zbase.listener.CustomContextLoaderListener</listener-class>
    </listener>

3.5 resources目錄下的屬性文件添加

在resources目錄下,爲了演示,添加了三個資源文件:

與<profile>無關的application.properties:

# 當前使用的maven profile
# 當你在控制檯使用命令:mvn clean package -DskipTests -P***
# ***代表此處的值(不太準確,這是因爲此處的mvn.profiles.active與pom.xml中的<profiles>|<profile>的<id>設
# 爲相同的值
zbase.profiles.active=${mvn.profiles.active}

<profile>爲dev時的資源:application-dev.properties:

# 遠程服務器上的配置文件根目錄
# zbase.main.config.root=http://127.0.0.1:8070/dev
zbase.main.config.root=file:/${project.basedir}/conf/dev

<profile>爲sit時的資源:application-sit.properties:

zbase.main.config.root=file:/e:/proj/java/zbase-test-web/conf/sit

需要注意的是:application-dev.properties與application-sit.properties文件中屬性集應該是相同的。

 

3.6 自定義的外部配置文件組

如目錄結構圖中的conf文件夾,裏面包含了dev跟sit兩個子文件夾,這裏以dev/biz.properties文件爲例(bean創建開關的屬性文件):

# PropConfigurationCondition類中使用本屬性,用以判斷是否創建bean
propconfig.enabled=true

3.7 實現Condition接口的條件類

/**
 * Created by zcn on 2018/2/11.
 * PropConfiguration類由spring生成bean的條件
 */
public class PropConfigurationCondition implements Condition {

    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return conditionContext.getEnvironment().getProperty("propconfig.enabled").equalsIgnoreCase("true");
    }
}

3.8 bean配置類

@Configuration
@Conditional(PropConfigurationCondition.class)
public class PropConfiguration {

    @Value("${propconfig.enabled}")
    private String _strConfigRoot;

    @Value("${zbase.config.app.val01}")
    private String _strAppVal01;

    @Value("${zbase.profiles.active}")
    private String _strProfilesActive;

    @Bean("bizParasConfig")
    public BizParasConfig appConfig() {
        BizParasConfig objBean = new BizParasConfig();
        objBean.setMainConfigRoot(_strConfigRoot);
        return objBean;
    }

}

至此,主要的代碼段就結束,關於測試,就是如下的片段了:

@Controller
public class MainController {
    private static final Logger logger = LoggerFactory.getLogger(MainController.class);
    private static final String MAIN_CONFIG_ROOT = "mainConfigRoot";

    @Autowired(required = false)
    @Qualifier("bizParasConfig")
    private BizParasConfig bizParasConfig;

    @RequestMapping(value = {"/", "/index"})
    public String index(Model model) {
        String strVal = "nothing";
        if (bizParasConfig != null) {
            strVal = bizParasConfig.getMainConfigRoot();
        }

        model.addAttribute(MAIN_CONFIG_ROOT, strVal);
        logger.info(strVal);
        return "index";
    }
}

 

注意上面的@Autowired的required屬性爲false。

 

最後,是本文示例的下載鏈接:

https://github.com/basenumber/zbase-test-web.git

 

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