在springboot中,推荐的方式是直接打包成可执行JAR,这样简单、方便,并且使应用的部署和运行更加一致和可靠,非常适用于应用容器化部署。但我们希望也能兼容旧的打包部署方式,在一些非容器化部署的环境中,可能有直接修改应用配置文件甚至替换jar包的行为。所以我们还是保持以前的打包目录结构,在应用部署之后分为bin、config、lib、log4个子目录,区别只是lib中从原来的多个jar变成了现在的一个jar。
应用配置
应用中可能存在多个配置文件,并且在启动时需要分别检查修改,这对于运维是无法接受的,因为运维要面对很多应用的部署,他没有精力去关心这些细节。所以可以将部署时需要修改的所有配置项全部提取到一个约定的配置文件env.properties中,这样所有应用在部署时都只修改同一个配置文件
:bin/env.properties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 jvm_option =-Xms256m -Xmx256m -XX:MetaspaceSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails java_agent =off server_port =8080 datasource_url =jdbc:postgresql://127.0.0.1:5432/sys-demo datasource_username =postgres datasource_password =postgres
对于应用中的配置文件,一般会用不同的环境来区分。比如我们将配置文件分为dev和prod两套,环境上启动使用prod配置,而在本地运行则使用dev,这样无论开发如何修改提交dev中的配置,也不会影响环境上的打包运行。
:resources/config/application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: config: activate: on-profile: dev import: - dev\application.yml - dev\application-xxx.yml --- spring: config: activate: on-profile: prod import: - prod\application.yml - prod\application-xxx.yml
在真正启动应用之前,我们会将env.properties中所有配置项读取出来,并放到当前环境变量中。然后提供一个config()
调用给开发去进行配置替换,只有开发自己清楚应该使用哪个配置项替换应用中的的哪个配置文件。
具体在解析env.properties并设置环境变量时,会先检测在环境变量中是否已经存在该变量,如果存在则忽略配置的值。这在使用docker部署的场景中非常有用,我们可以直接使用env.properties中的配置项来设置docker容器的环境变量。还有一种常见的场景,我们可以在环境上预先写一个配置文件和启动脚本,然后启动时先将配置文件读取到环境变量,再调用应用的启动脚本(子进程会继承父继承的环境变量),这样如果要升级部署,直接替换应用目录就行。这样相当于对应用的配置做了一个优先级管理,首先环境上已有的配置,然后是应用的env.properties,最后是应用的具体配置文件。
对于配置的替换,提供了一个replace()
方法,作用是可以按照次序来替换yml文件中的配置项。比如replace xxx.yaml key "$value" 2
表示将目标文件中第二个key配置项的值替换成$value。sed替换yaml相比properties文件确实麻烦得多了,但是yaml配置文件也确实具有更强的表达能力。
setenv.sh中还有一些其他方法和配置,其目的也是希望脚步能更结构化一点,将所有需要开发修改的内容全部放在setenv.sh中,然后再作为其他脚本的依赖。这些配置项会在打包、部署、启动时分别进行修改,起到一个记录作用,比如应用的名称版本以及代码的提交记录,部署和启动的时间。
:bin/setenv.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 # ! /bin/bash SETCOLOR_SUCCESS="echo -en \\033[1;32m" SETCOLOR_FAILURE="echo -en \\033[1;31m" SETCOLOR_WARNING="echo -en \\033[1;33m" SETCOLOR_NORMAL="echo -en \\033[0;39m" LogSuccess(){ time=`date "+%D %T"` $SETCOLOR_SUCCESS echo "[$time] [INFO]: $*" $SETCOLOR_NORMAL } LogInfo(){ time=`date "+%D %T"` echo "[$time] [INFO]: $*" $SETCOLOR_NORMAL } LogError(){ time=`date "+%D %T"` $SETCOLOR_FAILURE echo "[$time] [ERROR]: $*" $SETCOLOR_NORMAL } LogWarn(){ time=`date "+%D %T"` $SETCOLOR_WARNING echo "[$time] [WARN]: $*" $SETCOLOR_NORMAL } # java_home= # app_name="unknown" # app_home="/opt/xxx/$app_name" # app_version="unknown" # code_version="unknown" # build_time="unknown" # install_time="unknown" # start_time="unknown" setenv(){ file="$(cd "$(dirname "$0")"; pwd)/env.properties" if [ ! -f $file ];then LogError "setenv failed, env.properties not exist." exit 1 fi temp=temp.conf.`date +%s` cat $file | sed '/^#.*/d' | sed '/^[ \t ]*$/d' | grep = | sed 's/[ \t]*=[ \t]*/=/' > $temp while read line do key=`echo $line | awk -F "=" '{print $1}'` if [ -z $key ]; then continue fi v=`eval echo '$'"$key"` value=`echo $line | awk -F '=' '{ key=$1; sub(/^[ \t]+/, "", key); sub(/[ \t]+$/, "", key); value=substr($0,length(key)+2); print value}'` if [ "jvm_append" == "$key" ]; then export jvm_option="$jvm_option $value" elif [ -z "$v" ]; then export $key="$value" if [ "jvm_option" != "$key" ]; then echo "[Properties]: $key=$value" fi else echo "[Environment]: $key=$v" fi done < $temp rm -f $temp } replace(){ file=$1 key=$2 value=$3 index=$4 if [ -z "$file" ] || [ -z "$key" ] || [ -z "$index" ]; then LogError "Usage: replace <file> <key> <value> <index>" exit 1 fi line_numbers=($(grep -n "$key:" "$file" | awk -F':' '{print $1}' | tr '\n' ' ')) if [ "${#line_numbers[@]}" -lt "$index" ]; then LogError "there are less than $index occurrences of \"$key:\" in $file" exit 1 fi line_number="${line_numbers[$index-1]}" sed -i "${line_number}s|$key:.*$|$key: $value|" "$file" } # 替换配置,env.properties中的配置即环境变量 config(){ ## server.port replace $app_home/config/prod/application.yml port "$server_port" 2 ## datasource replace $app_home/config/prod/application.yml url "$datasource_url" 1 replace $app_home/config/prod/application.yml username "$datasource_username" 1 replace $app_home/config/prod/application.yml password "$datasource_password" 1 } # 安装文件拷贝,工作目录为当前安装包解压目录 install_copy(){ cp -rf bin lib config $app_home if [ -d static ];then cp -rf static $app_home fi } # 卸载时备份操作,工作目录为安装包当前解压目录 uninstall_bak(){ cp -rf $app_home/log . }
安装 安装操作就是一个文件拷贝过程,对于应用的安装目录app_home在setenv.sh中已经确定好了,具体的拷贝操作由开发者自己在install_copy中定义
:install.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 # ! /bin/bash pwd=$(cd "$(dirname "$0")"; pwd) if [ -f $pwd/setenv.sh ];then . $pwd/setenv.sh fi if [ -f $pwd/bin/setenv.sh ];then . $pwd/bin/setenv.sh fi check(){ if [ "unknown" = "$app_version" ];then LogError "install terminated: unknown version." exit 0 fi if [ -d $app_home ];then v_old=`sed -n '/^app_version=.*/p' $app_home/bin/setenv.sh 2>/dev/null | sed 's/app_version=//g' 2>/dev/null` LogError "install terminated: $app_name was already installed, version=$v_old" exit 1 fi } install(){ LogInfo "prepare to install $app_name, version: $app_version" mkdir -p $app_home/log install_copy if [ ! $? == 0 ];then LogError "install terminated unexpectedly: copy failed." if [ ! -z $app_home ] && [ ! "$app_home" = "/" ];then rm -rf $app_home fi exit fi install_time=`date "+%Y-%m-%d %H:%M:%S"` sed -i 's/^install_time=.*/install_time="'"$install_time"'"/' $app_home/bin/setenv.sh cd $app_home find $app_home -type f -name "*.sh" -exec chmod 744 {} \; find $app_home -type f -name "*.sh" -exec dos2unix {} \; 2>/dev/null find $app_home -type f -name "*.xml" -exec dos2unix {} \; 2>/dev/null find $app_home -type f -name "*.properties" -exec dos2unix {} \; 2>/dev/null LogSuccess "$app_name install success." sh $app_home/bin/run.sh start } uninstall(){ if [ -d $app_home ];then if [ ! -z $app_home ] && [ ! $app_home = "/" ];then sh $app_home/bin/run.sh stop uninstall_bak rm -rf $app_home LogSuccess "$app_name cleared[home=$app_home]." fi fi LogSuccess "$app_name uninstall success." } case "$1" in install) check install ;; uninstall) uninstall ;; reinstall) uninstall install ;; *) LogError $"usage: $0 {install|uninstall|reinstall}" exit 1 esac exit 0
启动 如果是在容器中,直接java -jar启动就行,但默认会加一些启动参数,具体如下面所示,这个启动时会在控制台打印
1 -Duser.dir=$app_home -Dspring.profiles.active=prod --spring.config.location=$app_home/config/
在非容器情况下也类似,但是会使用exec或nohup来启动进程。然后通过curl循环检测端口能否访问来判断应用是否启动成功,默认等待80秒,为了防止应用启动失败而导致干等的情况,会在每次检测完端口之后,再检查一下进程pid是否还存活,如果已经挂掉了,则提前结束。
同样的对于停止操作也有一个等待过程,先kill -15通知应用结束,然后默认等待15秒,如果应用还没结束再kill -9
:bin/run.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 # ! /bin/bash pwd=$(cd "$(dirname "$0")"; pwd) if [ ! -f $pwd/setenv.sh ];then echo "can not find setenv.sh." exit 1 else . $pwd/setenv.sh fi java_commond=`which java 2>/dev/null` if [ ! -z "$java_home" ];then java_commond=$java_home/bin/java fi jps_commond=`which jps 2>/dev/null` if [ ! -z "$java_home" ];then jps_commond=$java_home/bin/jps fi version(){ $SETCOLOR_SUCCESS echo "$app_name: $app_version" $SETCOLOR_NORMAL echo "commit: $code_version" echo "build: $build_time" echo "install: $install_time" } status(){ pid=`$jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}'` if [ ! -z "$pid" ];then $SETCOLOR_SUCCESS echo "$app_name Running[pid=$pid]." $SETCOLOR_NORMAL else $SETCOLOR_FAILURE echo "$app_name Dead." $SETCOLOR_NORMAL fi echo "last start time: $start_time" } up(){ if [[ $pwd != $app_home* ]];then LogError "[run.sh up] can only run in $app_name home: $app_home." exit fi setenv config if [ ! $? == 0 ];then exit fi jvm_option="$jvm_option -Duser.dir=$app_home" if [ -d $app_home/config/prod ];then jvm_option="$jvm_option -Dspring.profiles.active=prod" fi if [ "on" = "$java_agent" ];then jvm_option="$jvm_option -javaagent:$app_home/lib/$app_name-$app_version.jar" fi echo "[Arguments]: $jvm_option" mkdir -p $app_home/log s_time=`date "+%Y-%m-%d %H:%M:%S"` echo "################################################################### start: $s_time" >> $app_home/log/boot.log echo "java $jvm_option -jar $app_home/lib/$app_name-$app_version.jar --spring.config.location=$app_home/config/" >> $app_home/log/boot.log ## java $jvm_option -cp "$app_home:$app_home/lib/*" $main_class java $jvm_option -jar $app_home/lib/$app_name-$app_version.jar --spring.config.location=$app_home/config/ } waitStart(){ declare -i counter=0 declare -i max_counter=40 # 80s declare -i total_time=0 SERVER_URL="http://localhost:$server_port" printf "waiting for $app_name startup..." until [[ (( counter -ge max_counter )) || "$(curl -X GET --silent --connect-timeout 1 --max-time 2 --head $SERVER_URL | grep "HTTP")" != "" ]]; do printf "." counter+=1 sleep 2 if [ "$($jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}')" == "" ];then printf "\n" LogError "$app_name start failed, see details in $app_home/log/boot.log" exit 1 fi done printf "\n" total_time=counter*2 if [[ (( counter -ge max_counter )) ]];then LogWarn "$app_name failed to start in $total_time seconds, see details in $app_home/log/boot.log" exit 1; fi s_time=`date "+%Y-%m-%d %H:%M:%S"` pid=`$jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}'` sed -i 's/^start_time=.*/start_time="'"$s_time"'"/' $app_home/bin/setenv.sh LogSuccess "$app_name started in $total_time seconds, pid=$pid" } start(){ if [[ $pwd != $app_home* ]];then LogError "[run.sh start] can only run in $app_name home: $app_home." exit fi echo "[Effective java]: $java_commond" $java_commond -version if [ ! $? == 0 ];then exit fi setenv config if [ ! $? == 0 ];then exit fi jvm_option="$jvm_option -Duser.dir=$app_home" if [ -d $app_home/config/prod ];then jvm_option="$jvm_option -Dspring.profiles.active=prod" fi if [ "on" = "$java_agent" ];then jvm_option="$jvm_option -javaagent:$app_home/lib/$app_name-$app_version.jar" fi echo "[Arguments]: $jvm_option" mkdir -p $app_home/log s_time=`date "+%Y-%m-%d %H:%M:%S"` echo "################################################################### start: $s_time" >> $app_home/log/boot.log echo "$java_commond $jvm_option -jar $app_home/lib/$app_name-$app_version.jar --spring.config.location=$app_home/config/" >> $app_home/log/boot.log exec $java_commond $jvm_option -jar $app_home/lib/$app_name-$app_version.jar --spring.config.location=$app_home/config/ 1>>$app_home/log/boot.log 2>&1 & if [ ! $? == 0 ];then LogWarn "$app_name start failed, see details in $app_home/log/boot.log" exit fi waitStart } waitStop(){ declare -i counter=0 declare -i max_counter=15 # 15s pid=`$jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}'` kill -15 $pid printf "waiting for $app_name shutdown..." until [[ (( counter -ge max_counter )) || "$($jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}')" == "" ]]; do printf "." counter+=1 sleep 1 done printf "\n" if [[ (( counter -ge max_counter )) ]];then kill -9 $pid fi LogSuccess "$app_name stoped[pid=$pid]." } stop(){ if [[ $pwd != $app_home* ]];then LogError "[run.sh stop] can only run in $app_name home: $app_home." exit fi pid=`$jps_commond -l | grep $app_home/lib/$app_name-$app_version.jar | awk '{print $1}'` if [ -z "$pid" ];then LogWarn "$app_name was not running." exit fi s_time=`date "+%Y-%m-%d %H:%M:%S"` echo "################################################################### stop: $s_time">>$app_home/log/boot.log waitStop } restart(){ if [[ $pwd != $app_home* ]];then LogError "[run.sh restart] can only run in $app_name home: $app_home." exit fi stop start } case "$1" in version) version ;; status) status ;; config) setenv config ;; up) up ;; start) start ;; stop) stop ;; restart) restart ;; *) LogError $"usage: $0 {version|status|config|up|start|stop|restart}" exit 1 esac exit 0
启动控制台提示,会打印java环境,生效的配置以及配置是来源于环境变量还是env.properties,对于Jvm参数需要拼接,则单独打印
:sh bin/run.sh start 1 2 3 4 5 6 7 8 9 10 11 12 [Effective java]: /opt/java/jdk-17.0.3.1/bin/java java version "17.0.3.1" 2022-04-22 LTS Java(TM) SE Runtime Environment (build 17.0.3.1+2-LTS-6) Java HotSpot(TM) 64-Bit Server VM (build 17.0.3.1+2-LTS-6, mixed mode, sharing) [Properties]: java_agent=off [Properties]: server_port=19081 [Environment]: datasource_url=jdbc:postgresql://127.0.0.1:5432/sys-demo [Environment]: datasource_username=postgres [Environment]: datasource_password=postgres [Arguments]: -Xms256m -Xmx256m -XX:MetaspaceSize=128m -XX:+HeapDumpOnOutOfMemoryError -Duser.dir=/opt/xxx/sys-demo -Dspring.profiles.active=prod waiting for sys-demo startup........... [04/12/22 17:21:19] [INFO]: sys-demo started in 16 seconds, pid=10091
:sh bin/run.sh status 1 2 sys-xxA Running[pid=10729]. last start time: 04-12-22 17:21:19
查看版本,这些信息在一些问题追溯场景可能会帮上忙,常见的比如确认当前应用运行的代码版本
:sh bin/run.sh version 1 2 3 4 sys-xxA 1.0.0-snapshot commit: remotes/origin/master 88465c9c59ab5255eb5db58f5527efe2137df369 build: 04-12-22 17:21:19 install: 04-12-22 17:21:19
附录 有时我们会感觉应用的配置文件太过臃肿,而有很多的配置项基本都一样,所以想提取出来一个配置文件作为项目中应用共同的默认配置。所以我们给所有的应用添加一个META-INF/info.yml配置文件,然后在ApplicationContextInitializer中进行加载。并将加载的配置项设为最低优先级,使其可以被覆盖。
:ApplicationConfiguration 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ApplicationConfiguration implements ApplicationContextInitializer <ConfigurableApplicationContext> { @Override public void initialize (ConfigurableApplicationContext applicationContext) { ConfigurableEnvironment environment = applicationContext.getEnvironment(); String infoPath = "/META-INF/info.yml" ; Resource infoResource = new ClassPathResource (infoPath); if (!infoResource.exists()){ return ; } try { log.info("prepare to load: META-INF/info.yml" ); List<PropertySource<?>> list = new YamlPropertySourceLoader ().load(infoPath, infoResource); for (PropertySource<?> source : list){ environment.getPropertySources().addLast(source); } }catch (Exception e){ log.error("failed to load: META-INF/info.yml" , e); System.exit(-1 ); } } }
参考: