反序列化类加载

反序列化的类加载

前面关于虚拟机类加载机制只是参考《深入理解Java虚拟机》这本书理解了个大概。具体的细节比如双亲委派的操作也没有进入代码中调试。为什么需要整理关于Java反序列化时的类加载,因为有一些CTF考题,以及一些反序列化漏洞都会涉及到类加载时的各种变形,比如:shiro...

ObjectInputStream#readObject

值得注意的地方:

  1. 由于JDK版本的不同readObject()方法代码可能会发生变化,这里使用的是 JDK 1.8.0_311

序列化类无重写readObject

test.java

package ReadObject;

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student student = new Student();
        Serialize(student);
        UnSerialize("test.ser");
    }
    public static void Serialize(Object obj) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("test.ser");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(obj);
    }
    public static void UnSerialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(filename);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();
    }
}

Student.java

package ReadObject;

import java.io.Serializable;

public class Student implements Serializable {
    String name = "BUTLER";
    int id = 0;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }
}

在objectInputStream.readObject()代码处下断点,然后进入ObjectInputSteam#readObject()方法

    private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) { //这里不用管,继承ObjectInputStream的类并重写了readObjectOveriide方法才会进入
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(type, false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

然后就是主要的方法调用栈,最后会到达Object的resolveClass(ObjectStreamClass desc)方法

-ObjectInputStream#readObject()
    -ObjectInputStream#readObject(Class<?> type)
       -ObjectInputStream#readObject0(Class<?> type, boolean unshared)
           -ObjectInputStream#readOrdinaryObject(boolean unshared)
    	 -ObjectInputStream#readClassDesc(boolean unshared)
    	   -ObjectInputStream#readNonProxyDesc(boolean unshared)
    	     -ObjectInputStream#resolveClass(ObjectStreamClass desc)

这里的ObjectInputStream#resolveClass(ObjectStreamClass desc)方法特别关注一下,这里会使用java.lang.Class#forName(name, false, latestUserDefinedLoader())来加载这个类最后返回一个类对象,然后返回ReadObject.Student的类对象

    protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
    {
        String name = desc.getName();
        try {
            return Class.forName(name, false, latestUserDefinedLoader());
        } catch (ClassNotFoundException ex) {
            Class<?> cl = primClasses.get(name);
            if (cl != null) {
                return cl;
            } else {
                throw ex;
            }
        }
    }

在这里其实才完成了类加载中的加载阶段。P神说过Java反序列化是为了还原一个完整的类实例,所以下面还有进行还原完整的类实例的操作

-ObjectInputStream#readOrdinaryObject(boolean unshared)
    -ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)

ObjectInputStream#readOrdinaryObject(boolean unshared)

ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)

    private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;

            if (slots[i].hasData) {
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } else if (slotDesc.hasReadObjectMethod()) { //这个判断比较重要
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } else {
                    defaultReadFields(obj, slotDesc);
                    }

                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else { 
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }
            }
        }
 }

这里注意if (slotDesc.hasReadObjectMethod()) 判断比较重要,代码的意思简单来说就是序列化的类有没有重写readObject()方法,如果重写了进入if,反之进入else,这里序列化类没有重写readObject()方法会进入else部分即ObjectInputStream#defaultReadFields()。在ObjectInputStream#defaultReadFields()里面会有恢复属性的方法desc.setPrimFieldValues(obj, primVals)desc.setObjFieldValues(obj, objVals)。具体里面的操作就不跟进了,以后有时间在详细分析

    private void defaultReadFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }

        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
            bin.readFully(primVals, 0, primDataSize, false);
        if (obj != null) {
            desc.setPrimFieldValues(obj, primVals); //恢复属性1
        }

        int objHandle = passHandle;
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            objVals[i] = readObject0(Object.class, f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        if (obj != null) {
            desc.setObjFieldValues(obj, objVals); //恢复属性2
        }
        passHandle = objHandle;
    }

该方法里面具体又会调用ObjectInputStream#defaultReadFields()方法执行完成以后可以看到已经有"真正属性值的实例"了

