使用Assembly打包和部署SpringBoot工程方式
作者:大饼酥
1、Spring Boot项目的2种部署方式
目前来说,Spring Boot 项目有如下 2 种常见的部署方式。
1、一种是使用 docker 容器去部署。将 Spring Boot 的应用构建成一个 docker image,然后通过容器去启动镜像。这种方式在需要部署大规模的应用以及对应用进行扩展时,是非常方便的,属于目前工业级的部署方案,但是需要掌握 docker 的生态圈技术。
2、另一种则是使用 FatJar 直接部署启动(将一个 jar 及其依赖的三方 jar 全部打到一个包中,这个包即为 FatJar)。这是很多初学者或者极小规模情况下的一个简单应用部署方式。
2、Assembly 的优势
上面介绍的 Fatjar 部署方案存在以下缺陷。
1、如果直接构建一个 Spring Boot 的 FatJar 交由运维人员部署的话,整个配置文件都被隐藏到 jar 中,想要针对不同的环境修改配置文件就变成了一件很困难的事情。
2、如果需要启动脚本启动项目的时候,这种直接通过 jar 的方式后续会需要处理很多工作。
而通过 assembly 将 Spring Boot 服务化打包,便能解决上面提到的 2 个问题。
1、使得 Spring Boot 能够加载 jar 外的配置文件。
2、提供一个服务化的启动脚本,这个脚本一般是 shell 或者 windows 下的 bat ,有了 Spring Boot 的应用服务脚本后,就可以很容易的去启动和停止 Spring Boot 的应用了。
3、项目配置
3.1、添加插件
编辑项目的 pom.xml 文件,加入 assembly 打包插件。
<build>
<!-- 指定需要打包编译的文件 -->
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<!-- 指定启动类,将依赖打成外部jar包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.5</version>
<configuration>
<!-- 不打包配置文件 -->
<excludes>
<exclude>*.xml</exclude>
<exclude>*.properties</exclude>
<exclude>*.yml</exclude>
</excludes>
<archive>
<!-- 生成的jar中,不要包含pom.xml和pom.properties这两个文件 -->
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<!-- 是否要把第三方jar加入到类构建路径 -->
<addClasspath>true</addClasspath>
<!-- 外部依赖jar包的最终位置 -->
<classpathPrefix>../lib</classpathPrefix>
<!-- 项目启动类 -->
<mainClass>com.example.TestApplication</mainClass>
</manifest>
<manifestEntries>
<!--MANIFEST.MF 中 Class-Path 加入配置文件目录-->
<Class-Path>../config/</Class-Path>
<Implementation-Title>${project.artifactId}</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
<Build-Time>${maven.build.timestamp}</Build-Time>
</manifestEntries>
</archive>
</configuration>
</plugin>
<!-- 拷贝配置文件到config目录下 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.xml</include>
<include>*.properties</include>
<include>*.yml</include>
</includes>
</resource>
</resources>
<outputDirectory>${project.build.directory}/config</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- 将依赖jar包拷贝到lib目录下 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- 打包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<finalName>${project.artifactId}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<!--具体的配置文件-->
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<!--绑定到maven操作类型上-->
<phase>package</phase>
<!--运行一次-->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.17</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>从上面代码可以看出,把 assembly 的配置都放在 main/assembly 目录下(具体目录里面的文件接下来会创建)。

