基于Profile实现技术栈切换适配多环境部署

问题背景

在商业化的场景下,客户经常会有私有化部署的需求,然而客户的应用环境和基础设施是多样的,可能是纯开源的自建机房,也可能是基于商业化云服务商提供的公有云和私有云,由于这种差异的存在,上层应用产品不可能通过一套实现完全适配所有的技术栈,但是应用产品核心的功能和对外提供的服务都是标准、统一的,和具体的技术栈没有关系,在这个背景下,我们期望把产品核心的非技术栈相关的能力抽象出来,技术栈相关的通过SPI和多技术栈切换能力进行适配和管理,做到一套核心代码+技术栈适配支持在不同的技术栈环境下部署。

这里面涉及到的关键技术就包括maven profile+Spring profile+
Java SPI技术,其他是一些领域设计时模块划分的设计。

基础知识

对于软件开发者而言,经常要控制的就是当前程序是在开发环境运行还是在生产环境运行,主流的控制手段有两种:

  • Maven profile标签
  • Spring Profile机制

下文我们将对这两个特性的基本用法做一些介绍,了解和熟练使用它们是我们进一步实现多环境部署时,应用和中间件适配并且具备良好可扩展性的重要保证。

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只是一种环境控制的参考手段,他的好处是可以在代码级别去控制,具体使用什么根据项目的需要去考量。

名词解释

Environment

在spring中,Environment是对应用环境的抽象(即对Profile和properties的抽象).

Profile

Profile定义了应用环境,即开发, 生产, 测试等部署环境的抽象.

使用方式

编程式

下面通过一个具体的Case来介绍下如何在实际的编程代码中使用Prifle机制。

定义一个servuce接口和三个service的实现类:

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配置讲接口的每个实现添加到容器中:

@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) {
        //在启动容器之前,先指定环境中的profiles参数
        System.setProperty("spring.profiles.active", "project");
        ApplicationContext ctx = new AnnotationConfigApplicationContext(EnvironmentApp.class);
        //当前的profile值是project,所以获取的实现类是A
        A a = ctx.getBean(A.class);
    }
}

@Configuration类中每一个@Bean注解之后都有一个@Profile注解。@Profile中的字符串就标记了当前适配的环境变量,他配合System.setProperty("spring.profiles.active", "project");这一行一起使用。当设定环境参数为wow时,标记了@Profile("project")的方法会被启用,对应的Bean会添加到容器中。而其他标记的Bean不会被添加,当没有适配到任何Profile值时,@Profile("default")标记的Bean会被启用。

Spring Profile的功能就是根据在环境中指定参数的方法来控制@Bean的创建。

@Profile可以用在类上, 还可以用在方法上. 用于在不同的环境加载不同的配置。

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

@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");
    }
}

备注:

  • 用于方法上时, 可能是在不同环境对同一个bean的加载, 由于类中不允许存在签名完全相同的方法, 故可用@Bean("beanName")来定义不同的方法指向同一个bean.
  • 如果一个配置类中有多个@Bean重载方法. 在所有的重载方法上定义的@Profile注解定义应当一致,否则只有第一个声明有效.

Spring profile的激活方式可以有多种:
方式一:设置系统环境变量
Profile的环境变量可以包含多个值。例如:

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

这样环境中就包含了2个Profile的值。对使用的@Profile或profile配置就会被启用。

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

备注:该方式最大的特点适用于纯Java项目,大型的Java工程都不太方便使用该方式,且是在程序运行时修改的,修改profile参数修改改动代码,不符合代码与配置分离的原则,基本属于玩具性质的。

方式二:设置JVM启动参数
与修改环境变量的方式类似,也可以指定同时激活一个或者多个profile。

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

备注:该方式最大的特性是在运行期指定profile。

方式三:SpringBoot properties设置
针对SpringBoot项目,在properties文件中指定,在应用依赖的properties文件中增加spring.profiles.active=test等配置,即可切换场景。

备注:该方式最大的特点是可以能够在打包的时候就指定profile确定启动场景

即在properties文件中使用占位符,在maven的profile中通过filter,通过maven profile来编译时替换。

@profile注解的更多用法