之后就是返回调用栈就OK了

序列化类有重写readObject

这部分相比于上部分Student类重写了readObject()方法

package ReadObject;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Student implements Serializable {
    String name = "BUTLER";
    int id = 0;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }

    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        System.out.println("Student defaultReadObject");
    }
}

前面类加载中的加载阶段就不多少了,直接看ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)部分的代码,上一节的if (slotDesc.hasReadObjectMethod()) 判断为假没有进入if,这一部分判断为真会进入if语句中

    private void readSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;

            if (slots[i].hasData) {
                if (obj == null || handles.lookupException(passHandle) != null) {
                    defaultReadFields(null, slotDesc); // skip field values
                } else if (slotDesc.hasReadObjectMethod()) {
                    ThreadDeath t = null;
                    boolean reset = false;
                    SerialCallbackContext oldContext = curContext;
                    if (oldContext != null)
                        oldContext.check();
                    try {
                        curContext = new SerialCallbackContext(obj, slotDesc);

                        bin.setBlockDataMode(true);
                        slotDesc.invokeReadObject(obj, this);
                    } catch (ClassNotFoundException ex) {
                        /*
                         * In most cases, the handle table has already
                         * propagated a CNFException to passHandle at this
                         * point; this mark call is included to address cases
                         * where the custom readObject method has cons'ed and
                         * thrown a new CNFException of its own.
                         */
                        handles.markException(passHandle, ex);
                    } finally {
                        do {
                            try {
                                curContext.setUsed();
                                if (oldContext!= null)
                                    oldContext.check();
                                curContext = oldContext;
                                reset = true;
                            } catch (ThreadDeath x) {
                                t = x;  // defer until reset is true
                            }
                        } while (!reset);
                        if (t != null)
                            throw t;
                    }

                    /*
                     * defaultDataEnd may have been set indirectly by custom
                     * readObject() method when calling defaultReadObject() or
                     * readFields(); clear it to restore normal read behavior.
                     */
                    defaultDataEnd = false;
                } else {
                    defaultReadFields(obj, slotDesc);
                    }

                if (slotDesc.hasWriteObjectData()) {
                    skipCustomData();
                } else {
                    bin.setBlockDataMode(false);
                }
            } else {
                if (obj != null &&
                    slotDesc.hasReadObjectNoDataMethod() &&
                    handles.lookupException(passHandle) == null)
                {
                    slotDesc.invokeReadObjectNoData(obj);
                }
            }
        }
}

然后进入ObjectInputStream#invokeReadObjcet()方法,该方法中将会使用readObjectMethod.invoke(obj, new Object[]{ in })代码激活Student#readObject()方法,之后便会进入Student#readObject()方法

    void invokeReadObject(Object obj, ObjectInputStream in)
        throws ClassNotFoundException, IOException,
               UnsupportedOperationException
    {
        requireInitialized();
        if (readObjectMethod != null) {
            try {
                readObjectMethod.invoke(obj, new Object[]{ in }); //激活Student#readObject()方法
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ClassNotFoundException) {
                    throw (ClassNotFoundException) th;
                } else if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

来到Student#Object方法,可以看到readObject方法里面调用了ObjectInputStream#defaultReadObject()方法。这个方法是做什么的?其实是和ObjectInputStream#defaultReadFields()方法是一样的都是还原一个完整的"真正属性值的实例。也就是P神说的在反序列化时将TC_OBJECT的classdata数组中对象非静态、非transient的属性全部读取出来。然后再实例化成一个对象。

    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        System.out.println("Student defaultReadObject");
    }

序列化类重写readResolve

网上查了查一般重写readResolve方法单例模式有关。如果我们反序列化的类有重写readResolve方法,在ObjectInputStream#readObject反序列化的时候会调用到该类的readResolve 简单看看

-ObjectInputStream#readObject()
    -ObjectInputStream#readObject(Class<?> type)
       -ObjectInputStream#readObject0(Class<?> type, boolean unshared)
           -ObjectInputStream#readOrdinaryObject(boolean unshared)
    	 -ObjectInputStream#readClassDesc(boolean unshared)
    	   -ObjectInputStream#readNonProxyDesc(boolean unshared)
    	     -ObjectInputStream#resolveClass(ObjectStreamClass desc)
             -ObjectInputStream#hasReadResolveMethod() //true
             -ObjectInputStream#invokeReadResolve(Object obj)

ObjectInputStream#readOrdinaryObject(boolean unshared)

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
    ......
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }
        return obj;
    }

