springboot应用配置和脚本

words: 3.6k    views:    time: 18min

在springboot中,推荐的方式是直接打包成可执行JAR,这样简单、方便,并且使应用的部署和运行更加一致和可靠,非常适用于应用容器化部署。但我们希望也能兼容旧的打包部署方式,在一些非容器化部署的环境中,可能有直接修改应用配置文件甚至替换jar包的行为。所以我们还是保持以前的打包目录结构,在应用部署之后分为bin、config、lib、log4个子目录,区别只是lib中从原来的多个jar变成了现在的一个jar。

应用配置

  • env.properties

应用中可能存在多个配置文件,并且在启动时需要分别检查修改,这对于运维是无法接受的,因为运维要面对很多应用的部署,他没有精力去关心这些细节。所以可以将部署时需要修改的所有配置项全部提取到一个约定的配置文件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
#### 应用启动参数
jvm_option=-Xms256m -Xmx256m -XX:MetaspaceSize=128m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
#### 启用Agent(on/off)
java_agent=off
#### 开放JMX端口
#jvm_append=-Djava.rmi.server.hostname=0.0.0.0
#jvm_append=-Dcom.sun.management.jmxremote
#jvm_append=-Dcom.sun.management.jmxremote.port=29010
#jvm_append=-Dcom.sun.management.jmxremote.ssl=false
#jvm_append=-Dcom.sun.management.jmxremote.authenticate=false
#### 开放远程debug端口
#jvm_append=-agentlib:jdwp=transport=dt_socket,address=0.0.0.0:18010,server=y,suspend=n

####################################################################### 服务端口
#### 服务Http端口
server_port=8080

####################################################################### DataSource
#### 数据库地址【修改】
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
  • setenv.sh

在真正启动应用之前,我们会将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路径
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);
}
}
}


参考: