Bootstrap

Java 日志框架解析

Java 日志框架解析

https://zhuanlan.zhihu.com/p/24272450

https://zhuanlan.zhihu.com/p/24275518

https://www.cnblogs.com/chanshuyi/p/something_about_java_log_framework.html

https://www.jianshu.com/p/7b5860be190f

https://dzone.com/articles/how-configure-slf4j-different

https://blog.csdn.net/yangzl2008/article/details/81503579

日志技术框架一览

JUL:JDK中的日志记录工具,也常称为JDKLog、jdk-logging。
Log4J 1.x:一个具体的日志实现框架。
Log4J 2.x:一个具体的日志实现框架,是LOG4J1的下一个版本。
Logback:一个具体的日志实现框架,但其性能更好。
JCL:一个日志门面,提供统一的日志记录接口,也常称为commons-logging。
SLF4J:一个日志门面,与JCL一样提供统一的日志记录接口,可以方便地切换看具体的实现框架。
JUL、LOG4J1、LOG4J2、LOGBACK是日志实现框架,而JCL、SLF4J是日志实现门面

Java Util Log

简称JUL,是JDK 中自带的log功能。虽然是官方自带的log lib,JUL的使用确不广泛。主要原因:

  1. JUL从JDK1.4 才开始加入(2002年),当时各种第三方log lib已经被广泛使用了
  2. JUL早期存在性能问题,到JDK1.5上才有了不错的进步,但现在和Logback/Log4j2相比还是有所不如
  3. JUL的功能不如Logback/Log4j2等完善,比如Output Handler就没有Logback/Log4j2的丰富,有时候需要自己来继承定制,又比如默认没有从ClassPath里加载配置文件的功能

Log4j 1.x

Log4j 是在 Logback 出现之前被广泛使用的 Log Lib, 由 Gülcü 于2001年发布,后来成为Apache 基金会的顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,也产生了Log4c, Log4s, Log4perl 等到其他语言的移植。Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。

Commons Logging

简称JCL,是Apache下面的项目。JCL 是一个Log Facade,只提供 Log API,不提供实现,然后有 Adapter 来使用 Log4j 或者 JUL 作为Log Implementation。

就像之前所说,JDK现在带了自己的JUL,然后又有第三方的 Log4j 等日志库存在,不同的项目可能各自使用了不同的日志库。如果你的项目依赖的其他 lib 各自使用了不同的日志库,你想控制日志行为,就需要针对每个日志库都写一个配置文件,是不是很酸爽?

然后这个时候 JCL 就出现了。在程序中日志创建和记录都是用JCL中的接口,在真正运行时,会看当前ClassPath中有什么实现,如果有Log4j 就是用 Log4j, 如果啥都没有就是用 JDK 的 JUL。

这样,在你的项目中,还有第三方的项目中,大家记录日志都使用 JCL 的接口,然后最终运行程序时,可以按照自己的需求(或者喜好)来选择使用合适的Log Implementation。如果用Log4j, 就添加 Log4j 的jar包进去,然后写一个 Log4j 的配置文件;如果喜欢用JUL,就只需要写个 JUL 的配置文件。如果有其他的新的日志库出现,也只需要它提供一个Adapter,运行的时候把这个日志库的 jar 包加进去。

到这个时候一切看起来都很简单,很美好。接口和实现做了良好的分离,在统一的JCL之下,不改变任何代码,就可以通过配置就换用功能更强大,或者性能更好的日志库实现。

这种简单美好一直持续到SLF4J出现。

SLF4J/Logback

SLF4J(The Simple Logging Facade for Java) 和 Logback 也是Gülcü 创立的项目,其创立主要是为了提供更高性能的实现。其中,SLF4j 是类似于JCL 的Log Facade,Logback 是类似于Log4j 的 Log Implementation。

之前已经说过,Apache 有了个JCL,用来做各种Log lib统一的接口,如果 Gülcü 要搞一个更好的 Log 实现的话,直接写一个实现就好了,为啥还要搞一个和SLF4J呢?

原因是Gülcü 认为 JCL 的 API 设计得不好,容易让使用者写出性能有问题的代码。

比如在用 JCL 输出一个 debug 级别的 log:

logger.debug("start process request, url:" + url);

这个有什么问题呢?一般生产环境 log 级别都会设到 info 或者以上,那这条 log 是不会被输出的。然而不管会不会输出,这其中都会做一个字符串连接操作,然后生产一个新的字符串。如果这条语句在循环或者被调用很多次的函数中,就会多做很多无用的字符串连接,影响性能。

所以 JCL 的最佳实践推荐这么写:

if (logger.isDebugEnabled()) {
    logger.debug("start process request, url:" + url);
}

然而开发者常常忽略这个问题或是觉得麻烦而不愿意这么写。所以SLF4J提供了新的API,方便开发者使用:

logger.debug("start process request, url:{}", url);

这样的话,在不输出 log 的时候避免了字符串拼接的开销;在输出的时候需要做一个字符串format,代价比手工拼接字符串大一些,但是可以接受。

而 Logback 则是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。

现在事情变复杂了。我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。Gülcü 是个追求完美的人,他决定让这些Log之间都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接:

可以看到甚至 Log4j 和 JUL 都可以桥接到SLF4J,再通过 SLF4J 适配到到 Logback!

在这里需要注意不能搞出循环的桥接,比如下面这些依赖就不能同时存在:

  1. jcl-over-slf4j 和 slf4j-jcl
  2. log4j-over-slf4j 和 slf4j-log4j12
  3. jul-to-slf4j 和 slf4j-jdk14

总感觉事情在变得更麻烦呢!

Log4j2

现在有了更好的 SLF4J 和 Logback——你会想事情到这里总该了解了吧,让他们慢慢取代JCL 和 Log4j 好了。

然而维护 Log4j 的人不这样想,他们不想坐视用户一点点被 SLF4J /Logback 蚕食,继而搞出了 Log4j2。

Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。

Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core

在项目中怎么使用日志库

现在好了,我们有了三个流行的Log 接口和四个流行的Log实现

1. 总是使用Log Facade,而不是具体Log Implementation

正如之前所说的,使用 Log Facade 可以方便的切换具体的日志实现。而且,如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过 Adapter 转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,就麻烦的多了。

具体来说,现在推荐使用 Log4j-API 或者 SLF4j,不推荐继续使用 JCL。

2. 只添加一个 Log Implementation依赖

毫无疑问,项目中应该只使用一个具体的 Log Implementation,建议使用 Logback 或者Log4j2。如果有依赖的项目中,使用的 Log Facade不支持直接使用当前的 Log Implementation,就添加合适的桥接器依赖。

3. 具体的日志实现依赖应该设置为optional和使用runtime scope

在项目中,Log Implementation的依赖强烈建议设置为runtime scope,并且设置为optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;

Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不适用Log Facade中的类。

4. 如果有必要, 排除依赖的第三方库中的Log Impementation依赖

这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的,比如他依赖了Log4j,你想使用Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和Log实现,也极容易形成环。

这种情况下,推荐的处理方法,是使用exclude来排除所有的这些Log实现和桥接器的依赖,只保留第三方库里面对Log Facade的依赖。

比如阿里的JStorm就没有很好的处理这个问题,依赖jstorm会引入对Logback和log4j-over-slf4j的依赖,如果你想在自己的项目中使用Log4j或其他Log实现的话,就需要加上excludes:

<dependency>
    <groupId>com.alibaba.jstorm</groupId>
    <artifactId>jstorm-core</artifactId>
    <version>2.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </exclusion>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>

5. 避免为不会输出的log付出代价

Log库都可以灵活的设置输出界别,所以每一条程序中的log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。

先看两个有问题的写法:

logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));

第一条是直接做了字符串拼接,所以即使日志级别高于debug也会做一个字符串连接操作;第二条虽然用了SLF4J/Log4j2 中的懒求值方式来避免不必要的字符串拼接开销,但是toJson()这个函数却是都会被调用并且开销更大。

推荐的写法如下:

logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2
    logger.debug("receive request: " + toJson(request)); 
}

6. 日志格式中最好不要使用行号,函数名等字段

原因是,为了获取语句所在的函数名,或者行号,log库的实现都是获取当前的stacktrace,然后分析取出这些信息,而获取stacktrace的代价是很昂贵的。如果有很多的日志输出,就会占用大量的CPU。在没有特殊需要的情况下,建议不要在日志中输出这些这些字段。

最后, log中不要输出稀奇古怪的字符!

部分开发人员为了方便看到自己的log,会在log语句中加上醒目的前缀,比如:

logger.debug("========================start process request=============");

虽然对于自己来说是方便了,但是如果所有人都这样来做的话,那log输出就没法看了!正确的做法是使用grep 来看只自己关心的日志。

slf4j使用

slf4j绑定不同日志实现

测试类
package log;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogTest {
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        LOGGER.debug("test slf4j:{}","debug");
        LOGGER.info("test slf4j:{}","info");
        LOGGER.warn("test slf4j:{}","warn");
        LOGGER.error("test slf4j:{}","error");
    }
}

pom.xml只引入slf4j-api

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.5</version>
    </dependency>
</dependencies>

上面pom.xml文件只引入slf4j-api,LogTest不会有任何输出,出现如下警告(缺少日志实现)

SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder”.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

pom.xml添加slf4j-simple依赖

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.5</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.5</version>
    </dependency>
</dependencies>

上面pom.xml添加了日志实现slf4j-simple,slf4j-simple日志框架将所有的事件输出到System.err。只有INFO级别以上的消息才被打印

日志输出如下

[main] INFO log.LogTest - test slf4j:info
[main] WARN log.LogTest - test slf4j:warn
[main] ERROR log.LogTest - test slf4j:error

要打印debug级别日志,需要添加-Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG参数

绑定log4j实现

使用slf4j-log4j12日志实现,绑定log4j 1.2版本,一个广泛使用的日志框架,此时需要在src/main/resources路径下添加log4j.properties

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.5</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.5</version>
    </dependency>
</dependencies>

log4j.properties

log4j.rootLogger=DEBUG, STDOUT
log4j.logger.log=INFO
log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender
log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout
log4j.appender.STDOUT.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

日志输出

INFO [main] (LogTest.java:10) - test slf4j:info
WARN [main] (LogTest.java:11) - test slf4j:warn
ERROR [main] (LogTest.java:12) - test slf4j:error

使用jdk日志
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.5</version>
</dependency>

绑定java.util.logging,也被称为JDK 1.4日志

使用logback日志实现
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

此时需要在src/main/resources路径添加logback.xml

logback.xml

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <logger name="deng" level="DEBUG"/>
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

java日志框架冲突

日志混乱

日志系统几乎是所有库都会用到的一个功能,每个库由于早期的技术选型和开发者喜好等原因,通常各自都依赖着不同的日志体系,一旦引入多个日志框架,轻则会导致程序中有好几套日志系统同时工作,日志输出混乱,重则会导致项目日志系统初始化死锁,项目无法启动。

直接去除

项目中引入了多个日志框架,如果我们直接暴力的排除其他日志框架,可能导致第三方库在调用日志接口时抛出ClassNotFound异常,这里就需要用到日志系统桥接器。

日志系统桥接器

日志系统桥接器说白了就是一种偷天换日的解决方案。

比如log4j-over-slf4j,即log4j -> slf4j的桥接器,这个库定义了与log4j一致的接口(包名、类名、方法签名均一致),但是接口的实现却是对slf4j日志接口的包装,即间接调用了slf4j日志接口,实现了对日志的转发。

但是,jul-to-slf4j是个意外例外,毕竟JDK自带的logging包排除不掉啊,其实是利用jdk-logging的Handler机制,在root logger上install一个handler,将所有日志劫持到slf4j上。要使得jul-to-slf4j生效,需要执行

SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

spring boot中的日志初始化模块已经包括了该逻辑,故无需手动调用。在使用其他框架时,建议在入口类处的static{ }区执行,确保尽早初始化。

日志桥接器使用

log4j -> slf4j,slf4j -> log4j两个桥接器同时存在会出现什么情况?

互相委托,无限循环,堆栈溢出。

slf4j -> logback,slf4j -> log4j两个桥接器同时存在会如何?

两个桥接器都会被slf4j发现,在slf4j中定义了优先顺序,优先使用logback,仅会报警,发现多个日志框架绑定实现;但有一些框架中封装了自己的日志facade,如果其对绑定日志实现定义的优先级顺序与slf4j不一致,优先使用log4j,那整个程序中就有两套日志系统在工作。

为达到统一使用slf4j & logback的目的,必须要做4件事:

  1. 引入slf4j & logback日志包和slf4j -> logback桥接器;
  2. 排除common-logging、log4j、log4j2日志包;
  3. 引入jdk-logging -> slf4j、common-logging -> slf4j、log4j -> slf4j、log4j2 -> slf4j桥接器;
  4. 排除slf4j -> jdk-logging、slf4j -> common-logging、slf4j -> log4j、slf4j -> log4j2桥接器

*ps*:log4j2桥接器由log4j2提供,其他桥接器由slf4j提供。

如果再严谨一点,还要排除掉slf4j-simple、slf4j-nop两个框架,不过这两个一般没人用。

maven首先遵循路径最短优先原则,即直接引入最优先,传递依赖层级越浅,越优先。若依然无法仲裁,则遵循优先声明原则,在pom中声明靠前的优先

如何进行日志系统转换?

在实际的日志转换过程中,SLF4J其实是充当了一个中介的角色。例如当我们一个项目原来是使用LOG4J进行日志记录,但是我们要换成LogBack进行日志记录。

此时我们需要先将LOG4J转换成SLF4J日志系统,再从SLF4J日志系统转成LogBack日志系统。

从日志框架转向SLF4J

  • jul-to-slf4j:jdk-logging到slf4j的桥梁
  • log4j-over-slf4j:log4j1到slf4j的桥梁
  • jcl-over-slf4j:commons-logging到slf4j的桥梁

从SLF4J转向具体的日志框架

  • slf4j-jdk14:slf4j到jdk-logging的桥梁
  • slf4j-log4j12:slf4j到log4j1的桥梁
  • log4j-slf4j-impl:slf4j到log4j2的桥梁
  • logback-classic:slf4j到logback的桥梁
  • slf4j-jcl:slf4j到commons-logging的桥梁

例如我们一开始使用的是 Log4J 日志框架,现在我们希望转成 LogBack 框架,那么我们首先需要加入 log4j-over-slf4j.jar 将 Log4J 转成 SLF4J,之后再加入 logback-classic.jar 将 SLF4J 转成 LogBack。

jar冲突的解决方法

冲突实例

es api组件依赖guava18.0,spark项目由于业务需要写入es所以需要依赖es ,但spark项目的环境又需要依赖guava14.0,如果换成高版本可能会报错,这个决定了你不能都使用统一的低版本或者高版本来规避此问题,因此必须面对现实。

导致异常的原因简单说下:

spark环境首先启动,导致jvm里面已经加载了guava14.0,这个时候jvm不会加载es依赖的guava18.0,而当es初始化的时候,恰巧需要使用guava18.0新版本的api,而这个api在14.0里面却并不存在,这个时候就会发生异常,就是我们常看到的:

java.lang.NoSuchMethodException

在深入了解一下,为什么会发生这个异常?是因为java里面的类加载器是双亲委派模式,一个类只需要在双亲委派模式下正常加载过(唯一全限定名:包名+类名)一次,就不会重复加载,从而引发了上面的问题。想要解决这种问题,靠重新再写一个类加载器是不现实的,因为重新写一个类加载器,不遵守双亲委派模式,就相当于把环境隔离了,技术上可行,但没法解决问题,如果A加载器加载的类,要调B加载器里面的类,或者B调A,会引发新的依赖问题。

那么如何比较优雅的解决这种进退两难的困境问题呢?maven-shade-plugin的出现,就可以解决这个问题的。它的解决手段也非常简单,前面说明JVM类加载器只会加载某个类一次,是通过全路径的包名+类名来区分做到的,我们要想加载不同版本的同一个类,有两种简单的方式,第一种改类名,第二种改包名。综合考虑来说改包名是最为妥当的一种方式,如果改了类名,那么要修改和替换的地方就要比改包名复杂的多了,不仅类调用的每一个地方都要替换,另外包名导入的地方也需要替换(.*导入除外,现实中不建议用这种方式),而修改包名,只需要把每一个依赖该类的类文件头部导入路径调换成新的即可,文件里面的类无需修改。

通过maven-shade-plugin插件的功能,就可以很容易做到这件事。

