Spring Boot的Maven插件(Spring Boot Maven plugin)能够以Maven的方式为应用提供Spring Boot的支持,即为Spring Boot应用提供了执行Maven操作的可能。
Spring Boot Maven plugin能够将Spring Boot应用打包为可执行的jar或war文件,然后以通常的方式运行Spring Boot应用。
Spring Boot Maven plugin的最新版本为2017.6.8发布的1.5.4.RELEASE,要求Java 8, Maven 3.2及以后。
Spring Boot Maven plugin的5个Goals
- spring-boot:repackage,默认goal。在mvn package之后,再次打包可执行的jar/war,同时保留mvn package生成的jar/war为.origin
- spring-boot:run,运行Spring Boot应用
- spring-boot:start,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
- spring-boot:stop,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
- spring-boot:build-info,生成Actuator使用的构建信息文件build-info.properties
配置pom.xml文件
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.0.1.RELEASE</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
mvn package spring-boot:repackage说明
Spring Boot Maven plugin的最主要goal就是repackage,其在Maven的package生命周期阶段,能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original。
基于上述配置,对一个生成Jar软件包的项目执行如下命令
可以看到生成的两个jar文件,一个是*.jar,另一个是*.jar.original。在执行上述命令的过程中,Maven首先在package阶段打包生成*.jar文件;然后执行spring-boot:repackage重新打包,查找Manifest文件中配置的Main-Class属性,如下所示:
Manifest-Version: 1.0 Implementation-Title: gs-consuming-rest Implementation-Version: 0.1.0 Archiver-Version: Plexus Archiver Built-By: exihaxi Implementation-Vendor-Id: org.springframework Spring-Boot-Version: 1.5.3.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.ericsson.ramltest.MyApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Created-By: Apache Maven 3.5.0 Build-Jdk: 1.8.0_131
注意,其中的Main-Class属性值为org.springframework.boot.loader.JarLauncher;
Start-Class属性值为com.ericsson.ramltest.MyApplication。
其中com.ericsson.ramltest.MyApplication类中定义了main()方法,是程序的入口。
通常,Spring Boot Maven plugin会在打包过程中自动为Manifest文件设置Main-Class属性,事实上该属性究竟作用几何,还可以受Spring Boot Maven plugin的配置属性layout控制的,示例如下
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.5.4.RELEASE</version> <configuration> <mainClass>${start-class}</mainClass> <layout>ZIP</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
layout属性的值可以如下:
- JAR,即通常的可执行jar
Main-Class: org.springframework.boot.loader.JarLauncher
- WAR,即通常的可执行war,需要的servlet容器依赖位于WEB-INF/lib-provided
Main-Class: org.springframework.boot.loader.warLauncher
- ZIP,即DIR,类似于JAR
Main-Class: org.springframework.boot.loader.PropertiesLauncher
- MODULE,将所有的依赖库打包(scope为provided的除外),但是不打包Spring Boot的任何Launcher
- NONE,将所有的依赖库打包,但是不打包Spring Boot的任何Launcher
RepackageMojo 的关键方法execute:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.16.RELEASE</version>
</dependency>
@Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.project.getPackaging().equals("pom")) { getLog().debug("repackage goal could not be applied to pom project."); return; } if (this.skip) { getLog().debug("skipping repackaging as per configuration."); return; } //得到项目中的原始的jar,就是使用maven-jar-plugin生成的jar File source = this.project.getArtifact().getFile(); //要写入的目标文件,就是fat jar File target = getTargetFile(); Repackager repackager = new Repackager(source) { //从source中寻找spring boot 应用程序入口的main方法。 @Override protected String findMainMethod(JarFile source) throws IOException { long startTime = System.currentTimeMillis(); try { return super.findMainMethod(source); } finally { long duration = System.currentTimeMillis() - startTime; if (duration > FIND_WARNING_TIMEOUT) { getLog().warn("Searching for the main-class is taking some time, " + "consider using the mainClass configuration " + "parameter"); } } } }; //如果插件中指定了mainClass就直接使用 repackager.setMainClass(this.mainClass); if (this.layout != null) { getLog().info("Layout: " + this.layout); repackager.setLayout(this.layout.layout()); } //寻找项目运行时依赖的jar,过滤后. org.apache.maven.project.DefaultProjectBuilder.resolveDependencies(MavenProject, RepositorySystemSession)获取到依赖的jar对象列表 RepositoryUtils.toArtifacts( artifacts, resolutionResult.getDependencyGraph().getChildren(), Collections.singletonList( project.getArtifact().getId() ), null ); Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); //将Artifact转化成Libraries Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog()); try { LaunchScript launchScript = getLaunchScript(); //进行repackage repackager.repackage(target, libraries, launchScript); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } if (this.classifier != null) { getLog().info("Attaching archive: " + target + ", with classifier: " + this.classifier); this.projectHelper.attachArtifact(this.project, this.project.getPackaging(), this.classifier, target); } else if (!source.equals(target)) { this.project.getArtifact().setFile(target); getLog().info("Replacing main artifact " + source + " to " + target); }
基本上重要的步骤都有注释,应该不难理解的。再来看下面,当然也不是重点,看看就行
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException { if (destination == null || destination.isDirectory()) { throw new IllegalArgumentException("Invalid destination"); } if (libraries == null) { throw new IllegalArgumentException("Libraries must not be null"); } if (alreadyRepackaged()) { return; } destination = destination.getAbsoluteFile(); File workingSource = this.source; //如果源jar与目标jar的文件路径及名称是一致的 if (this.source.equals(destination)) { //将源jar重新命名为原名称+.original,同时删除原来的源jar workingSource = new File(this.source.getParentFile(), this.source.getName() + ".original"); workingSource.delete(); renameFile(this.source, workingSource); } destination.delete(); try { //将源jar变成JarFile JarFile jarFileSource = new JarFile(workingSource); try { repackage(jarFileSource, destination, libraries, launchScript); } finally { jarFileSource.close(); } } finally { if (!this.backupSource && !this.source.equals(workingSource)) { deleteFile(workingSource); } } }
这一步所做的是清理工作,如果源jar同目标文件路径名称等一致,将源jar重命名,原来的文件删除。为目标文件腾位置。下面的重点来了。
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript) throws IOException { JarWriter writer = new JarWriter(destination, launchScript); try { final List<Library> unpackLibraries = new ArrayList<Library>(); final List<Library> standardLibraries = new ArrayList<Library>(); libraries.doWithLibraries(new LibraryCallback() { @Override public void library(Library library) throws IOException { File file = library.getFile(); if (isZip(file)) { if (library.isUnpackRequired()) { unpackLibraries.add(library); } else { standardLibraries.add(library); } } } }); //按照规则写入manifest文件 writer.writeManifest(buildManifest(sourceJar)); Set<String> seen = new HashSet<String>(); writeNestedLibraries(unpackLibraries, seen, writer); //写入源jar中的内容 writer.writeEntries(sourceJar); //写入标准的jar,依赖的jar writeNestedLibraries(standardLibraries, seen, writer); if (this.layout.isExecutable()) { //写入spring boot loader的类 writer.writeLoaderClasses(); } } finally { try { writer.close(); } catch (Exception ex) { // Ignore } }
来源:oschina
链接:https://my.oschina.net/xiaominmin/blog/4450755