Springboot 2.0打包與自定義launch.script


Springboot 2.0打包與自定義launch.script

今天和首架聊到springboot的配置參數問題。他說,這些配置的參數,開發人員拷貝來拷貝去的,很容易出錯,不如我們屏蔽一下吧。

確實,經過工程師的ctrl+c和ctrl+v,大多數重要的參數已經面目全非,完全不是當初的模樣。我見過太多這樣的案例,所以我表示贊同。

為什麼複製粘貼也會出問題?有兩個原因:因為提供者善變;因為使用者驕傲。

我摸著首架的手說:可以開工了

需求和思考

隨著我們對springboot 2.0的瞭解逐步加深,以及部署環境對打包方式的要求變化,我們逐步希望將springboot應用打包成可執行jar並在啟動時更便捷的指定系統參數。

比如在linux環境中,或者將其容器化。

一個可能的方式,是將springboot 打包成可執行jar,然後通過類似於如下方式啟動或者關閉程序:

<code>$> ./application.jar start 

$> ./application.jar stop

$> JAVA_OPTS=-Xmx2g ./application.jar start
/<code>

可以看到這種方式非常的簡潔,但是SpringBoot默認卻不支持。

除此之外,我們可能希望統一管理springboot的打包方式,比如限定日誌目錄、統一指定JVM參數,或者在啟動時額外的從配置中心拉取一些靜態文件等。

這些特殊要求,原生的launch.script無法完成,我們需要擴展launch.scipt或者自定義它。

但是達成這個結果,還是有些困難,因為原生的機制無法支持。

面臨的問題:

  1. 即使我們重新開發了launch.script,藉助<embeddedlaunchscript>,但是這個腳本只能放在項目的本地目錄。如果我們將此腳本嵌入在外部的jar中(主要是不希望所有的項目都重複這個腳本)則可能無法加載。/<embeddedlaunchscript>
  2. 即使我們使用<inlinedconfscript>,但是這種內聯腳本無法支持複雜的腳本邏輯。/<inlinedconfscript>

解決問題的方式:

  1. 為了保留springboot原生的launch.script的絕大部分功能,所以從springboot源碼中copy一份。
  2. 開發一個maven-plugin,將我們自定義的launch.script和自定義的inlined-conf.script文件都放在此插件模塊中。我們的初心是希望此插件可以被眾多項目通用,script統一管理(修改、升級),業務項目只需要引用即可。
  3. 這個maven-plugin,功能非常簡單,就是在package階段,將這兩個script複製到項目的target目錄中。
  4. spring-boot-maven-plugin的配置稍微調整一下,就可以引用到這兩個script了,因為這個兩個script已經通過我們自研的plugin複製到了項目target目錄。

一、maven-plugin開發

從上面可以看出,我們的目的很簡單,就是引用此插件的web項目,在打包時,將兩個script複製到web項目的target目錄中,以供spring-boot-maven-plugin使用。

此插件的處於package階段,主要包含:

  1. LauncherWriterMojo:在package期間,用於複製腳本文件到使用插件的web項目的target目錄。
  2. inlined-conf.script:spring-boot-maven-plugin支持的<inlinedconfscript>配置,內部主要是指定一些springboot可執行jar支持的一些系統參數。/<inlinedconfscript>
  3. launch.script:啟動腳本,底板來自springboot自帶的源碼,我們在內部增加了一些功能,比如拼裝JVM參數、系統參數配置等。

LauncherWriterMojo.java

<code> import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

import java.io.*;

