基于Profile實現(xiàn)技術(shù)棧切換適配多環(huán)境部署

問題背景

在商業(yè)化的場景下,客戶經(jīng)常會有私有化部署的需求,然而客戶的應(yīng)用環(huán)境和基礎(chǔ)設(shè)施是多樣的,可能是純開源的自建機房,也可能是基于商業(yè)化云服務(wù)商提供的公有云和私有云,由于這種差異的存在,上層應(yīng)用產(chǎn)品不可能通過一套實現(xiàn)完全適配所有的技術(shù)棧,但是應(yīng)用產(chǎn)品核心的功能和對外提供的服務(wù)都是標準、統(tǒng)一的,和具體的技術(shù)棧沒有關(guān)系,在這個背景下,我們期望把產(chǎn)品核心的非技術(shù)棧相關(guān)的能力抽象出來,技術(shù)棧相關(guān)的通過SPI和多技術(shù)棧切換能力進行適配和管理,做到一套核心代碼+技術(shù)棧適配支持在不同的技術(shù)棧環(huán)境下部署。

這里面涉及到的關(guān)鍵技術(shù)就包括maven profile+Spring profile+
Java SPI技術(shù),其他是一些領(lǐng)域設(shè)計時模塊劃分的設(shè)計。

基礎(chǔ)知識

對于軟件開發(fā)者而言,經(jīng)常要控制的就是當(dāng)前程序是在開發(fā)環(huán)境運行還是在生產(chǎn)環(huán)境運行,主流的控制手段有兩種:

  • Maven profile標簽
  • Spring Profile機制

下文我們將對這兩個特性的基本用法做一些介紹,了解和熟練使用它們是我們進一步實現(xiàn)多環(huán)境部署時,應(yīng)用和中間件適配并且具備良好可擴展性的重要保證。

Maven Profile

<profiles>
    <profile>
        <id>internal</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <build.profile.id>internal</build.profile.id>
        </properties>
    </profile>
    <profile>
        <id>outer</id>
        <properties>
            <build.profile.id>outer</build.profile.id>
        </properties>
    </profile>
</profiles>

<build>
        <filters>
            <filter>profiles/${build.profile.id}/config.properties</filter>
        </filters>

        <resources>
            <resource>
                <filtering>true</filtering>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        ...
</build>    

Spring Profile

Spring Profile只是一種環(huán)境控制的參考手段,他的好處是可以在代碼級別去控制,具體使用什么根據(jù)項目的需要去考量。

名詞解釋

Environment

在spring中,Environment是對應(yīng)用環(huán)境的抽象(即對Profile和properties的抽象).

Profile

Profile定義了應(yīng)用環(huán)境,即開發(fā), 生產(chǎn), 測試等部署環(huán)境的抽象.

使用方式

編程式

下面通過一個具體的Case來介紹下如何在實際的編程代碼中使用Prifle機制。

定義一個servuce接口和三個service的實現(xiàn)類:

public interface BAT {
    String getName();
}
class B implements BAT {
    public String getName() {
        return "B";
    }

}
class A implements BAT {
    public String getName() {
        return "A";
    }

}
class T implements BAT {
    public String getName() {
        return "T";
    }
}

然后我們通過純Java配置講接口的每個實現(xiàn)添加到容器中:

@Configuration
public class EnvironmentApp {

    @Bean
    @Profile("test")
    public B b() {
        return new B();
    }
    
    @Bean
    @Profile("project")
    public A a() {
        return new A();
    }
    
    @Bean
    @Profile("production")
    public T t() {
        return new T();
    }
}

下面建一個測試類:

public class TestMain
      public static void main(String[] args) {
        //在啟動容器之前,先指定環(huán)境中的profiles參數(shù)
        System.setProperty("spring.profiles.active", "project");
        ApplicationContext ctx = new AnnotationConfigApplicationContext(EnvironmentApp.class);
        //當(dāng)前的profile值是project,所以獲取的實現(xiàn)類是A
        A a = ctx.getBean(A.class);
    }
}

@Configuration類中每一個@Bean注解之后都有一個@Profile注解。@Profile中的字符串就標記了當(dāng)前適配的環(huán)境變量,他配合System.setProperty("spring.profiles.active", "project");這一行一起使用。當(dāng)設(shè)定環(huán)境參數(shù)為wow時,標記了@Profile("project")的方法會被啟用,對應(yīng)的Bean會添加到容器中。而其他標記的Bean不會被添加,當(dāng)沒有適配到任何Profile值時,@Profile("default")標記的Bean會被啟用。

Spring Profile的功能就是根據(jù)在環(huán)境中指定參數(shù)的方法來控制@Bean的創(chuàng)建。

@Profile可以用在類上, 還可以用在方法上. 用于在不同的環(huán)境加載不同的配置。

首先看一下直接作用在類上的用法:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

}

接著看一下直接作用在方法上的用法:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development")
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production")
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

備注:

  • 用于方法上時, 可能是在不同環(huán)境對同一個bean的加載, 由于類中不允許存在簽名完全相同的方法, 故可用@Bean("beanName")來定義不同的方法指向同一個bean.
  • 如果一個配置類中有多個@Bean重載方法. 在所有的重載方法上定義的@Profile注解定義應(yīng)當(dāng)一致,否則只有第一個聲明有效.

Spring profile的激活方式可以有多種:
方式一:設(shè)置系統(tǒng)環(huán)境變量
Profile的環(huán)境變量可以包含多個值。例如:

System.setProperty("spring.profiles.active", "project,test");

這樣環(huán)境中就包含了2個Profile的值。對使用的@Profile或profile配置就會被啟用。

ctx.getEnvironment().setActiveProfiles("project", "test");

備注:該方式最大的特點適用于純Java項目,大型的Java工程都不太方便使用該方式,且是在程序運行時修改的,修改profile參數(shù)修改改動代碼,不符合代碼與配置分離的原則,基本屬于玩具性質(zhì)的。

方式二:設(shè)置JVM啟動參數(shù)
與修改環(huán)境變量的方式類似,也可以指定同時激活一個或者多個profile。

-Dspring.profiles.active="project,test"

備注:該方式最大的特性是在運行期指定profile。

方式三:SpringBoot properties設(shè)置
針對SpringBoot項目,在properties文件中指定,在應(yīng)用依賴的properties文件中增加spring.profiles.active=test等配置,即可切換場景。

備注:該方式最大的特點是可以能夠在打包的時候就指定profile確定啟動場景

即在properties文件中使用占位符,在maven的profile中通過filter,通過maven profile來編譯時替換。

@profile注解的更多用法

值得一提的是,想很多其他Spring注解一樣,@Profile注解可以被當(dāng)做元注解來使用。這就意味著你可以定義自己自定義的注解,使用@Profile標記,并且Spring仍然可以檢測出來,就像它們被直接聲明使用的那樣。

package com.bank.annotation;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("dev")
pubilc @interface Dev {
}

這樣做的話我們就可以同時使用到@Component注解和我們自定義的@Dev 注解來標記類,而不是直接使用Spring提供的Profile,這樣做的好處在于多了一層代理,發(fā)生修改的情況下直接去修改@Profile的內(nèi)容即可。

@Dev 
@Component
public class MyDevService { ... }

或者,也可以給@Configuration的類加上@Dev的注解,那么這個配置類的下所有的bean都會根據(jù)@Profile的指定進行加載了。

@Dev 
@Configuration
public class StandaloneDataConfig { ... }

@profile的原理

Profile特性的實現(xiàn)也不復(fù)雜,其實就是實現(xiàn)了Conditional功能(Conditional功能見@Configuration與混合使用一文中關(guān)于Conditionally的介紹)。

首先@Profile注解繼承實現(xiàn)了@Conditional:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {}

然后他的處理類實現(xiàn)了Condition接口:

class ProfileCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles((String[]) value)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

處理過程也很簡單,實際上就檢查@Profile注解中的值,如果和環(huán)境中的一致則添加。

XML定義

根據(jù)參考資料1中Spring的官方說明, Spring Framework 3.1 M1 released發(fā)布了一個新特性Bean definition profiles,即可以在<Beans>標簽內(nèi)部再增加</beans>標簽,并且通過參數(shù)profile指定對應(yīng)的激活環(huán)境。

下面通過一個官方文檔中的Demo來加以說明,注意這里非常關(guān)鍵的一步 是確保xsd的schema版本在3.1以上,低于這個版本的是不允許在<beans>里面再定義<beans>的,也就無法使用到procile的屬性配置了,本示例使用的是4.0版本的xsd,高于3.1版本即可。

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

    <bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
        <constructor-arg ref="accountRepository"/>
        <constructor-arg ref="feePolicy"/>
    </bean>

    <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>

    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

