Java Spring Boot 打包部署方案
背景
最近在做一个内网的项目,项目前后端分离,后端用Spring Boot,前端用Ant Design React。部署的环境是学校内网,部署是通过4
G网卡+VPN连接到内网的机器部署的,考虑到Docker需要连接网络且镜像很大,我们采用最原始的部署方式:后端打成jar包传到服务器上,然后java -jar xxx.jar
运行。
这其中就遇到了一些问题:
- 网络慢,打出来的jar包60M左右,要传10分钟。而大部分时候jar包中以来的其他库都是相同的,所以想把依赖的java包分离出来。
- 在机器上部署工序繁杂,大部分都是备份、复制、重启等操作。所以想写个shell脚本减少部署的工作量。
- 配置文件也要从 Spring Boot 的 jar 包中分离出来,方便在服务器上配置
打包配置
项目采用Gradle做依赖管理,我在build.gradle
中写了自定义的任务packTar
和packTarWithoutLibs
,分别可以打出含有依赖库和不含依赖库的tar.gz
包,而这个包解压后的结构是这样的:
app-server-1.0.0
|-bin/
| |-app.sh # 应用脚本,能用于启动、停止、查看状态、查看日志、安装、升级
|-config/
| |-application.yml #配置文件
|-lib/
| |-a.jar # 依赖的jar
| |-b.jar
| |-c.jar
|-app.jar # 应用的jar
以下就是我的Gradle打包配置:
build.gralde:
buildscript {
repositories {
maven {
url 'https://maven.aliyun.com/nexus/content/groups/public'
}
}
// 引入 spring boot 插件
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.3.3.RELEASE"
}
}
group = 'com.xxx'
version = '1.0.12'
// 插件
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
sourceCompatibility = 1.8
configurations {
compileClasspath {
extendsFrom annotationProcessor
}
}
repositories {
maven { url 'https://maven.aliyun.com/nexus/content/groups/public' }
}
// 依赖
dependencies {
// ......
implementation 'org.springframework.boot:spring-boot-starter-web'
// ......
}
// =========================== 打包 ===========================
// 复制依赖的jar
task copyJar(type: Copy) {
fileMode 0755
delete "$buildDir/libs/lib"
from configurations.default
into "$buildDir/libs/lib"
}
// 删除依赖的jar
task removeLibs(type: Delete) {
delete "$buildDir/libs/lib"
}
// 复制配置文件
task copyConfig(type: Copy) {
fileMode 0755
delete "$buildDir/libs/config"
from('src/main/resources') {
include 'application-prod.yml'
}
rename 'application-prod.yml', 'application.yml'
into "$buildDir/libs/config"
}
// 复制脚本
task copyScript(type: Copy) {
fileMode 0755
delete "$buildDir/libs/bin/app.sh"
from('src/main/resources') {
include 'app.sh'
}
into "$buildDir/libs/bin"
}
bootJar {
fileMode 0755
archiveFileName = 'app-server.jar'
// 例外所有的jar
excludes = ["*.jar",'application.yml','application-prod.yml','app.sh']
// lib目录的清除和复制任务
dependsOn copyJar
dependsOn copyConfig
dependsOn copyScript
// 指定classpath
manifest {
attributes "Manifest-Version": 1.0,
"Class-Path": configurations.default.files.collect {"lib/$it.name"}.join(' ')
}
}
// 打Tar包
task packTar(type: Tar) {
dependsOn bootJar
archiveFileName = "app-server-${archiveVersion.get()}.tar.gz"
destinationDirectory = file("$buildDir/dist")
from "$buildDir/libs"
fileMode 0755
}
// 打Tar包
task packTarWithoutLibs(type: Tar) {
dependsOn bootJar
dependsOn removeLibs
archiveFileName = "app-server-nolib-${archiveVersion.get()}.tar.gz"
destinationDirectory = file("$buildDir/dist")
from "$buildDir/libs"
fileMode 0755
}
应用部署脚本包含有前后端部署安装的逻辑,假设应用名称是app,app应用有两个模块server
和browser
,shell脚本如下:
app.sh:
#!/bin/bash
# 程序名
APP=app
# 程序目录
APP_PATH="/usr/local/iot/$APP"
SERVER_NAME="$APP-server"
SERVER_JAR="$APP-server.jar"
TODAY="`date +%Y%m%d`"
# 启动前需要设置的环境变量
setEnvVars(){
# 如果使用默认的glibc的ptmalloc2内存分配器,为避免64M arena问题,应加上这个参数
# export MALLOC_ARENA_MAX=4
# 使用jemalloc内存分配器
export LD_PRELOAD=/usr/local/lib/libjemalloc.so
# jvm崩溃是导出core文件
ulimit -c unlimited
}
# JVM启动参数
JVM_FLAGS=(
'-Xms500M'
'-Xmx1500M'
'-XX:NativeMemoryTracking=detail'
)
#使用说明,用来提示输入参数
usage() {
echo -e "Usage: ./app.sh <command>"
echo -e "commands:"
echo -e " status"
echo -e " start"
echo -e " stop"
echo -e " restart"
echo -e " log"
echo -e " install <module> <version>"
echo -e " update <server-part> <version>"
echo -e "<module>:"
echo -e " server"
echo -e " browser"
echo -e "<server-part>:"
echo -e " jar"
echo -e " config"
echo -e "<version>:"
echo -e " when install, it can install sever or browser module tar.gz file like:"
echo -e " - $APP-server-1.0.11.tar.gz"
echo -e " - $APP-browser-1.0.11.tar.gz
"
echo -e " when update, it can only update server module from tar.gz file like:"
echo -e " - $APP-server-nolib-1.0.11.tar.gz"
exit 1
}
############################### 检查状态 ###############################
is_exist(){
pid=`ps -ef|grep $SERVER_JAR|grep -v grep|awk '{print $2}' `
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}
############################### 重启 ###############################
restart(){
stop
start
}
############################### 启动 ###############################
start(){
is_exist
if [ $? -eq "0" ]; then
echo "${SERVER_JAR} is already running. pid=${pid} ."
else
cd "$APP_PATH/server"
setEnvVars
nohup java ${JVM_FLAGS[*]} -jar $SERVER_JAR >/dev/null 2>&1 &
status
fi
}
############################### 停止 ###############################
stop(){
is_exist
if [ $? -eq "0" ]; then
echo "kill Java process: $pid"
kill -9 $pid
else
echo "${SERVER_JAR} is not running"
fi
}
############################### 查看状态 ###############################
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${SERVER_JAR} is running. Pid is ${pid}"
else
echo "${SERVER_JAR} is NOT running."
fi
}
############################### 查看日志 ###############################
log(){
tail -fn 1000 "$APP_PATH/log/$APP.log"
}
############################### 更新server模块的jar或config ###############################
update(){
serverPart=$1
version=$2
if [ -z "$serverPart" ] ;then
echo "invalid <server-part>, it can't be empty. example: $APP update jar 1.0.11, 'jar' is the <server-part>"
exit 1
fi
if [ "$serverPart" != "jar" ] && [ "$serverPart" != "config" ] ;then
echo "invalid <server-part>, it must be 'config' or 'jar', like: $APP update jar 1.0.11"
exit 1
fi
if [ -z "$version" ] ;then
echo "invalid <version>, it can't be empty. example: $APP update jar 1.0.11, '1.0.11' is the <version>"
exit 1
fi
versionFile="$APP_PATH/version/$APP-server-nolib-$version.tar.gz"
if [ ! -f "$versionFile" ]; then
echo "invalid <version>, corresponding tar.gz file not exist: $versionFile"
exit 1
fi
if [ "$serverPart" == "jar" ]; then
updateJar "$version"
fi
if [ "$serverPart" == "config" ]; then
updateConfig "$version"
fi
}
######################### 将version目录下指版本的tar.gz文件copy到temp目录下解压出来 #########################
extractVersionTagFile(){
version=$1
tarFile="$APP_PATH/version/$SERVER_NAME-nolib-$version.tar.gz"
tempDir="$APP_PATH/temp/$version"
echo -e "copy to tempdir: $tarFile --> $tempDir"
rm -rf "$tempDir"
mkdir -p "$tempDir"
cp "$tarFile" "$tempDir"
echo -e "extract file: $tempDir/$SERVER_NAME-nolib-$version.tar.gz"
cd "$tempDir"
tar -xf "$SERVER_NAME-nolib-$version.tar.gz"
rm "$SERVER_NAME-nolib-$version.tar.gz"
}
######################################### 更新jar文件 #########################################
updateJar(){
# print version
version=$1
echo "update jar of version: $version"
if [ -z "$version" ] ;then
echo "version is empty"
exit 0
fi
# backup old jar
oldJar="$APP_PATH/server/$SERVER_JAR"
if [ -f $oldJar ]; then
echo -e "backup file: $oldJar --> $oldJar.bak"
mv -f "$oldJar" "$oldJar.bak"
fi
# copy tar.gz file in version/ to temp/ and extract it
extractVersionTagFile $version
# copy new jar to server/
echo -e "replace jar: $tempDir/$SERVER_JAR --> $APP_PATH/server/$SERVER_JAR"
cp "$tempDir/$SERVER_JAR" "$APP_PATH/server/$SERVER_JAR"
}
######################################### 更新application.yml配置文件 #########################################
updateConfig(){
# print version
version=$1
echo -e "update config of version: $version"
if [ -z "$version" ] ;then
echo "version is empty"
exit 0
fi
# backup old config file
oldConfig="$APP_PATH/server/config/application.yml"
if [ -f $oldConfig ]; then
echo -e "backup file: $oldConfig --> $oldConfig.$TODAY.bak"
mv -f "$oldConfig" "$oldConfig.$TODAY.bak"
fi
# copy tar.gz file in version/ to temp/ and extract it
extractVersionTagFile $version
# copy new jar to server/config/
echo -e "replace config: $tempDir/config/application.yml --> $APP_PATH/server/config/application.yml"
cp "$tempDir/config/application.yml" "$APP_PATH/server/config/application.yml"
}
######################################### 安装 #########################################
# 语法:
# app install <module> <version>
# 例如:
# app install server 1.0.11
# app install browser 1.0.8
install(){
module=$1
version=$2
versionFile="$APP_PATH/version/$APP-$module-$version.tar.gz"
if [ ! -f "$versionFile" ]; then
echo "file not exist: $versionFile"
exit 1
fi
# 备份
oldModuleDir="$APP_PATH/$module"
if [ -d $oldModuleDir ]; then
echo -e "backup module dir: $oldModuleDir --> $oldModuleDir-bak"
rm -r "$oldModuleDir-bak"
mv -f "$oldModuleDir" "$oldModuleDir-bak"
fi
installDir="$APP_PATH/$module" # example: /usr/local/iot/app/server
mkdir -p "$installDir"
echo -e "copy tar.gz: $versionFile --> $installDir"
cp "$versionFile" "$installDir"
# extact
cd "$installDir"
echo "extract tar.gz: $APP-$module-$version.tar.gz"
tar -xf "$APP-$module-$version.tar.gz"
rm "$APP-$module-$version.tar.gz"
# install server module
if [ "$module" == "server" ]; then
echo "install server completed."
echo "!!!! remember to restart server module: app restart !!!! "
fi
# or install browser module
if [ "$module" == "browser" ]; then
echo "install browser completed. change dir permission 755..."
# change the browser module dir permission
chmod 755 -R "$installDir"
echo "!!!! remember to reload nginx: sudo nginx -s reload !!!! "
fi
}
#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"log")
log
;;
"restart")
restart
;;
"update")
update $2 $3
;;
"install")
echo "install 2: $2, 3: $3"
install $2 $3
;;
*)
usage
;;
esac