值得一提的是,想很多其他Spring注解一样,@Profile注解可以被当做元注解来使用。这就意味着你可以定义自己自定义的注解,使用@Profile标记,并且Spring仍然可以检测出来,就像它们被直接声明使用的那样。

package com.bank.annotation;

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

这样做的话我们就可以同时使用到@Component注解和我们自定义的@Dev 注解来标记类,而不是直接使用Spring提供的Profile,这样做的好处在于多了一层代理,发生修改的情况下直接去修改@Profile的内容即可。

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

或者,也可以给@Configuration的类加上@Dev的注解,那么这个配置类的下所有的bean都会根据@Profile的指定进行加载了。

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

@profile的原理

Profile特性的实现也不复杂,其实就是实现了Conditional功能(Conditional功能见@Configuration与混合使用一文中关于Conditionally的介绍)。

首先@Profile注解继承实现了@Conditional:

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

然后他的处理类实现了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注解中的值,如果和环境中的一致则添加。

XML定义

根据参考资料1中Spring的官方说明, Spring Framework 3.1 M1 released发布了一个新特性Bean definition profiles,即可以在<Beans>标签内部再增加</beans>标签,并且通过参数profile指定对应的激活环境。

下面通过一个官方文档中的Demo来加以说明,注意这里非常关键的一步 是确保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>

加入我们在开发环境和生产环境使用不同类型的数据库,那么就可以通过profile来区分,但是注册的bean都是datasource,从而减少上面代码的适配成本。

下面举一个在实际工程中使用到的Case来介绍,业务背景是预发环境没有DTS中间件,相应的bean不需要加载,对应的定时任务不触发也无所谓。

首先,使用xml文件的方式注入一个DTS相关的client Bean实例。


image.png

接着,定义一个bean-config的配置文件作为所有bean引用文件,在这个文件中我们在<beans> profile属性中,prepub环境不加载DTS,而其他环境(包括测试和线上环境)都需要加载DTS。


image.png

包含所有环境的资源配置文件目录结构如下:


image.png

预发环境对应的Spring properties文件中,我们使用下面的配置来激活prepub 的profile。

# Spring profile
spring.profiles.active = prepub

其他环境我们使用default的profile来激活

# Spring profile
spring.profiles.active = default

备注:当然这里做的不够好的一点是,应该根据所有环境定义N个profile出来,然后在xml文件的default换成所有其他profile的使用逗号分隔的字符串,这里偷懒啦。

在启动类的入口处,可以设置IDEA默认使用的Spring profile,当前使用的profile是default。


image.png

技术栈的切换方案

整体方案

image.png
  1. 本方案的实现基于在maven profile和springboot profile
  2. 产品核心逻辑(非技术栈相关)抽象成独立的一个或多个模块,保证不同技术栈下是同一套逻辑
  3. 依赖技术栈相关的逻辑封装成单独的SPI,右不同的技术栈会做对应的适配逻辑。对于SPI机制不了解的可以阅读先阅读这篇文章:JAVA SPI机制详解
  4. 针对每套技术栈新建单独的profile,管理其对应的SPI实现、配置项、外部依赖等
  5. 在不同的技术栈环境下,通过激活对应的profile实现技术栈产品的打包和部署

切换方式

image.png

打包和部署两个阶段是和技术栈相关的,可以通过profile进行切换

  1. 打包阶段是生成对应技术栈的可行性JAR,可以通过mvn packag -P${profile}的方式进行不同技术栈的打包,建议这一块用以适配不同的中间件等外部依赖。
  2. 部署阶段是激活对应技术栈的配置并运行可执行JAR,可以通过java -jar -Dspring.profiles.active=${profile}的方式进行不同技术栈的部署,建议这一步用以适配不同的配置。

mvn profile的管控

在集成pom中支持outer和inner两个版本,在应用打包时使用maven profile动态切换,参数: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>
                <!--内部基座,主要是中间件等依赖-->
            </dependencies>
        </profile>
    </profiles>

Spring profile的管控

配置文件分为outer和inter两个技术栈的文档,在应用启动时增加profile参数动态切换,参数:Dspring.profiles.active=${profile}

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

参考资料

  1. 官方文档:SpringBoot XML Profile Configuration
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容