继承ObjectInputStream类

这一节根据几个例子来看,一个是shiro550漏洞,另外是一个0ctf的buggyloader题目,Searilkiller的类加载。我见过的继承ObjectInputStream类的,它们都重写了resolveClass方法,它们的区别都在于获取类对象的地方是URLloadClass.loadClass还是Class.forName....

以及还有继承ObjectInputStream类的,它们都重写了resolveProxyClass方法,这个在weblogic中遇到的多

反序列化的类是普通对象

ObjectInpuStream#resolveClass:根据类描述符返回相应的类

    protected Class<?> resolveClass(ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
    {
        String name = desc.getName();
        try {
            return Class.forName(name, false, latestUserDefinedLoader());
        } catch (ClassNotFoundException ex) {
            Class<?> cl = primClasses.get(name);
            if (cl != null) {
                return cl;
            } else {
                throw ex;
            }
        }
    }

shiro550的resolveClass

这里前面的就不说了,直接看反序列部分

这里有一个shiro自己实现的反序列化流ClassResolvingObjectInputStream,该流继承了ObjectInputStream并且重写了resolveClass()方法。resolveClass方法里面会使用org.apache.shiro.util.ClassUtils#forName来完成加载阶段得到类对象。因为这里的改变shiro这里是不支持反序列化数组类型的(具体的原因我也不清楚,可以参考这篇文章https://blog.zsxsoft.com/post/35) 所以之前在shiro笔记中使用CC6漏洞利用失败,之后使用CB和CC6+CC2的结合才成功

package org.apache.shiro.io;

import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

/**
 * Enables correct ClassLoader lookup in various environments (e.g. JEE Servers, etc).
 *
 * @since 1.2
 * @see <a href="https://issues.apache.org/jira/browse/SHIRO-334">SHIRO-334</a>
 */
public class ClassResolvingObjectInputStream extends ObjectInputStream {

    public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    /**
     * Resolves an {@link ObjectStreamClass} by delegating to Shiro's 
     * {@link ClassUtils#forName(String)} utility method, which is known to work in all ClassLoader environments.
     * 
     * @param osc the ObjectStreamClass to resolve the class name.
     * @return the discovered class
     * @throws IOException never - declaration retained for subclass consistency
     * @throws ClassNotFoundException if the class could not be found in any known ClassLoader
     */
    @Override
    protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
        try {
            return ClassUtils.forName(osc.getName());
        } catch (UnknownClassException e) {
            throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
        }
    }
}

buggyloader的resolveClass

URLClassLoader#loadClass获取类对象

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.net.URL;
import java.net.URLClassLoader;
import org.apache.commons.collections.Transformer;

public class MyObjectInputStream extends ObjectInputStream {
  private ClassLoader classLoader;
  
  public MyObjectInputStream(InputStream inputStream) throws Exception {
    super(inputStream);
    URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
    this.classLoader = new URLClassLoader(urls);
  }
  
  protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
    Class<?> clazz = this.classLoader.loadClass(desc.getName());
    return clazz;
  }
}

Searialkiller的resolveClass

Class#forName获取类对象

反序列化是动态代理对象

ObjectInputStream#resolveProxyClass 根据代理类描述符中所有接口的代理类

-ObjectInputStream#readObject()
    -ObjectInputStream#readObject(Class<?> type)
       -ObjectInputStream#readObject0(Class<?> type, boolean unshared)
           -ObjectInputStream#readOrdinaryObject(boolean unshared)
    	 -ObjectInputStream#readClassDesc(boolean unshared)
    	   -ObjectInputStream#readProxyDesc(boolean unshared)
    	     -ObjectInputStream#resolveProxyClass(String[] interfaces)

