基于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- 本方案的实现基于在maven profile和springboot profile
- 产品核心逻辑(非技术栈相关)抽象成独立的一个或多个模块,保证不同技术栈下是同一套逻辑
- 依赖技术栈相关的逻辑封装成单独的SPI,右不同的技术栈会做对应的适配逻辑。对于SPI机制不了解的可以阅读先阅读这篇文章:JAVA SPI机制详解
。 - 针对每套技术栈新建单独的profile,管理其对应的SPI实现、配置项、外部依赖等
- 在不同的技术栈环境下,通过激活对应的profile实现技术栈产品的打包和部署
切换方式
image.png打包和部署两个阶段是和技术栈相关的,可以通过profile进行切换
- 打包阶段是生成对应技术栈的可行性JAR,可以通过
mvn packag -P${profile}
的方式进行不同技术栈的打包,建议这一块用以适配不同的中间件等外部依赖。 - 部署阶段是激活对应技术栈的配置并运行可执行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