Springboot 2.0打包与自定义launch.script

今天和首架聊到springboot的配置参数问题。他说,这些配置的参数,开发人员拷贝来拷贝去的,很容易出错,不如我们屏蔽一下吧。

确实,经过工程师的ctrl+c和ctrl+v,大多数重要的参数已经面目全非,完全不是当初的模样。我见过太多这样的案例,所以我表示赞同。

为什么复制粘贴也会出问题?有两个原因:因为提供者善变;因为使用者骄傲。

我摸着首架的手说:可以开工了

需求和思考

随着我们对springboot 2.0的了解逐步加深,以及部署环境对打包方式的要求变化,我们逐步希望将springboot应用打包成可执行jar并在启动时更便捷的指定系统参数。

比如在linux环境中,或者将其容器化。

一个可能的方式,是将springboot 打包成可执行jar,然后通过类似于如下方式启动或者关闭程序:

$> ./application.jar start
$> ./application.jar stop

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

可以看到这种方式非常的简洁,但是SpringBoot默认却不支持。

除此之外,我们可能希望统一管理springboot的打包方式,比如限定日志目录、统一指定JVM参数,或者在启动时额外的从配置中心拉取一些静态文件等。

这些特殊要求,原生的launch.script无法完成,我们需要扩展launch.scipt或者自定义它。

但是达成这个结果,还是有些困难,因为原生的机制无法支持。

面临的问题:

  1. 即使我们重新开发了launch.script,借助<embeddedLaunchScript>,但是这个脚本只能放在项目的本地目录。如果我们将此脚本嵌入在外部的jar中(主要是不希望所有的项目都重复这个脚本)则可能无法加载。
  2. 即使我们使用<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支持的一些系统参数。
  3. launch.script:启动脚本,底板来自springboot自带的源码,我们在内部增加了一些功能,比如拼装JVM参数、系统参数配置等。

LauncherWriterMojo.java

 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();
}
}
}
}

inlined-conf.script

 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<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

简单描述一下:此行内脚本,主要是降低用户配置spring-boot-maven-plugin的复杂度,将日志目录、PID文件、除了heap大小之外的其他通用JVM参数等,统一指定,这样使用此插件打包的项目就可以更加规范。

launch.script:代码copy自spring-boot自带的,本文你可以认为没有什么差别。

二、使用方式

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

 <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>

当然为了统一插件的使用,你可能会将上述配置放在一个parent-pom.xml中或者一个parent项目中,其他使用此框架的项目直接引用上述插件而不再指定插件中的配置即可。例如:

<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>

使用maven打包之后,生成的application.jar就是可执行文件,且已经将我们自定义的launch.script融入进去,执行时会运行我们自定义的的script。

END

到此为止,我们自定义打包脚本的功能就已经实现了。在一些持续集成工具之中,这种方式被频繁使用,可以帮助我们在对项目的技术管理、部署管理,有一个统一的视图。

脚本可以封装一些常用、容易出现问题的点和面,提供相应的覆盖机制,也会将公司的基础建设进行集成。减少出错的概率,封装冗余的重复。

您可能还会对下面的文章感兴趣: