maven-shade-plugin遭遇log4j2会出现的问题与解决办法
引言
Java 应用开发时常会有一个 隐隐作痛 的问题, 就是部署的 jar 很臃肿。 大多数情况下,当传输和存储都还未成瓶颈时, 对此我们还是可以容忍的, 哪怕是它已经有几百兆。但在有些场景下, 例如 Android 开发, 我们则需要非常严肃地对待这个问题,将其 jar 的大小降至最低。
Android 开发中使用的打包裁剪工具是 ProGuard。相较 maven-shade-plugin 而言,它裁剪粒度更精细,使得最终裁剪的大小可以做得更小, 只是相应的我们需要付出更多时间才能做到。
作为一个有轻度强迫症的程序员, 我在近期试图应对这个问题的时候, 花了不少时间踩坑, 觉得有必要记录一下。
当 maven-shade-plugin 遭遇 log4j2
我在自己的开发项目中使用 maven-shade-plugin ,一方面实现最终只用交付一个 jar 以提升部署效率;另一方面则是使用它的一个容易被忽略的功能,类级别的无用代码裁剪, 来为最终的 jar 进行瘦身。
使用起来非常简单, 开启一个配置选项即可:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
构建打包下来效果 出奇 的好,实际运行则出现各种 log4j2 相关 ClassNotFoundExcepton
或是 NoClassDefFoundError
。
究其原因,既不不出在 log4j2, 也非 maven-shade-plugin, 而是我忽略代码裁剪基本原理。 它是基于静态代码(或字节码)分析出来的依赖关系,来判断哪些代码是无用,这也就意味着那些借助 反射机制 实现运行时的依赖, 例如 Class.forName
或 ClassLoader.loadClass
, maven-shade-plugin 是无法知道的。
不光是 log4j2, 类似的基于类加载器实现自动装配 插件(Plugins) 或 模块(Modules) 的开发库,都存在这样的问题, 请一定小心警惕!
解决方法
相信聪明的你已经想到了解决办法,很简单,那就是在代码中 声明 依赖, 例如:
abstract class ResolvingLinkes {
static {
System.out.println(org.apache.logging.log4j.core.appender.AppenderSet.class);
...
}
}
完整版本可见ResolvingLinkes.java
通过编写一个运行时并不会用到的ResolvingLinks
, 在其静态初始化块里 声明 要用到的那些类, 这样一来 maven-shade-plugin 可以帮我们保留它们以及它们的依赖。
为什么不使用 maven-shade-plugin 提供的 filter 配置呢?这个问题挺有趣推荐你自己思考一下 😜
实践过程中,去找出哪些类需要声明,常常是耗时且令人崩溃的。
这里分享一个小技巧,类似 log4j2 这样需要自动装配的插件,还是会在某个 隐藏 角落进行声明, 不然它自己怎么知道要如何装配呢?好在这个 隐藏 的角落通常会在 META-INF/
下,例如 META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat
,若是使用 ServiceLoader 机制的会在 META-INF/services/
下。
声明文件的小坑
当出现依赖的多个 jar 中都有同名的声明文件, maven-shade-plugin 在默认配置下是会相互覆盖的,这也就意味着哪怕是代码声明了依赖,还是会遇到实际运行的时候这些类并未被相应的库进行装配的尴尬😅。 解决的办法是使用 transformers, 参考的范例可见 pom.xml。
值不值得
会不会出现费那么大劲,结果发现 “瘦身” 效果很不明显。 理论上讲是 有可能的, 做与不做的预判是要靠经验的,这方面我还没有发现有什么捷径😊。
The End.