如果jar包冲突不可避免,如何实现jar包隔离?

Posted by ACRusher on July 26, 2016

jar包隔离机制简介

业务背景

随着业务的发展 和 架构的升级, 业务会越来越多的依赖公司内部提供的 中间件 ,如 rpc服务框架、分库分表框架、异步消息框架、公共工具包等等。

每个中间件都有自己的 jar包依赖体系,最常用的如: logback、log4j、httpclient 、common-lang 、guava、zookeeper 等等 ,

这些jar包依赖不仅会产生版本冲突,甚至会有jar包不兼容的情况出现,比如 log4j 和 logback , guava 和 common-collection ,等等。

随着时间推移,业务越来越复杂,依赖的中间件也越来越多,每当引入新的中间件时,jar包版本管理的风险也越来越大,甚至出现无法兼容的情况。

解决方案

中间件容器与 业务web容器隔离,jar包依赖互不影响。 不仅解决了令人头痛的jar包冲突、jar包不兼容问题,还释放了业务开发人员的压力和责任,

公司内部的中间件通过专门的运维人员来推动升级,将业务与中间件隔离,通过透明的方式提供服务。

实现原理

先来回顾一下jvm的classloader加载机制。

当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:

bootstrap classloader | extension classloader | system classloader

bootstrap classloader -引导(也称为原始)类加载器,它负责加载Java的核心类。 在执行java的命令中使用-Xbootclasspath选项或使用 - D选项指定sun.boot.class.path系统属性值可以指定附加的类。 这个加载器的是非常特殊的,它实际上不是 java.lang.ClassLoader的子类,而是由JVM自身实现的。 可以通过下面的代码来查看原始类加载器加载了那些jar包, 它主要负责加载 JAVA_HOME/lib/*.jar

URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls.toExternalform()); }

extension classloader -扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包。 默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的 JAR类包对所有的JVM和system classloader都是可见的。 临时解决jar冲突的终极大招:将jar包拷贝到ext目录下。

在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrap classloader不是一个真正的ClassLoader实例。 system classloader -系统类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性 或者 CLASSPATH*作系统属性所指定的JAR类包和类路径。 总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。 重要:

classloader 加载类用的是全盘负责、委托机制。

所谓全盘负责,即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;

委托机制则是先让parent(父)类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。

现在我们了解了classloader的结构和工作原理,那么我们如何实现jar包的容器隔离呢?

答案很简单: 我们实现一个新的classloader就可以了!

代码示例

业务应用 依赖—–> middleware-client.jar

中间件具体实现 依赖—–> middleware-client.jar

通过client包 解耦合,实现最少依赖。

client包中类的示例代码:


IMiddlewareService

public interface IMiddlewareService {

    String reverse(String str);

}
SimpleClassLoader

public class SimpleClassLoader extends URLClassLoader{

    private static final String JAR_BASE_PATH="e:/middleware/";

    public SimpleClassLoader(URL[] urls) {
        //parent 设置为null 则不会被 system classloader 加载
        super(urls,null);
        try {
            File file=new File(JAR_BASE_PATH);
            String[]arr=file.list();
            for(String jar : arr) {
                super.addURL(new URL("file:"+JAR_BASE_PATH+jar));
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
//        if("com.qiyi.middleware.service.IMiddlewareService".equals(name)){
//            return SimpleClassLoader.class.getClassLoader().loadClass(name);
//        }
        try {
            return super.findClass(name);
        }catch (ClassNotFoundException e){
            return SimpleClassLoader.class.getClassLoader().loadClass(name);
        }
    }
}
 

MiddlewareServiceFactory

public class MiddlewareServiceFactory  implements FactoryBean<IMiddlewareService> {

    private SimpleClassLoader simpleClassLoader=new SimpleClassLoader(new URL[0]);


    public IMiddlewareService getObject() throws Exception {
        Class clazz=simpleClassLoader.loadClass("com.qiyi.middleware.service.impl.MiddlewareServiceImpl");
        //转型为 systemclassloader 加载的接口类
        return (IMiddlewareService) clazz.newInstance();
    }

    public Class<?> getObjectType() {
        return IMiddlewareService.class;
    }

    public boolean isSingleton() {
        return false;
    }
}

middleware-core 中负责具体实现,并且依赖了自己的jar包,这儿作为示例,依赖了 apache common-lang 2.5 版本,与业务层的2.6版本区别对待。


public class MiddlewareServiceImpl implements IMiddlewareService{

    public String reverse(String str) {
        if(str==null){
            return null;
        }
        else{
            // 2.5 版本的 common-lang jar 包
            return StringUtils.reverse(str);
        }
    }
}

业务层的使用非常简单,在不引入额外jar包的前提下,达到了透明使用。

在 spring 配置文件中,添加配置:

然后直接在服务层直接调用:


@Controller
@RequestMapping("/demo")
@Scope()
public class MyController {

    @Autowired
    private IMiddlewareService middlewareService;

    @RequestMapping("/reverse")
    @ResponseBody
    public String echo(@RequestParam("s") String str){
        return middlewareService.reverse(str);
    }
}

Creative Commons License
This work is licensed under a CC A-S 4.0 International License.