解法是:

单独为es的依赖创建一个maven项目,然后pom里面引入依赖的es组件,并对es组件里面依赖的guava的包名和部分组件,进行shade修改,如下:

<groupId>es.shade</groupId>
<artifactId>es-shade</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
	<elasticsearch.version>2.3.4</elasticsearch.version>
</properties>
 
<dependencies>
	<dependency>
		<groupId>org.elasticsearch</groupId>
		<artifactId>elasticsearch</artifactId>
		<version>${elasticsearch.version}</version>
	</dependency>
</dependencies>

<build>       
	<plugins>      
		<plugin>   
			<groupId>org.apache.maven.plugins</groupId>    
			<artifactId>maven-shade-plugin</artifactId>   
			<version>3.2.1</version>
			<configuration>
				<createDependencyReducedPom>false</createDependencyReducedPom>
			</configuration>
			<executions>
				<execution>
					<phase>package</phase>
					<goals>
						<goal>shade</goal>
					</goals>
					<configuration>
						<relocations>
							<relocation>
								<pattern>com.google.guava</pattern>
								<shadedPattern>my.elasticsearch.guava</shadedPattern>
							</relocation>
							<relocation>
								<pattern>org.joda</pattern>
								<shadedPattern>my.elasticsearch.joda</shadedPattern>
							</relocation>
							<relocation><pattern>com.google.common</pattern>
								<shadedPattern>my.elasticsearch.common</shadedPattern>
								</relocation>	 
							<relocation>
								<pattern>com.google.thirdparty</pattern>
							<shadedPattern>my.elasticsearch.thirdparty</shadedPattern>
							</relocation>
						</relocations>
						<transformers>
							<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"/>
						</transformers>
					</configuration>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

通过这样修改后再进行打包,那么相当于把所有guava这个改名后的组件和es的依赖在编译后的class文件层进行绑定,将其两者变成一个整体依赖jar,并且这个组件也会自动修改es里面所有导入guava的旧路径为改动后的新路径,看如下从反编译后的jar中,拷贝出来的类文件信息:

改动后的guava的MoreExecutors这个类文件头部,变成了我们修改后的包名:

package my.elasticsearch.common.util.concurrent;
import my.elasticsearch.common.collect.*;
import my.elasticsearch.common.base.*;
import my.elasticsearch.common.annotations.*;
import java.util.concurrent.locks.*;
import java.util.*;
import java.util.concurrent.*;
public final class MoreExecutors{
	// 省略主体内容
}

注意头部的包名已经变成了my.elasticsearch.common替代了原来的com.google.common。

下面我们再看下es源码里面依赖的guava路径是否变化,打开org.elasticsearch.threadpool.ThreadPool这个类,我们发现其头部已经变成了新的guava路径,如下:

package org.elasticsearch.threadpool;

import org.elasticsearch.common.component.*;
import org.elasticsearch.node.settings.*;
import org.apache.lucene.util.*;
import my.elasticsearch.common.collect.*;
import org.elasticsearch.common.*;
import org.elasticsearch.common.util.concurrent.*;
import org.elasticsearch.common.unit.*;
import org.elasticsearch.common.collect.*;
import org.elasticsearch.common.settings.*;
import org.elasticsearch.common.logging.*;
import my.elasticsearch.common.util.concurrent.*;
import java.io.*;
import org.elasticsearch.common.io.stream.*;
import org.elasticsearch.common.xcontent.*;
import org.elasticsearch.cluster.settings.*;
import org.elasticsearch.cluster.*;
public class ThreadPool extends AbstractComponent{
// 省略主体内容
}

如此已来,这个shade jar里面的es就只对这个版本的guava进行了绑定依赖,这个时候在spark项目中,引入这个es的uber-shade-jar,就不会发生冲突,通过使用不同的包名完美解决了类冲突的问题,这两个类都可以被同一个JVM虚拟机加载,这样以来,spark仍旧可以使用guava14.0版本,而我们的es也可以完美的使用改名后的guava18.0的版本,从而比较优雅的解决了这种不可避免的多版本冲突问题。

参考链接:

https://stackoverflow.com/questions/13620281/what-is-the-maven-shade-plugin-used-for-and-why-would-you-want-to-relocate-java

;