目录

一个jar包启动脚本

一个启 jar 包的脚本,你希望它还能有什么功能?

我希望它还能停止重启上线回滚,能查看运行状态,能查看日志,于是有了下面这个脚本。

如何使用?

 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
# 在你放脚本的地方执行下面这条命令下载脚本,本例是放在root目录下
[root@localhost ~]# curl -s https://devops.xwlearn.com/shell/boot-jar.sh > boot-jar.sh

# 查看使用帮助
[root@localhost ~]# bash boot-jar.sh 
========================================================================
 usage: boot-jar.sh [option] ... [start | stop | status | restart | log | upgrade]
 bash boot-jar.sh start        : start service			# 启动服务
 bash boot-jar.sh stop         : stop service			# 停止服务
 bash boot-jar.sh status       : service status			# 查看运行状态
 bash boot-jar.sh log          : service log			# 查看日志
 bash boot-jar.sh restart      : restart service		# 重启服务
 bash boot-jar.sh upgrade/up   : upgrade service		# 更新包
 bash boot-jar.sh rollback/back: rollback service		# 回滚包
========================================================================

# 修改变量

[root@localhost ~]# vim boot-jar.sh 
UPLOAD_PATH=                    		# 上包目录,如/opt/upload
DEPLOY_PATH=            				# jar包安装路径,如/opt/test
PACKAGE_NAME= 							# jar包名,如test-hello-1.0.0.jar
SERVICE_NAME=${PACKAGE_NAME%-*}         # 去掉后缀及版本号,本例为test-hello
LOG_NAME=${SERVICE_NAME}.log            # 日志名,本例为test-hello.log
ACTIVE="test"                           # 启动相应环境配置,如test|pre|pro
PACKAGE_PATH=$DEPLOY_PATH/$PACKAGE_NAME # 安装包路径,本例为 /opt/test/test-hello-1.0.0.jar
LOG_PATH=$DEPLOY_PATH/logs/$LOG_NAME    # 日志路径,本例为 /opt/test/logs/test-hello.log
BACKUP_PATH=$DEPLOY_PATH/backup         # 备份目录,本例为 /opt/test/backup
BACKUP_LAST=$(find $BACKUP_PATH -name "${PACKAGE_NAME}*" | xargs ls -t | head -1)
BACKUP_LAST_NAME=$(basename $BACKUP_LAST)  # 上一个备份包文件名
......

# 按照上面例子,需提前创建好相关目录

上包目录	/opt/upload
jar包安装目录	/opt/test
日志目录	/opt/test/logs
备份目录	/opt/test/backup

# 这里利用花括号的扩展功能快速创建目录及子目录,下面脚本中会有花括号另一个妙用

[root@localhost ~]# mkdir -p /opt/{upload,test/{logs,backup}}
[root@localhost ~]# tree /opt
[root@vultr ~]# tree -L 2 /opt
/opt
├── test
│   ├── backup
│   └── logs
├── upload
...

# 把你的jar包放入 /opt/upload 就行了

[root@localhost ~]# bash boot-jar.sh upload		# 上包并启动服务
[root@localhost ~]# bash boot-jar.sh status		# 查看运行状态

# 还可以把你的脚本目录放入path,就可以全局执行了

[root@localhost ~]# chmod u+x boot-jar.sh
[root@localhost ~]# vim /etc/profile.d/boot-jar.sh
export PATH="$PATH:/root"
[root@localhost ~]# source /etc/profile.d/boot-jar.sh
[root@localhost ~]# boot-jar.sh upload		# 上包并启动服务
[root@localhost ~]# boot-jar.sh status		# 查看运行状态
[root@localhost ~]# boot-jar.sh log		        # 查看日志
......

在展示完整脚本前,我先介绍下它的几个亮点。

如何判断某程序是否安装?

1
2
# 比如 java 
[root@localhost ~]#  command -v java

为什么用 command 判断?而非 which

command 是 bash 内置命令,性能更好; 而 which 是外部命令,性能低些,这还不是主要的,我们来看看这两个命令的定义:

Run command with args suppressing the normal shell function lookup. Only builtin commands or commands found in the PATH are executed If either the -V or -v option is supplied, a description of command is printed …… If the -V or -v option is supplied, the exit status is 0 if command was found, and 1 if not.

由上可知,command是运行系统真正的命令,而非别名。换种说法,你先查看下你的ls命令别名

1
2
[root@localhost ~]# alias ls
alias ls='ls --color=auto'

所以说你键入ls命令时,实际运行的是ls --color=auto,假如有人把ls变成rm -rf /,你是不是该跑路了?

command命令能让你只运行真正的ls,而非其别名。其-v选项是打印给定命令的简要描述,是别名的输出别名,不是的打印其命令路径,内置命令就输出其名称。更重要的是,它明确告诉你,如果找到了命令,就退出0,找不到就退出1,我们就利用这点判断系统是否安装某命令,注意,如果你安装了某应用,但是没有把它加入PATH,那就没法判断了。

再来看看which

shows the full path of (shell) commands

Which returns the number of failed arguments, or -1 when no `programname´ was given

which 主要是用来寻找命令的完整路径,其返回值是失败参数个数,也就是说which后面可跟多个参数,都找到了就返回0,失败一个返回1,失败n个返回n。没有给定参数,返回-1,也就是255,这是因为exit只能使用0~255之间的值,-1 的unsigned值便是255。

看起来好像which也能根据返回状态码判断某程序是否安装?但是which是外置命令,不同系统上的which实现方式是不一样的,很多系统上的which甚至不设置退出状态码,这样不管你找没找到某程序,都返回0,请问你如何判断?

除了command,type和hash这两个内置命令也可以判断,具体请看stackoverflow

写成函数

1
2
3
4
5
function check_cmd() {

    command -v $1  >/dev/null 2>&1 || { echo >&2 " \033[31m  $1 is not installed.  Aborting. \033[0m"; exit 1; }

}

这里的花括号其实是一个匿名函数,所以里面的语句末尾都得加分号。在这里查看更多shell中括号的总结

如何获得运行程序的pid?

通常做法是

1
PID=$(ps -ef |grep -v grep |grep <keyword> |awk '{print $2 }')

这种方法不是不能用,但是我觉得不够优雅,最优雅的方式是使用pgrep命令,上述命令可以改成

1
PID=$(pgrep -f <keyword>)

The pattern is normally only matched against the process name. When -f is set, the full command line is used. The running pgrep or pkill process will never report itself as a match

如何获得上一条命令的最后一个参数?

命令行中可以用!$,这是与history命令相关的特殊变量,脚本中就不可以这样了,通用做法是$_,命令行和脚本中都可以。这个是我自己试出来了的,墙内还没找到相关资料,刚刚为了印证,才翻了墙找了找,果然如我所料。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mkdir test && cd $_    # 创建test目录并进入
# 通常我们进入目录前最好判断该目录是否存在,可以用 []、[[]]以及test来进行条件测试,但知道了$_后,我通通改成下面这样,因为用了 [ ] 和 [[ ]] 就不能使用$_
test -d test && cd $_
# 万一不存在test目录,而我们希望创建后然后又进去,该如何做呢?
test -d test && cd $_ || mkdir $_ && cd $_ 
# 这就有点多余了,我们来优化下
test -d test  || mkdir $_ && cd $_ 
# 扩展,删除文件时我们也会判断下某文件是否存在,比如
[ -f /usr/local/src/justfortest.md ] && rm -f /usr/local/src/justfortest.md
# 这就显得臃肿了,我们来优化下
test -f /usr/local/src/justfortest.md && rm -f $_

完整脚本

  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
#!/bin/bash

UPLOAD_PATH=						# 上包目录
DEPLOY_PATH=						# 安装路径
PACKAGE_NAME=						# 包名
SERVICE_NAME=${PACKAGE_NAME%-*}		# 切割包名,去掉后缀及版本号
LOG_NAME=${SERVICE_NAME}.log		# 日志名
ACTIVE="test"						# 运行环境 test|pre|pro
PACKAGE_PATH=$DEPLOY_PATH/$PACKAGE_NAME	# 安装包路径,如果是war的话, 改为$DEPLOY_PATH/webapps/$PACKAGE_NAME,$DEPLOY_PATH 值为tomcat路径,如/data/tomcat
LOG_PATH=$DEPLOY_PATH/logs/$LOG_NAME	# 日志路径
BACKUP_PATH=$DEPLOY_PATH/backup			# 备份目录
BACKUP_LAST=$(find $BACKUP_PATH -name "${PACKAGE_NAME}*" | xargs ls -t | head -1)
BACKUP_LAST_NAME=$(basename $BACKUP_LAST)

RETVAL="0"

function check_ok() {

        if [ $? != 0 ] 
        then
            echo -e "\033[31m ERROR! $1 \033[0m"
            exit 1
        fi  
}

function check_cmd() {

    command -v $1  >/dev/null 2>&1 || { echo >&2 " \033[31m  $1 is not installed.  Aborting. \033[0m"; exit 1; }

}

function get_pid() {

    PID=$(pgrep -f $PACKAGE_NAME)
}

function backup() {

local TIMESTAMP=$(date +%F-%H-%M)

    test -d $BACKUP_PATH || mkdir -p $_
    test -f $PACKAGE_PATH && mv -b  $_ $BACKUP_PATH/${PACKAGE_NAME}_${TIMESTAMP}   
}

function upload() {


    test -d $UPLOAD_PATH || mkdir -p $_

# 判断上包目录是否存在新包
    if [ -f $UPLOAD_PATH/$PACKAGE_NAME ];then
# 关服务
    stop

check_ok "stop $SERVICE_NAME"
# 判断安装目录是否存在

    test -d $DEPLOY_PATH || mkdir -p $_ 
    backup

    mv $UPLOAD_PATH/$PACKAGE_NAME $DEPLOY_PATH
# 起服务
    start

    else 

	echo -e " \033[31m there is no new package in $UPLOAD_PATH \033[0m"
	exit 1
    fi
}

function rollback() {

# 停服务
stop
# 删除安装目录中的包
test -f $PACKAGE_PATH && rm -f $_
# 回滚上一个包
test -f $BACKUP_PATH/$BACKUP_LAST_NAME && mv $_ $PACKAGE_PATH
# 起服务
start
}



function start() {

# 先判断java是否存在
    check_cmd java

#local  PID=$(pgrep -f $PACKAGE_NAME)

    get_pid

    if [ ${PID} ]; then
    echo -e  " $SERVICE_NAME is running, please run \033[34m $0 stop \033[0m first"
    exit 1
    fi

# 先判断日志文件是否存在
	
    test -d $DEPLOY_PATH/logs || mkdir -p $_ 

    cd $DEPLOY_PATH/logs

    test -f $LOG_NAME || touch $_
# 判断安装目录中是否有包

    test -f $PACKAGE_PATH || { echo "there is no package in $_";exit 1;}

    nohup java -jar $PACKAGE_PATH --spring.profiles.active=$ACTIVE  > $LOG_PATH 2>&1 &

    check_ok "running java -jar ..."

    echo -e  "$SERVICE_NAME \033[34m Started \033[0m"

    echo  -e "查看日志命令:\033[34m $0 log \033[0m 或者 \033[34m  tail -f $LOG_PATH \033[0m"
}

function stop() {

#local  PID=$(pgrep -f $PACKAGE_NAME)

    get_pid

    if [ ${PID} ]; then
    echo $SERVICE_NAME 'Stop Process'[${PID}]
    kill -15 $PID
    fi

    sleep 5
    
# local  PID=$(pgrep -f $PACKAGE_NAME)

    get_pid

    if [ ${PID} ]; then
        echo $SERVICE_NAME' Kill Process'[${PID}]
        kill -9 $PID
    else
        echo $SERVICE_NAME' Stop Success!'
    fi
}

function status(){

#local  PID=$(pgrep -f $PACKAGE_NAME)

    get_pid

    if [ "$PID" != "" ]; then
        echo -e "$SERVICE_NAME is \033[34m Running \033[0m [$PID] "
    else
        echo -e "$SERVICE_NAME is \033[31m Stopped \033[0m "
    fi
}

function log(){

	tail -100f $LOG_PATH
}

function usage(){

   echo "========================================================================================"
   echo -e "\033[34m usage: $0 [option] ... [start | stop | status | restart | log | upgrade]\033[0m"
   echo -e "\033[34m bash $0 start \033[0m       : start service"
   echo -e "\033[34m bash $0 stop \033[0m        : stop service"
   echo -e "\033[34m bash $0 status \033[0m      : service status"
   echo -e "\033[34m bash $0 log \033[0m         : service log"
   echo -e "\033[34m bash $0 restart \033[0m     : restart service"
   echo -e "\033[34m bash $0 upgrade/up\033[0m   : upgrade service"	# 升级服务
   echo -e "\033[34m bash $0 rollback/back\033[0m: rollback service"	# 回滚服务
   echo "========================================================================================"

   

   RETVAL="2"
}

RETVAL="0"

case "$1" in
    start)
	get_pid
	if [ $PID ];then
        echo -e " $SERVICE_NAME is running, please run \033[34m $0 stop \033[0m or \033[34m $0 restart \033[0m"
        else
	start
	fi
        ;;
    stop)
	echo "will stop $SERVICE_NAME"
        stop
        ;;
    restart)
	echo "will stop $SERVICE_NAME"
	stop
	echo "will start $SERVICE_NAME"
	start
        ;;
    upgrade|up)
	echo "will upgrade $SERVICE_NAME"
	upload
        ;;
    log)
        log
        ;;
    status)
        status
        ;;
    rollback|back)
	echo "will rollback to the last version"
        rollback
        ;;
    *)
       usage 
        ;;
esac

exit $RETVAL