这么个破问题来来回回解决了好几次了,但每每受限于项目的紧急程度和特殊性等原因导致无法进行有效沉淀,这实在有悖于笔者的人生信条。因此这次趁着部门内部的技术栈统一迭代更新,将相关的问题和解决方案进行汇总,形成一站式的解决方案,方便进行之后的更新和优化。
0. 目录
1. 概述
对于之所以需要在SpringBoot项目打包时候将配置文件和依赖置于JAR之外,原因无外乎「减少JAR体积」,「降低因为频繁修改导致的重复编译发包的频率」等问题,以上这些述求在CI/CD基础设施尚未完善的团队中还是比较明显的。
2.「配置文件」外置
经过汇总,我们的项目中的配置文件有外置需求的大约分为以下几种:
- SpringBoot默认配置文件 application.yml 以及相应的 profile yml配置文件。
- Mybatis映射文件
- log配置文件
- ehcache.xml配置文件
- html等静态资源
- 二方库配置文件
下面我们将就以上几个问题逐一给出解决方案:
2.1. SpringBoot默认配置文件application.yml
关于这一条,其实SpringBoot默认就给出了解决方案,SpringBoot程序会按优先级从下面这些路径来加载application.yml
配置文件(相关源码参见:ConfigFileApplicationListener
),注意:以下配置文件优先级从高到低,即 /config 下的拥有最高优先级。
- 当前目录下的/config目录。
- 当前目录下。
- classpath下的/config目录。
- classpath根目录。
因此,要外置配置文件就很简单了:
a. 在jar所在目录新建config文件夹,然后放入配置文件。
b. 或者直接放置配置文件在jar目录 。
当初如果读者愿意挑战COC,也是可以通过命令行指定配置文件位置的。
java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties
或者
java -jar -Dspring.config.location=D:\config\config.properties springboot-0.0.1-SNAPSHOT.jar
2.2. Mybatis映射文件
因为笔者所在部门使用的是Mybatis-Plus来简化MyBatis操作,因此以下只给出MP的相关配置:
# 注意,以下这段配置应该放在对应的profile文件中。
mybatis-plus:
# 放到各自的 profile里去配置
mapper-locations: classpath:org/springblade/**/mapper/*Mapper.xml, file:config/XXX/mybatis/${XXX.db-type}/**/*Mapper_${XXX.db-type}.xml
通过以上配置即可实现Mybatis映射文件外置,具体存放位置是于 JAR 同一目录下的 config/XXX/mybatis/${XXX.db-type}
下。
2.3. log配置文件
关于log配置文件的外置,SpringBoot是提供了相应的配置属性,因此我们只需要进行设置即可。
# 日志配置
logging:
config: file:config/log/logback-${blade.env}.xml
2.4. ehcache.xml配置文件
关于缓存这一块,我们直接使用了Spring对于Cache的抽象,因此也是可以直接使用SpringBoot提供的配置属性。
# 缓存配置 (相关源码参见: CacheAutoConfiguration )
spring:
cache:
ehcache:
config: file:ehcache.xml
2.5. html等静态资源
关于html静态资源外置这块无法像上面那样直接使用配置文件,但工作量也不大,我们只需要创建一个配置类即可。
@Configuration
@EnableWebMvc
public class XXXStaticResourceConfig implements WebMvcConfigurer {
@Value("${spring.profiles.active}")
private String activeProfile;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 这里我们直接写死了路径, 直接COC方式约定静态文件存放位置。
// 这里的判断是兼容 DEV 和 PROD 模式
if (activeProfile.contains(AppConstant.PROD_CODE)) {
registry.addResourceHandler("/**").addResourceLocations("file:config/static/");
registry.addResourceHandler("/static/**").addResourceLocations("file:config/static/");
} else {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
}
2.6. 二方库配置文件
关于二方库这一块,因为涉及到公司财产无法给出直接代码,但基本思路就是 对于需要读取外部资源的二方库,强烈推荐提供接收InputStream
或者Spring中的Resource
参数的方法,给予外部使用者一个自由控制的权利,也能减少因为配置文件存放的特殊性导致不得不进行二方库升级的问题。
另外,对于SpringBoot项目中的资源读取,强烈推荐统一采用Spring提供的ResourceLoader
或ResourcePatternResolver
来实现,具体实现上可以直接使用自动注入的方式获取到相应的实现类,使用者只需要关心API如何使用,无需关注该接口实现类的构造。
@Bean
public ServiceFactory serviceFactory(ResourcePatternResolver rl) {
// 配置文件
final InputStream configIS;
try {
configIS = rl.getResource(properties.getServiceXmlFilePath()).getInputStream();
} catch (IOException e) {
throw ExceptionUtil.wrapRuntime(e);
}
return new XMLSpringServiceFactory(configIS);
}
2.7. profile切换
对于以上显式配置了所在路径的配置文件,明显是不符合研发阶段资源文件存放位置的契约的,也就是我们需要将以上这些配置文件按照profile进行分别的配置,例如开发环境的application-dev.yml
,application-prod.yml
。
<resources>
<resource>
<directory>src/main/resources</directory>
<!-- 交给 ant 插件去做 -->
<excludes>
<!-- 排除目录 -->
<exclude>**/XXX/**/*.*</exclude>
<exclude>**/log/**/*.xml</exclude>
<exclude>**/templates/**/*.*</exclude>
<exclude>**/static/**/*.*</exclude>
<!-- 排除文件 -->
<exclude>**/*.yml</exclude>
<exclude>**/ehcache.xml</exclude>
</excludes>
</resource>
</resources>
<!-- Maven之打包时配置文件替换 : https://www.bbsmax.com/A/Ae5R97LmJQ/ -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<!-- Linux Shell启动脚本 -->
<copy file="${basedir}/doc/script/fatjar/service.sh"
tofile="${project.build.directory}/service.sh" overwrite="true">
<filterset>
<filter token="projectName" value="${bladex.project.id}" />
</filterset>
</copy>
<!-- Windows BAT启动脚本 -->
<copy file="${basedir}/doc/script/fatjar/service.cmd"
tofile="${project.build.directory}/service.bat" overwrite="true">
<filterset>
<filter token="projectName" value="${bladex.project.id}" />
</filterset>
</copy>
<!-- 配置文件/文件夹 -->
<!-- 文件 -->
<copy file="${basedir}/src/main/resources/application.yml"
tofile="${project.build.directory}/config/application.yml"
overwrite="true" />
<copy file="${basedir}/src/main/resources/application-prod.yml"
tofile="${project.build.directory}/config/application-prod.yml"
overwrite="true" />
<copy file="${basedir}/src/main/resources/ehcache.xml"
tofile="${project.build.directory}/config/ehcache.xml" overwrite="true" />
<!-- 文件夹 -->
<copy todir="${project.build.directory}/config/XXX" overwrite="true">
<fileset dir="${basedir}/src/main/resources/XXX/">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${project.build.directory}/config/log" overwrite="true">
<fileset dir="${basedir}/src/main/resources/log/">
<include name="**/*.*" />
</fileset>
</copy>
<copy todir="${project.build.directory}/config/static" overwrite="true">
<fileset dir="${basedir}/src/main/resources/static/">
<include name="**/*.*" />
</fileset>
</copy>
<!-- -->
</tasks>
</configuration>
</execution>
</executions>
</plugin>
3.「依赖」外置
对于上一小节中的"配置文件外置"的需求往往更针对于频繁更新的需要,而本小节所要讨论的"依赖外置"则是为了显著减小最终所生成JAR的体积。
关于"依赖外置"需求的实现,主要还是通过Maven插件来实现。
<!-- 修改 META-INF/MANIFEST.MF; 附加 Class-Path 屬性值 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>config/XXX/lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<!--设置jar所依赖的三方jar包存放的路径 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dep</id>
<phase>prepare-package</phase>
<goals>
<goal>
copy-dependencies
</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/config/XXX/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<configuration>
<finalName>${project.build.finalName}</finalName>
<!--本项配置, 搭配启动参数 loader.path, 可以顶替上面的maven-jar-plugin, 原理参见源码: PropertiesLauncher -->
<layout>ZIP</layout>
<!--这个插件会将所有的包打在一起, 为了做 lib分离, 这里我们故意设置一个不存在的依赖 -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
4. 整体打包
通过以上两步操作,我们将配置文件和依赖分别从JAR中抽离了出来,接下来我们来完成最后一步,将他们整个打包,压缩为一个发布包。
<!-- maven-assembly-plugin ;;; 用于拷贝项目的依赖 ;;; -->
<!-- 使用maven打包时,maven-jar-plugin插件会在target目录下生成可执行的xxx-0.0.1-SNAPSHOT.jar文件,
但是一般生产程序部署时需要打包自定义的格式包,这种情况就可以使用maven-assembly-plugin插件。 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<skip>false</skip>
<!-- 压缩包的名称 + 每个descriptor的id 就是最终的压缩包名称 -->
<finalName>${bladex.project.id}</finalName>
<!-- 打包后的包名是否包含assembly的id名 -->
<appendAssemblyId>true</appendAssemblyId>
<!-- 压缩包最终的输出位置 <outputDirectory> ${build.assemblySavePath} </outputDirectory> -->
<!-- 引用的assembly配置文件,可以用多个,即可以同时打包多个格式的包; -->
<descriptors>
<descriptor>
doc/script/fatjar/assembly.xml
</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<!-- this is used for inheritance merges -->
<id>make-assembly</id>
<!-- bind to the packaging phase -->
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
相应的assembly.xml
如下:
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<!--
-->
<id>bin</id>
<formats>
<format>zip</format>
</formats>
<!-- 配置文件;;;定义 代码文件 中与 输出文件 的映射 -->
<files>
<file>
<!-- 相对目录 POM.xml所在目录 -->
<source>target/${bladex.project.id}.jar</source>
<outputDirectory>.</outputDirectory>
<destName>${bladex.project.id}.jar</destName>
</file>
</files>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>.</outputDirectory>
<includes>
<include>*.bat</include>
<include>*.ps1</include>
<include>*.sh</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/config</directory>
<outputDirectory>./config</outputDirectory>
<includes>
<include>**/*.*</include>
</includes>
</fileSet>
</fileSets>
</assembly>
4. 最终效果图
执行以上操作之后,最终打包出来的成果包结构最外层就只有:
- 只包含业务class文件的JAR 。
- 包含所有配置文件的config/ 。
- 启动脚本service.bat / service.sh 。
另外附上Maven打包命令:
mvn clean package '-Dmaven.test.skip=true' -P prod -T 2C
mvn clean package '-Dmaven.test.skip=true' -P dev -T 2C
5. 最后
- 不是太建议将配置文件进行外置,虽然方便的修改,但及其容易出现"配置飘逸"的情况。
- 普通的属性配置变更也是可以直接通过启动命令行来进行修改的(例如修改端口的
java -jar myproject.jar --server.port=9000
)。当然最好的是统一,不要有的修改在配置文件,有的又在命令行。