3.2、编写服务启动/停止/重启脚本
在 assembly 目录下创建一个 bin 文件夹,然后在该文件夹下创建 start.sh 文件,这个是 linux 环境下的启动脚本,具体内容如下。
Tip:开头的项目名称、jar 包名称不用我们手动设置,这里使用参数变量,在项目打包后这些参数自动会替换为 pom 的 profiles 中 properties 的值(assembly 配置文件需要开启属性替换功能),下面另外两个配置文件也同理。
#!/bin/bash
# 项目名称
SERVER_NAME="${project.artifactId}"
# jar名称
APPLICATION="${project.build.finalName}"
# 进入bin目录
cd `dirname $0`
# bin目录绝对路径
BIN_PATH=`pwd`
# 返回到上一级项目根目录路径
cd ..
# 打印项目根目录绝对路径
# `pwd` 执行系统命令并获得结果
BASE_PATH=`pwd`
# 外部配置文件绝对目录,如果是目录需要/结尾,也可以直接指定文件
# 如果指定的是目录,spring则会读取目录中的所有配置文件
CONFIG_PATH=$BASE_PATH/config
LOG_PATH=$BASE_PATH/logs/${SERVER_NAME}
JAVA_OPT="-server -Xms1024m -Xmx1024m -Xmn512m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
APPLICATION_JAR=$(ls $BASE_PATH/boot/$APPLICATION.jar)
PROCESS_ID=$(ps -ef|grep $BASE_PATH/boot/$APPLICATION|grep -v grep|awk '{print $2}')
if [ ! -n "$PROCESS_ID" ];then
echo "$SERVER_NAME服务,进程正在启动中,请稍等..."
file="nohup.out"
if [ ! -f "$file" ]; then
touch "$file"
fi
source /etc/profile
nohup java ${JAVA_OPT} -jar ${APPLICATION_JAR} > /dev/null &
tail -f "${LOG_PATH}/system/info.log"
else
echo "$SERVER_NAME服务,进程(id:$PROCESS_ID)已存在,启动失败"
fi创建 stop.sh 文件,这个是 linux 环境下的停止脚本,具体内容如下。
#!/bin/bash
# 项目名称
APPLICATION="${project.artifactId}"
# 项目启动jar包名称
APPLICATION_JAR="${project.build.finalName}.jar"
# 通过项目名称查找到PID,然后kill -9 pid
PROCESS_ID=$(ps -ef|grep "${APPLICATION_JAR}" |grep -v grep|awk '{print $2}')
if [[ -z "$PROCESS_ID" ]]
then
echo "$APPLICATION服务没有启动,请进一步验证是否需要停止进程"
else
echo "$APPLICATION服务,进程已存在,正在kill进程,进程ID:$PROCESS_ID"
kill -9 ${PROCESS_ID}
echo "$APPLICATION服务,进程$PROCESS_ID停止成功"
fi创建 restart.sh 文件,这个是 linux 环境下的重启脚本,具体内容如下。
#!/bin/bash
# 项目名称
APPLICATION="${project.artifactId}"
# 进入bin目录
cd `dirname $0`
# bin目录绝对路径
BIN_PATH=`pwd`
echo "$APPLICATION服务正在停止"
sh $BIN_PATH/stop.sh
echo "$APPLICATION服务正在重启"
sh $BIN_PATH/start.sh创建 server.sh 文件,这个是 linux 环境下根据指令执行服务启动、停止、重启、查看状态的脚本,具体内容如下。
#!/bin/bash
# 项目名称
SERVER_NAME="${project.artifactId}"
# jar名称
APPLICATION=${project.build.finalName}
# 进入bin目录
cd `dirname $0`
# bin目录绝对路径
BIN_PATH=`pwd`
# 返回到上一级项目根目录路径
cd ..
# 打印项目根目录绝对路径
# `pwd` 执行系统命令并获得结果
BASE_PATH=`pwd`
# 外部配置文件绝对目录,如果是目录需要/结尾,也可以直接指定文件
CONFIG_PATH=$BASE_PATH/config
APP_NAME=$BASE_PATH/boot/$APPLICATION.jar
LOG_PATH=$BASE_PATH/logs/${SERVER_NAME}
JAVA_OPT="-server -Xms512m -Xmx1024m -Xmn512m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
#判断用户是否为lbs
loginUser() {
USER=`whoami`
if [ ${USER} != "lbs" ];then
echo "Please use user 'lbs'"
exit 1
fi
}
loginUser
# 使用说明,用来提示输入参数
usage() {
echo "Usage: sh server.sh [start|stop|restart|status]"
exit 1
}
# 检查程序是否在运行
is_exist() {
# 根据关键字过滤进程PID,关键字由业务方自定义
pid=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}')
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}
# 启动程序
start() {
# 启动程序时,可酌情根据各自服务启动条件做出相应的调整
is_exist
if [ $? -eq "0" ]; then
echo "${SERVER_NAME}服务进程已存在,pid=${pid}"
else
if [ ! -f "$LOG_PATH" ]; then
touch "$LOG_PATH"
fi
source /etc/profile
nohup java $JAVA_OPT -jar $APP_NAME >$LOG_PATH 2>&1 &
# 应以非阻塞方式执行服务启动命令,避免脚本一直阻塞在这里无法退出
# 业务方应对其服务启动时间进行预估,如果从 命令下发到端口开启并对外提供服务 期间的时长超过了1分钟
# 那么业务方则需酌情在此处使用sleep来阻塞脚本,避免因启动时间过长导致持续交付系统误判
# 这个阻塞的时间按照各业务方不同服务自行设定
# 且执行启动命令后,相关的服务日志应存储到指定的文件
echo "${SERVER_NAME}服务进程启动成功"
fi
}
# 停止程序
stop() {
# 服务停止方式及具体方法由业务方指定,避免因直接kill掉进程而影响线上业务
# 并且确保stop函数执行结束后,服务进程不存在,避免影响后续操作
is_exist
if [ $? -eq "0" ]; then
# 如果服务需要平稳的停止,保证业务流无问题,那么可使用不限于循环等方式,保证stop执行后已经停止了该服务
# 否则后续操作可能会影响相关的业务,务必确保stop函数执行结果的准确性
kill -9 $pid
echo "${SERVER_NAME}服务进程停止成功,pid=$pid"
else
echo "${SERVER_NAME}服务没有启动,请进一步验证是否需要停止进程"
fi
}
# 程序状态
status() {
is_exist
if [ $? -eq "0" ]; then
echo "${SERVER_NAME}服务进程正在运行,pid=${pid}"
else
echo "${SERVER_NAME}服务进程没有启动"
fi
}
# 重启程序
restart() {
stop
start
}
# 主方法入口,接收参数可支持start\stop\status\restart\
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac创建 start.bat 文件,这个是 Windows 环境下的启动脚本,具体内容如下。
echo off
:: 项目名称
set APP_NAME=${project.artifactId}
:: jar名称
set APP_JAR=${project.build.finalName}.jar
echo "开始启动服务 %APP_NAME%"
java -Xms512m -Xmx512m -server -jar ../boot/%APP_JAR%
echo "java -Xms512m -Xmx512m -server -jar ../boot/%APP_JAR%"
goto end
:end
pause3.3、创建打包配置文件
最后,我们在 assembly 文件夹下创建一个 assembly.xml 配置文件,具体内容如下。
<assembly>
<!--
必须写,否则打包时会有 assembly ID must be present and non-empty 错误
这个名字最终会追加到打包的名字的末尾,如项目的名字为 test-0.0.1-SNAPSHOT,
则最终生成的包名为 test-0.0.1-SNAPSHOT-assembly.tar.gz
-->
<id>assembly</id>
<!-- 打包的类型,如果有N个,将会打N个类型的包 -->
<formats>
<format>tar.gz</format>
<!--<format>zip</format>-->
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<!--第三方依赖设置-->
<dependencySets>
<dependencySet>
<!-- 不使用项目的artifact,第三方jar不要解压,打包进zip文件的lib目录 -->
<useProjectArtifact>false</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<unpack>false</unpack>
</dependencySet>
</dependencySets>
<!--文件设置-->
<fileSets>
<!--
0755->即用户具有读/写/执行权限,组用户和其它用户具有读写权限;
0644->即用户具有读写权限,组用户和其它用户具有只读权限;
-->
<!-- 将src/main/assembly/bin目录下的所有文件输出到打包后的bin目录中 -->
<fileSet>
<directory>${basedir}/src/main/assembly/bin</directory>
<outputDirectory>bin</outputDirectory>
<fileMode>0755</fileMode>
<!--如果是脚本,一定要改为unix.如果是在windows上面编码,会出现dos编写问题-->
<lineEnding>unix</lineEnding>
<filtered>true</filtered><!-- 是否进行属性替换 -->
</fileSet>
<!-- 将src/main/resources下配置文件打包到config目录 -->
<fileSet>
<directory>${basedir}/src/main/resources</directory>
<outputDirectory>/config</outputDirectory>
<includes>
<include>*.xml</include>
<include>*.properties</include>
<include>*.yml</include>
</includes>
<filtered>true</filtered><!-- 是否进行属性替换 -->
</fileSet>
<!-- 将第三方依赖打包到lib目录中 -->
<fileSet>
<directory>${basedir}/target/lib</directory>
<outputDirectory>lib</outputDirectory>
<fileMode>0755</fileMode>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
<!-- 将项目启动jar打包到boot目录中 -->
<fileSet>
<directory>${basedir}/target</directory>
<outputDirectory>boot</outputDirectory>
<includes>
<include>${project.build.finalName}.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>3.4、打包测试
配置修改完毕后,我们对项目进行打包。将生成的压缩包解压后可以发现,boot 文件夹下项目 jar 包和lib文件夹下第三方 jar 分开了,并且项目 jar 体积也十分小巧。

总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