参数:interfaces -- 接口名称被反序列化的代理类描述符列表

返回值:此方法返回一个代理类指定的接口

    protected Class<?> resolveProxyClass(String[] interfaces)
        throws IOException, ClassNotFoundException
    {
        ClassLoader latestLoader = latestUserDefinedLoader();
        ClassLoader nonPublicLoader = null;
        boolean hasNonPublicInterface = false;

        // define proxy in class loader of non-public interface(s), if any
        Class<?>[] classObjs = new Class<?>[interfaces.length];
        for (int i = 0; i < interfaces.length; i++) {
            Class<?> cl = Class.forName(interfaces[i], false, latestLoader);
            if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
                if (hasNonPublicInterface) {
                    if (nonPublicLoader != cl.getClassLoader()) {
                        throw new IllegalAccessError(
                            "conflicting non-public interface class loaders");
                    }
                } else {
                    nonPublicLoader = cl.getClassLoader();
                    hasNonPublicInterface = true;
                }
            }
            classObjs[i] = cl;
        }
        try {
            return Proxy.getProxyClass(
                hasNonPublicInterface ? nonPublicLoader : latestLoader,
                classObjs);
        } catch (IllegalArgumentException e) {
            throw new ClassNotFoundException(null, e);
        }
    }

比如说我的代理类是这样写的

public static void main(String[] args) throws Exception {
    ObjID id = new ObjID(new Random().nextInt()); // RMI registry
    TCPEndpoint te = new TCPEndpoint("127.0.0.1",1099);
    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
    RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
    Registry proxy = (Registry) Proxy.newProxyInstance(test.class.getClassLoader(), new Class[] {Registry.class}, obj);
    serialize(proxy);
    unserialize();
}

当执行到ObjectInputStream#resolveProxyClass方法的时候 参数和返回值是:

返回值:

ServerChannelInputStream的resolveProxyClass

不同的版本补丁此处有不同的变化,CVE-2017-3248的补丁:

protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
    String[] arr$ = interfaces;
    int len$ = interfaces.length;

    for(int i$ = 0; i$ < len$; ++i$) {
        String intf = arr$[i$];
        if (intf.equals("java.rmi.registry.Registry")) {
            throw new InvalidObjectException("Unauthorized proxy deserialization");
        }
    }

    return super.resolveProxyClass(interfaces);
}

加载类对象

这篇文章不错推荐阅读https://www.anquanke.com/post/id/260902#h2-7

Class#forName

Class.forName 不能加载原生类型,但其他类型都是支持的。跟踪Class#forName最后发现调用Class#forName0获取类对象。native修饰的方法最后由C/C++来实现,根据资料显示这个底层也遵循双亲委派机制

    private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

ClassLoader#loadClass

Classloader.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;
        }
    }

这里的resolveClass注意不要和ObjectInputStream#resolveClass混淆,它们是不同的概念

ClassLoader#findClass

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

URLClassLoader#loadClass

URLCLassLoader继承SecureClassLoader,SecureClassLoader继承ClassLoader。

  • 这里的loadClass里进行了判断然后使用ClassLoader#loadClass获取类对象
    public final Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // First check if we have permission to access the package. This
        // should go away once we've added support for exported packages.
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                sm.checkPackageAccess(name.substring(0, i));
            }
        }
        return super.loadClass(name, resolve);
    }

并且URLClassLoader还重写了findClass方法

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = 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 {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

破坏双亲为派机制

如果我们继承ClassLoasder并重写loadClass会破坏双亲委派机制

在Java中常见的几种破坏双亲委派场景

  • Tomcat类加载机制
  • OSGI模块化类加载
  • JDBC类加载机制
posted @ 2022-07-14 17:32  B0T1eR  阅读(138)  评论(0编辑  收藏  举报