Maven打包时Plugin的选择和其它一些知识点

2021-06-17

< view all posts

最近看到不同的项目里在Maven打包时用到了不同的插件,比如Assembly Plugin和Shade Plugin,它们似乎都能起到打包uber jar的作用。还有,使用 project.build.resources 标签和使用Resources Plugin都能够指定资源文件的位置。那么它们之间有什么不同之处呢,这篇笔记进行一些总结。

打包依赖

Assembly和Shade这两个插件在我们需要把项目的依赖一起打包到一个uber jar时使用,网上对于它们俩的区别说的大多不是很明确,这里先说结论:打包含依赖的jar时最好用Shade Plugin,不要用Assembly Plugin。

我们看一下Assembly Plugin的官方文档,里面很明确地提到了一句话:“If your project wants to package your artifact in an uber-jar, the assembly plugin provides only basic support. For more control, use the Maven Shade Plugin.”,也就是说Assembly Plugin在打包依赖方面只能提供基本的功能,官方也推荐我们需要打包uber jar时使用Shade Plugin。

具体的问题在哪里呢?首先,在提供依赖的多个jar包中可能存在具有相同路径、相同文件名的文件,而如果我们使用Assembly Plugin,这些文件会彼此覆盖,最终只会保留一个文件,就可能导致bug。

另外,除了文件可能存在冲突,如果我们进一步把打包的uber jar用作其它项目的依赖,那么一些库可能既存在于uber jar里,同时也被这个项目本身作为依赖引入了,这时这个项目的class path里面就会存在多个同名的类,也可能导致问题

Shade Plugin提供了能够解决上述两个问题的方法。不过要注意的是它也不是自动解决的,而是需要我们去配置的。解决文件冲突彼此覆盖的问题,可以使用Resource Transformers功能,例如对冲突的文件进行合并,或者重定向等。而要解决class pass可能存在多个同名类的问题,可以使用Relocating Classes功能,使uber jar中的一些类不会向外部暴露。

举一个典型的例子,Spring的各个jar包中有很多路径(META-INF)和名称(如spring.schemas等)相同的文件,但是它们的内容实际上是不一样的。Assembly和Shade插件在默认情况下只会保留其中的一个文件,会导致打包得到的uber jar在运行时报错,例如:Unable to locate Spring NamespaceHandler... 等等。对这种问题,就可以使用Shade Plugin的Transformers解决——规定相同文件彼此之间不进行覆盖,而是以追加的方式进行合并:

<transformers>
    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
        <resource>META-INF/spring.handlers</resource>
    </transformer>
    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
        <resource>META-INF/spring.schemas</resource>
    </transformer>
    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
        <resource>META-INF/spring.factories</resource>
    </transformer>
</transformers>

Shade Plugin还有很多其它的配置项。再举一个例子,有时候我们运行打包的uber jar的时候,会遇到这样的报错: java.lang.SecurityException: Invalid signature file digest for Manifest main attributes 。这是因为有一些依赖自带了签名验证,在将jar包都合并到一起之后,之前文件的签名当然不再能验证通过。这时候最简单的方案就是将签名文件在打包时去掉。因此可以加入如下的配置:

<configuration>
    <filters>
        <filter>
            <artifact>*:*</artifact>
            <excludes>
                <exclude>META-INF/*.SF</exclude>
                <exclude>META-INF/*.DSA</exclude>
                <exclude>META-INF/*.RSA</exclude>
            </excludes>
        </filter>
    </filters>
    <!-- Additional configuration. -->
</configuration>

话再说回来,Assembly Plugin既然并不适合打包uber jar,它的功能是什么呢。其实它主要是为了提供各种对项目本体的灵活打包方式,例如将项目的源码、二进制等打包成tar.gz, tar.bz2, zip等格式,可以通过Descriptor对打包的方式进行丰富的自定义配置。

另外,既然提到了打包依赖,也应该说一说Dependency Plugin的 copy-dependencies goal。它的功能并非是将依赖打包到一个jar中,而是将项目用到的依赖都复制到一个指定的目录,类似于“导出”。因为它不做复制以外的任何处理,最为松散,因此实际上提供了最大的自由度。例如,可以导出依赖后手动将build目录打成一个tar包,之后再上传部署

打包资源文件

打包资源文件有两种常见的写法,一种是使用 project.build.resources 标签,另一种是使用maven-resources-plugin。实际上,resources 标签就是由Resource Plugin进行处理的,因此指定build.resource实际上就是在使用Resource Plugin的copy-resources goal。

当我们想把资源文件打包在jar包的外部时,可以这样写:

    <build>
        <resources>
            <resource>
                <directory>${project.basedir}/config</directory>
                <targetPath>${project.build.directory}</targetPath>
            </resource>
        </resources>
    </build>

这里的${...}的值是从哪里来的呢,可以参考这篇官方文档。例如可以从里面里面看到,${project.basedir}实际上是pom文件所在的路径(所以不要随便移动pom文件的位置哦)。

另一个查看这些属性的实际值的方法是使用 mvn help:effective-pom 命令查看。因为maven是COC的(convention over configuration),实际上它自带了一些默认的配置(叫super pom),而effective pom就是一个合并了默认pom、其它父级pom和当前module内pom的pom文件,也就是说,它才是在当前项目或当前module中实际产生作用的完整pom。在effective pom里,我们可以查看各种${...}被解释成的实际路径。

补充一点,打包在jar包外面的资源文件要如何使用呢。例如我们将资源文件放在./resources目录中,并且将其指定为classpath,用-cp xxx.jar:./resources参数运行jar包。目录结构如下:

|- xxx.jar
|- resources/
|   |-config.properties

那么可以用如下的方法访问:

this.getClass().getClassLoader().getResourceAsStream("config.properties");

以及

Thread.currentThread().getContextClassLoader().getResource("config.properties")

或者

this.getClass().getResource("/config.properties")

注意前两个方法可以用相对路径,而第三个方法则必须指定绝对路径(在路径开头有“/”),这是因为第三个方法会以.class文件所在的目录为相对路径,而前两种则是接受相对于classpath root的路径。

关于pluginManagement标签

最后补充一个知识点,之前有同事把插件的配置写在了build.pluginManagement.plugins标签下,导致插件实际上没有起作用。正确的位置应该是把插件放在build.plugins标签下,不需要pluginManagement标签。

那么pluginManagement标签是做什么的呢,其实就像它的名字,它只起到manage的作用:具体来说,如果项目使用了pluginManagement当中的插件,则由pluginManagement来指定插件的version, configuration, executions等。而如果没有使用,里面的插件并不会加入build。

那么进一步,pluginManagement所管理(manage)的范围是多大呢?实际上它用的是一个向下继承且可被覆盖的机制。首先,它可以配置在同一个pom中 build.plugins 标签下引入的插件;其次,在上面提到过,maven自带一些默认的配置,pluginManagement标签可以配置super pom中的这些默认插件,这就是子pom覆盖了父级(super)pom;最后,一个pom中pluginManagement规定的配置,除非被它的子pom覆盖了,否则在子pom中同样有效,这就是向下继承。

网上的一些资料,有的只强调了pluginManagement有覆盖super pom的作用,有的只强调了它能被子pom向下继承。实际上它是同时具有这两种作用,只是向前覆盖的功能写在build.plugins标签里面效果也是一样的,所以比较少被提及。

与之类似,maven的另一组标签dependencyManagement和dependencies在功能上也是同样的区别。