@Mojo(name = "package", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class LauncherWriterMojo extends AbstractMojo {

@Parameter(defaultValue = "${basedir}/target", required = true)
private File outputDirectory;

public void setOutputDirectory(File outputDirectory) {
this.outputDirectory = outputDirectory;
}

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
try {
copy("launch.script");
getLog().info("launch.script has been created.");
copy("inlined-conf.script");
getLog().info("inlined-conf.script has been created.");
} catch (IOException ie) {
throw new MojoExecutionException("launch.script written error!",ie);
}
}

private void copy(String filename) throws IOException{
InputStream inputStream = getClass().getResourceAsStream("/" + filename);
BufferedWriter writer = null;
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
File target = new File(outputDirectory + "/" + filename);
target.setExecutable(true,false);
target.setWritable(true,false);
target.setReadable(true,false);
writer = new BufferedWriter(new FileWriter(target));
while (true) {
String line = reader.readLine();
if (line == null) {
break;

}
writer.write(line);
writer.newLine();
}
writer.flush();
} finally {
if (inputStream != null) {
inputStream.close();
}
if (writer != null) {
writer.close();
}
}
}
}
/<code>

inlined-conf.script

<code> MODE=service; identity=run; PID_FOLDER=./var; LOG_FOLDER=./; LOG_FILENAME=std.out; pidFilename=pid; JAVA_OPTS="$JAVA_OPTS -XX:NewRatio=2 -XX:G1HeapRegionSize=8m -XX:MaxMetaspaceSize=256m -XX:MaxTenuringThreshold=10 -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45 -XX:MaxGCPauseMillis=200 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=6 -XX:GCLogFileSize=32m -Xloggc:./var/run/gc.log.$(date +%Y%m%d%H%M) -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./var/run/java_pid.hprof -Dfile.encoding=UTF-8 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=${JMX_PORT:-0} -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"; mkdir -p var/run
/<code>

簡單描述一下:此行內腳本,主要是降低用戶配置spring-boot-maven-plugin的複雜度,將日誌目錄、PID文件、除了heap大小之外的其他通用JVM參數等,統一指定,這樣使用此插件打包的項目就可以更加規範。

launch.script:代碼copy自spring-boot自帶的,本文你可以認為沒有什麼差別。

二、使用方式

你的web項目或者module的pom.xml

<code> <plugin>  
<groupid>com.??.commons/<groupid>
<artifactid>meteor-spring-boot-maven-plugin/<artifactid>
<version>${meteor-project.version}/<version>
<executions>
<execution>
<goals>

<goal>package/<goal>
/<goals>
/<execution>
/<executions>
/<plugin>
<plugin>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-maven-plugin/<artifactid>
<version>${spring-boot.version}/<version>
<configuration>
<executable>true/<executable>
<embeddedlaunchscriptproperties>
<inlinedconfscript>${basedir}/target/inlined-conf.script/<inlinedconfscript>
/<embeddedlaunchscriptproperties>
<embeddedlaunchscript>${basedir}/target/launch.script/<embeddedlaunchscript>
/<configuration>
<executions>
<execution>
<goals>
<goal>repackage/<goal>
/<goals>
/<execution>
/<executions>
/<plugin>
/<code>

當然為了統一插件的使用,你可能會將上述配置放在一個parent-pom.xml中或者一個parent項目中,其他使用此框架的項目直接引用上述插件而不再指定插件中的配置即可。例如:

<code><build>  
<finalname>application/<finalname>
<plugins>
<plugin>
<groupid>com.??.commons/<groupid>
<artifactid>meteor-spring-boot-maven-plugin/<artifactid>
/<plugin>
<plugin>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-maven-plugin/<artifactid>
/<plugin>
/<plugins>
/<build>
/<code>

使用maven打包之後,生成的application.jar就是可執行文件,且已經將我們自定義的launch.script融入進去,執行時會運行我們自定義的的script。

END

到此為止,我們自定義打包腳本的功能就已經實現了。在一些持續集成工具之中,這種方式被頻繁使用,可以幫助我們在對項目的技術管理、部署管理,有一個統一的視圖。

腳本可以封裝一些常用、容易出現問題的點和麵,提供相應的覆蓋機制,也會將公司的基礎建設進行集成。減少出錯的概率,封裝冗餘的重複。

作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。


分享到:


相關文章: