springboot打包成zip部署,並實現優雅停機
更新:本文重點是springboot打包成zip(tar.gz),關於啟停應用可以看《springboot 啟動腳本優化》和《springboot shutdown(停機)》
眾所周知springboot項目,使用springboot插件打包的話,會打包成一個包含依賴的可執行jar,非常方便。只要有java運行環境的電腦上,運行java -jar xxx.jar就可以直接運行項目。
但是這樣的缺點也很明顯,如果我要改個配置,要將jar包中的配置文件取出來,修改完再放回去。這樣做在windows下還比較容易。如果在linux上面就很費勁了。
另外如果代碼中需要讀取一些文件(比如說一張圖片),也被打進jar中,就沒辦法像在磁盤中時一句File file = new File(path)
代碼就可以讀取了。(當然這個可以使用spring的ClassPathResource來解決)。
還有很多公司項目上線后,都是增量發布,這樣如果只有一個jar 的話,增量發布也是很麻煩的事情。雖然我是很討厭這種增量發布的方式,因為會造成線上生產環境和開發環境有很多不一致的地方,這樣在找問題的時候會走很多彎路。很不幸我現在在的項目也是這樣的情況,而且最近接的任務就是用springboot搭建一個定時任務服務,為了維護方便,最后決定將項目打包成zip進行部署。
網上找到了很多springboot打包成zip的文章,不過基本都是將依賴從springboot的jar中拿出來放到lib目錄中,再將項目的jar包中META-INF中指定lib到classpath中。這樣做還是會有上面的問題。
最后我決定自己通過maven-assembly-plugin來實現這個功能。
打包
首先maven-assembly-plugin是將項目打包的一個插件。可以通過指定配置文件來決定打包的具體要求。
我的想法是將class打包到classes中,配置文件打包到conf中,項目依賴打包到lib中,當然還有自己編寫的啟動腳本在bin目錄中。
如圖
maven的target/classes下就是項目編譯好的代碼和配置文件。原來的做法是在assembly.xml中配置篩選,將該目錄下class文件打包進classes中,除class文件打包到conf中(bin目錄文件打包進bin目錄,項目依賴打包進lib目錄)。結果發現conf目錄下會有空文件夾(java包路徑)。
pom.xml
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
assembly.xml
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>package</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<excludes>
<exclude>
${groupId}:${artifactId}
</exclude>
</excludes>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>bin</directory>
<outputDirectory>/bin</outputDirectory>
<fileMode>777</fileMode>
</fileSet>
<fileSet>
<directory>${project.build.directory}/conf</directory>
<outputDirectory>/conf</outputDirectory>
<excludes>
<exclude>**/*.class</exclude>
<exclude>META-INF/*</exclude>
</excludes>
</fileSet>
<fileSet>
<directory>${project.build.directory}/classes</directory>
<outputDirectory>/classes</outputDirectory>
<includes>
<include>**/*.class</include>
<include>META-INF/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>
其實這樣是不影響項目運行的,但是我看着很難受,嘗試了很多方法去修改配置來達到不打包空文件夾的效果。但是都沒成功。
然后我換了個方式,通過maven-resources-plugin插件將配置文件在編譯的時候就復制一份到target/conf目錄下,打包的時候配置文件從conf目錄中取。這樣就可以避免打包空白文件夾到conf目錄中的情況。
pom.xml
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>compile-resources</id>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<includes><!--只對yml文件進行替換-->
<include>*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-resources</id>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<includes><!--只對yml文件進行替換-->
<include>*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
</resource>
</resources>
<outputDirectory>${project.build.directory}/conf</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- springboot maven打包-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
assembly.xml
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>package</id>
<formats>
<format>zip</format>
<format>tar.gz</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<excludes>
<exclude>
${groupId}:${artifactId}
</exclude>
</excludes>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>bin</directory>
<outputDirectory>/bin</outputDirectory>
<fileMode>777</fileMode>
</fileSet>
<fileSet>
<directory>${project.build.directory}/conf</directory>
<outputDirectory>/conf</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.build.directory}/classes</directory>
<outputDirectory>/classes</outputDirectory>
<includes>
<include>**/*.class</include>
<include>META-INF/*</include>
</includes>
</fileSet>
</fileSets>
</assembly>
pom文件中resources插件配置了2個execution,一個是正常往classes中寫配置文件的execution,一個是往conf寫配置文件的execution。這樣做的好處是不影響maven本身的打包邏輯。如果再配置一個springboot的打包插件,也可以正常打包,執行。
執行
原來打包成jar后,只要一句java -jar xxx.jar
就可以啟動項目。現在為多個文件夾的情況下,就要手動指定環境,通過java -classpath XXX xxx.xxx.MainClass
來啟動項目,所以寫了啟動腳本。
run.sh
#!/bin/bash
#Java程序所在的目錄(classes的上一級目錄)
APP_HOME=..
#需要啟動的Java主程序(main方法類)
APP_MAIN_CLASS="io.github.loanon.springboot.MainApplication"
#拼湊完整的classpath參數,包括指定lib目錄下所有的jar
CLASSPATH="$APP_HOME/conf:$APP_HOME/lib/*:$APP_HOME/classes"
s_pid=0
checkPid() {
java_ps=`jps -l | grep $APP_MAIN_CLASS`
if [ -n "$java_ps" ]; then
s_pid=`echo $java_ps | awk '{print $1}'`
else
s_pid=0
fi
}
start() {
checkPid
if [ $s_pid -ne 0 ]; then
echo "================================================================"
echo "warn: $APP_MAIN_CLASS already started! (pid=$s_pid)"
echo "================================================================"
else
echo -n "Starting $APP_MAIN_CLASS ..."
nohup java -classpath $CLASSPATH $APP_MAIN_CLASS >./st.out 2>&1 &
checkPid
if [ $s_pid -ne 0 ]; then
echo "(pid=$s_pid) [OK]"
else
echo "[Failed]"
fi
fi
}
echo "start project......"
start
run.cmd
@echo off
set APP_HOME=..
set CLASS_PATH=%APP_HOME%/lib/*;%APP_HOME%/classes;%APP_HOME%/conf;
set APP_MAIN_CLASS=io.github.loanon.springboot.MainApplication
java -classpath %CLASS_PATH% %APP_MAIN_CLASS%
這樣就可以啟動項目了。
停止
linux下停止tomcat一般怎么做?當然是通過運行shutdown.sh。這樣做有什么好處呢?可以優雅停機。何為優雅停機?簡單點說就是讓代碼把做了一半工作的做完,還沒做的(新的任務,請求)就不要做了,然后停機。
因為做的是定時任務處理數據的功能。試想下如果一個任務做了一半,我給停了,這個任務處理的數據被我標記了在處理中,下次重啟后,就不再處理,那么這些數據就一直不會再被處理。所以需要像tomcat一樣能優雅停機。
網上查詢springboot優雅停機相關資料。主要是使用spring-boot-starter-actuator
,不過很多人說這個在1.X的springboot中可以用,springboot 2.X不能用,需要自己寫相關代碼來支持,親測springboot 2.0.4.RELEASE可以用。pom文件中引入相關依賴。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.loanon</groupId>
<artifactId>spring-boot-zip</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<encoding>UTF-8</encoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- springboot監控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--springboot自定義配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</dependency>
<!--定時任務-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!--發送http請求 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>compile-resources</id>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<includes><!--只對yml文件進行替換-->
<include>*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>copy-resources</id>
<goals>
<goal>resources</goal>
</goals>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<includes><!--只對yml文件進行替換-->
<include>*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
</resource>
</resources>
<outputDirectory>${project.build.directory}/conf</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- springboot maven打包-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
在application.yml中配置一下
application.yml
management: #開啟監控管理,優雅停機
server:
ssl:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
shutdown:
enabled: true #啟用shutdown端點
啟動項目,可以通過POST方式訪問/actuator/shutdown
讓項目停機。
實際線上可能沒辦法方便的發送POST請求,所以寫個類處理下
Shutdown.java
package io.github.loanon.springboot;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClients;
import java.io.IOException;
/**
* 應用關閉入口
* @author dingzg
*/
public class Shutdown {
public static void main(String[] args) {
String url = null;
if (args.length > 0) {
url = args[0];
} else {
return;
}
HttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
try {
httpClient.execute(httpPost);
} catch (IOException e) {
e.printStackTrace();
}
}
}
只要將啟動腳本中的啟動類改成Shutdown類,並指定請求的地址即可。
stop.sh
#!/bin/bash
#Java程序所在的目錄(classes的上一級目錄)
APP_HOME=..
#需要啟動的Java主程序(main方法類)
APP_MAIN_CLASS="io.github.loanon.springboot.MainApplication"
SHUTDOWN_CLASS="io.github.loanon.springboot.Shutdown"
#拼湊完整的classpath參數,包括指定lib目錄下所有的jar
CLASSPATH="$APP_HOME/conf:$APP_HOME/lib/*:$APP_HOME/classes"
ARGS="http://127.0.0.1:8080/actuator/shutdown"
s_pid=0
checkPid() {
java_ps=`jps -l | grep $APP_MAIN_CLASS`
if [ -n "$java_ps" ]; then
s_pid=`echo $java_ps | awk '{print $1}'`
else
s_pid=0
fi
}
stop() {
checkPid
if [ $s_pid -ne 0 ]; then
echo -n "Stopping $APP_MAIN_CLASS ...(pid=$s_pid) "
nohup java -classpath $CLASSPATH $SHUTDOWN_CLASS $ARGS >./shutdown.out 2>&1 &
if [ $? -eq 0 ]; then
echo "[OK]"
else
echo "[Failed]"
fi
sleep 3
checkPid
if [ $s_pid -ne 0 ]; then
stop
else
echo "$APP_MAIN_CLASS Stopped"
fi
else
echo "================================================================"
echo "warn: $APP_MAIN_CLASS is not running"
echo "================================================================"
fi
}
echo "stop project......"
stop
stop.cmd
@echo off
set APP_HOME=..
set CLASS_PATH=%APP_HOME%/lib/*;%APP_HOME%/classes;%APP_HOME%/conf;
set SHUTDOWN_CLASS=io.github.loanon.springboot.Shutdown
set ARGS=http://127.0.0.1:8080/actuator/shutdown
java -classpath %CLASS_PATH% %SHUTDOWN_CLASS% %ARGS%
這樣就可以通過腳本來啟停項目。
其他
關於停機這塊還是有缺點,主要是安全性。如果不加校驗都可以訪問接口,別人也就可以隨便讓我們的項目停機,實際操作過程中我是通過將web地址綁定到127.0.0.1
這個地址上,不允許遠程訪問。當然也可添加spring-security做嚴格的權限控制,主要項目中沒有用到web功能,只是spring-quartz的定時任務功能,所以就將地址綁定到本地才能訪問。而且項目本身也是在內網運行,基本可以保證安全。