最近遇到一个问题,自己直接通过Maven去建立一个SpringBoot项目,最终打包的结果却并不是SpringBoot的打包结果,导致Jar包不能正常通过java -jar启动。
项目依赖很简单,如下面的所示:
<?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>
<groupId>com.wanna</groupId>
<artifactId>maven-test-2</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.15</version>
</dependency>
</dependencies>
</project>
只是简单导入了一个SpringBoot的WebStarter和一个SpringBoot的Maven编译器插件。
自己捣鼓了几天都没有结果,在网上无意间发现了一篇文章说,将Maven插件的配置改为如下的方式,就可以。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
实验了一下,最终发现确实可以,能正常打包,也能正常运行Jar包。
可是为什么呢?为什么加上这样的配置之后就行。我之前的SpringBoot项目都是,直接导入插件就行。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
</plugin>
刚刚我们的这个repackage是Maven插件的一个Mojo(RepackageMojo),于是我们需要弄清楚为什么这个Mojo没生效。
于是开始去Debug MAVEN源码,找到MAVEN当中构建Mojo的地方,代码位置如下。
org.apache.maven.lifecycle.internal.DefaultLifecycleMappingDelegate#calculateLifecycleMappings
public Map<String, List<MojoExecution>> calculateLifecycleMappings( MavenSession session, MavenProject project,
Lifecycle lifecycle, String lifecyclePhase )
throws PluginNotFoundException, PluginResolutionException, PluginDescriptorParsingException,
MojoNotFoundException, InvalidPluginDescriptorException
{
/*
* Initialize mapping from lifecycle phase to bound mojos. The key set of this map denotes the phases the caller
* is interested in, i.e. all phases up to and including the specified phase.
*/
Map<String, Map<Integer, List<MojoExecution>>> mappings =
new LinkedHashMap<>();
for ( String phase : lifecycle.getPhases() )
{
Map<Integer, List<MojoExecution>> phaseBindings = new TreeMap<>();
mappings.put( phase, phaseBindings );
if ( phase.equals( lifecyclePhase ) )
{
break;
}
}
/*
* Grab plugin executions that are bound to the selected lifecycle phases from project. The effective model of
* the project already contains the plugin executions induced by the project's packaging type. Remember, all
* phases of interest and only those are in the lifecycle mapping, if a phase has no value in the map, we are
* not interested in any of the executions bound to it.
*/
for ( Plugin plugin : project.getBuild().getPlugins() )
{
for ( PluginExecution execution : plugin.getExecutions() )
{
// if the phase is specified then I don't have to go fetch the plugin yet and pull it down
// to examine the phase it is associated to.
if ( execution.getPhase() != null )
{
Map<Integer, List<MojoExecution>> phaseBindings = mappings.get( execution.getPhase() );
if ( phaseBindings != null )
{
for ( String goal : execution.getGoals() )
{
MojoExecution mojoExecution = new MojoExecution( plugin, goal, execution.getId() );
mojoExecution.setLifecyclePhase( execution.getPhase() );
addMojoExecution( phaseBindings, mojoExecution, execution.getPriority() );
}
}
}
// if not then i need to grab the mojo descriptor and look at the phase that is specified
else
{
for ( String goal : execution.getGoals() )
{
MojoDescriptor mojoDescriptor =
pluginManager.getMojoDescriptor( plugin, goal, project.getRemotePluginRepositories(),
session.getRepositorySession() );
Map<Integer, List<MojoExecution>> phaseBindings = mappings.get( mojoDescriptor.getPhase() );
if ( phaseBindings != null )
{
MojoExecution mojoExecution = new MojoExecution( mojoDescriptor, execution.getId() );
mojoExecution.setLifecyclePhase( mojoDescriptor.getPhase() );
addMojoExecution( phaseBindings, mojoExecution, execution.getPriority() );
}
}
}
}
}
Map<String, List<MojoExecution>> lifecycleMappings = new LinkedHashMap<>();
for ( Map.Entry<String, Map<Integer, List<MojoExecution>>> entry : mappings.entrySet() )
{
List<MojoExecution> mojoExecutions = new ArrayList<>();
for ( List<MojoExecution> executions : entry.getValue().values() )
{
mojoExecutions.addAll( executions );
}
lifecycleMappings.put( entry.getKey(), mojoExecutions );
}
return lifecycleMappings;
}
这里的一个MojoExecution对象,就是一个我们配置的Mojo,如果我们在plugin当中添加了如下这样的配置,那么这里就能构建出来repackage这样的一个MojoExecution,如果我们没有添加,则会缺少了这样的MojoExecution。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
这里,我们开始怀疑,是在哪里悄悄的配置了repackage这个Mojo。
我们找到了SpringBootMavenPlugin的官方文档:
https://docs.spring.io/spring-boot/docs/3.2.2/maven-plugin/reference/htmlsingle/
在第三章"Using the Plugin"当中,有下面这样的介绍。
其中一条告诉了我们,我们如果继承了"spring-boot-starter-parent"的话,将会生成一个repackage goal的Execution(并且指定了executionId为repackage)。
我们找到了spring-boot-starter-parent这个POM,发现其中定义的spring-boot-maven-plugin,确实通过execution标签去指定了repackage的goal。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
这下明白了,默认情况下spring-boot-maven-plugin并不会指定Maven的自动打包,只是定义了repackage这个Mojo,我们通过Intellij可以看到。
如果我们想要在没有spring-boot-starter-parent的情况下使用,就可以添加上如下的配置,去进行开启。(实际上,没有spring-boot-starter-parent的情况是很常见的,比如在一些大公司当中,公司可能会自定义一些中间件,就可能会要求所有的项目统一导入公司的父POM*,此时就会导致我们无法使用spring-boot-starter-parent去作为父工程,就很可能需要用到手动配置repackage)
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>