Java自定义类加载器实战

本文介绍了在项目Q和D中遇到的Mesos与Hadoop Protobuf版本冲突,以及不同Hadoop组件版本冲突问题。为解决这些问题,作者深入探讨了Java类加载的原理,包括Bootstrap、Extension和Application类加载器,并详细阐述了双亲委派模型。通过自定义类加载器,实现了从特定目录加载jar包以避免冲突。文中提供了两种使用自定义加载类的方法:面向接口编程和反射调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在项目Q中,使用Mesos进行资源隔离和任务调度。调度的任务类型包括一些Hadoop相关任务,在某次升级Hadoop集群之后,这些任务出错,跟踪日志发现是Mesos和Hadoop依赖的Protobuf版本出现了冲突,升级或降级Protobuf都不能解决问题。同时,在另外一个负责数据传输的项目D中,随着数据传输场景的多样化,项目D开始要和不同类型的输入输入打交道,包括不同版本的Hdfs、HBase和Hive集群等,同样出现了各种jar包冲突问题。按照经验,此类问题适合使用自定义类加载器来解决,但一路下来,磕磕碰碰踩了不少坑,记录下来。

原理学习

所谓类加载,就是虚拟机通过类名称获取类的二进制字节流,然后在方法区中生成代表这个类的Class对象的过程,而开发人员自定义类加载器能控制的就是如何获取字节流方式进行加载这一步。当然一个类能够被使用,还必须经过链接(验证+准备+解析)和初始化(clinit静态变量初始化和运行静态语句块)。

类加载过程

java提供了三种类加载器:
1. Bootstrap启动类加载器,负责加载jdk_home/lib目录下的核心api类。
2. Extension扩展类加载器,负责加载jdk_home/lib/ext目录下的jar包或-Djava.ext.dirs指定目录下jar包。
3. Application应用类加载器,负责加载用户类路径ClassPath下jar包。

其中,用户自定义类加载器的父类加载起是3,3的父是2,2的父是1。。。类加载过程使用双亲委派模型,即先交给父类加载器去加载,只有父类加载器无法加载再交给子类去完成,最后才会由用户自定义类加载器来加载。

源码分析

首先注意的是父类加载器是父“类加载器”而不是“父类”加载器,即双亲委派不是通过继承关系实现的,扩展类加载器和应用类加载器都是URLClassLoader的子类。

这里写图片描述
这里写图片描述

双亲关系通过parent成员变量在构造函数中初始化:

public abstract class ClassLoader {
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ……
}

委派加载流程在loadClass方法中实现:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派加载模型对保证虚拟机中类体系稳定很重要,所以一般情况下都不推荐自定义类加载器覆写loadClass默认行为。注意到在loadClass方法中,如果父类加载器加载失败后,会调用findClass方法,所以应当在自定义类加载器中覆写findClass方法,可以参考URLClassLoader实现,注意其中调用的defineClass方法,用于将二进制字节流转化为Class对象。

protected Class<?> findClass(final String name) throws ClassNotFoundException{
    try {
        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class>() {
                public Class run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
}

问题解决

自定义类加载器

使用自定义类类加载起的过程分成2步:
1. 定义入口类,其加载过程由自定义Classloader负责。这一步在D项目对应抽象出的Plugin组件,封装和各种不同类型数据源打交道逻辑。
2. 实现自定义ClassLoader,从自定义目录加载所需要jar包,而因为类加载器还有全盘负责机制,即其引用的其他相关类也会由当前类加载器全盘负责。这部分活要求比较细致,需要通过代码调试的方式整理出所需jar包放入不同的目录下,确保原先导致版本冲突的类按需加载。

public class PluginClassLoader extends URLClassLoader{
    public PluginClassLoader(URL[] urls, ClassLoader parent, Context context) {
        super(urls);
        this.parent = parent;
        this.context = context;
    }
    ……
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            Class<?> clz = findLoadedClass(name);
            if(clz == null){
                //use super URLClassLoader
                clz = super.findClass(name);
            }
            log.debug("QCExtClassLoad load succ:" + name);
            return clz;
        } catch (ClassNotFoundException e) {
            //use parent ApplicationClassLoader
            return parent.loadClass(name);
        }
    }
}

如何使用自定义加载类

接下来遇到的问题是,如果在外部方法中使用自定义ClassLoader加载的类进行赋值,会抛出ClassCastException异常,这是因为不同类加载器加载的类被JVM视为不同类,无法互相转换。

有两种解决方案。第一种是面向接口编程,项目D中,因为自定义类较多,所以抽象出Plugin接口,接口由默认应用类加载器加载,具体的Plugin实现类由自定义类加载器加载。

public  class XXXPlugin implements Plugin {……}
Plugin plugin = Plugin.create(xxxContext);

第二种方法应用在Q项目中,比较粗暴,在使用自定义类加载器加载特定类之后,直接通过反射实例化对象并调用其方法。这种方式适合自定义类方法调用比较简单的场景。

public class QCExtClassInvocationHandler {
    public static Object call(Context context, Class className, String methodName, Object[] args) {
        ClassLoader classLoader = QCClassLoader
                    .getInstance(context);
        try {
            Class clz = classLoader.loadClass(className.getCanonicalName());
            Object clzObj = clz.newInstance();

            List<Class> argsList = new ArrayList<Class>();
            for (Object arg : args) {
                argsList.add(arg.getClass());
            }

            Method method = clz.getMethod(methodName,
                    argsList.toArray(new Class[] {}));
            return method.invoke(clzObj, args);
        } catch (Exception e) {
            log.error("QCClassInvocationHandler fail", e);
            throw new RuntimeException(e);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值