加入我們在開發(fā)環(huán)境和生產(chǎn)環(huán)境使用不同類型的數(shù)據(jù)庫,那么就可以通過profile來區(qū)分,但是注冊的bean都是datasource,從而減少上面代碼的適配成本。

下面舉一個在實際工程中使用到的Case來介紹,業(yè)務(wù)背景是預(yù)發(fā)環(huán)境沒有DTS中間件,相應(yīng)的bean不需要加載,對應(yīng)的定時任務(wù)不觸發(fā)也無所謂。

首先,使用xml文件的方式注入一個DTS相關(guān)的client Bean實例。


image.png

接著,定義一個bean-config的配置文件作為所有bean引用文件,在這個文件中我們在<beans> profile屬性中,prepub環(huán)境不加載DTS,而其他環(huán)境(包括測試和線上環(huán)境)都需要加載DTS。


image.png

包含所有環(huán)境的資源配置文件目錄結(jié)構(gòu)如下:


image.png

預(yù)發(fā)環(huán)境對應(yīng)的Spring properties文件中,我們使用下面的配置來激活prepub 的profile。

# Spring profile
spring.profiles.active = prepub

其他環(huán)境我們使用default的profile來激活

# Spring profile
spring.profiles.active = default

備注:當(dāng)然這里做的不夠好的一點是,應(yīng)該根據(jù)所有環(huán)境定義N個profile出來,然后在xml文件的default換成所有其他profile的使用逗號分隔的字符串,這里偷懶啦。

在啟動類的入口處,可以設(shè)置IDEA默認使用的Spring profile,當(dāng)前使用的profile是default。


image.png

技術(shù)棧的切換方案

整體方案

image.png
  1. 本方案的實現(xiàn)基于在maven profile和springboot profile
  2. 產(chǎn)品核心邏輯(非技術(shù)棧相關(guān))抽象成獨立的一個或多個模塊,保證不同技術(shù)棧下是同一套邏輯
  3. 依賴技術(shù)棧相關(guān)的邏輯封裝成單獨的SPI,右不同的技術(shù)棧會做對應(yīng)的適配邏輯。對于SPI機制不了解的可以閱讀先閱讀這篇文章:JAVA SPI機制詳解
  4. 針對每套技術(shù)棧新建單獨的profile,管理其對應(yīng)的SPI實現(xiàn)、配置項、外部依賴等
  5. 在不同的技術(shù)棧環(huán)境下,通過激活對應(yīng)的profile實現(xiàn)技術(shù)棧產(chǎn)品的打包和部署

切換方式

image.png

打包和部署兩個階段是和技術(shù)棧相關(guān)的,可以通過profile進行切換

  1. 打包階段是生成對應(yīng)技術(shù)棧的可行性JAR,可以通過mvn packag -P${profile}的方式進行不同技術(shù)棧的打包,建議這一塊用以適配不同的中間件等外部依賴。
  2. 部署階段是激活對應(yīng)技術(shù)棧的配置并運行可執(zhí)行JAR,可以通過java -jar -Dspring.profiles.active=${profile}的方式進行不同技術(shù)棧的部署,建議這一步用以適配不同的配置。

mvn profile的管控

在集成pom中支持outer和inner兩個版本,在應(yīng)用打包時使用maven profile動態(tài)切換,參數(shù):mvn -P${profile}。

<profiles>
        <profile>
            <id>outer</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <dependencies>
                <!--開源基座,主要是中間件等依賴-->
            </dependencies>
        </profile>
        <profile>
            <id>inner</id>
            <activation>
                <activeByDefault>false</activeByDefault>
            </activation>
            <dependencies>
                <!--內(nèi)部基座,主要是中間件等依賴-->
            </dependencies>
        </profile>
    </profiles>

Spring profile的管控

配置文件分為outer和inter兩個技術(shù)棧的文檔,在應(yīng)用啟動時增加profile參數(shù)動態(tài)切換,參數(shù):Dspring.profiles.active=${profile}。

├── config
│   ├── application.properties
│   ├── application-outer.properties
│   ├── application-innter.properties

參考資料

  1. 官方文檔:SpringBoot XML Profile Configuration
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容