1. 程式人生 > >反序列化 動態載入jar的裡的類報ClassNotFoundException解決辦法

反序列化 動態載入jar的裡的類報ClassNotFoundException解決辦法

1.背景

自己在寫一個RPC框架時,碰到第一個麻煩就是做動態載入載入jar包後,在進行反序列化(不要吐槽為啥用java原生的序列化方案,一步一步來,框架寫完能跑後在優化)時報CNF錯誤,當時感覺應該是原生的序列化方案中使用的ClassLoader是應用載入器AppCloassLoader,而我使用的URLClassLoader載入的外部jar包,導致沒有找到。

關於java的類載入器,如果知道雙親委託機制就可以往下接著看了,如果不瞭解,我幫大家找了一篇
雙親委託機制總的來說就是,如果一個classloader要載入一個類,先問問他的父級classloader沒有載入這個類,父級也會在去問它的父級,直至bootstrap classloader,如果其父級載入器無法載入該類,才會交由它本身載入。

2.解決方案

java反序列化程式碼一般為下面這樣

public static Object deserialize(byte[] bytes) {
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            // 反序列化
            bais = new ByteArrayInputStream(bytes);
            ois = new ObjectInputStream(bais);
            return
ois.readObject(); } catch (Exception e) { log.error("反序列化錯誤", e); } return null; }

檢視ObjectInputStream原始碼找到了類載入的地方

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

可以看到Class.forName中使用的是AppClassLoader,解決方案其實就是複寫該方法,使用我們動態載入的ClassLoader

public class ObjectInputStreamWithLoader extends ObjectInputStream {

    private ClassLoader loader;

    public ObjectInputStreamWithLoader(InputStream in, ClassLoader loader)
                throws IOException, StreamCorruptedException {

            super(in);
            if (loader == null) {
                throw new IllegalArgumentException("Illegal null argument to ObjectInputStreamWithLoader");
            }
            this.loader = loader;
        }

    /**
     * Use the given ClassLoader rather than using the system class
     */
    @SuppressWarnings("rawtypes")
    protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException {

        String cname = classDesc.getName();
        return loader.loadClass(cname);
    }

}

可以看到在例項化該類的時候,需傳入一個classloader

動態載入我使用的是URLClassLoader,程式碼如下

URL[] urls = new URL[2];
urls[0] = new URL("file:///D:/jar/a.jar");
urls[1] = new URL("file:///D:/jar/a.impl.jar");

URLClassLoader classLoader = new URLClassLoader(urls, ClassLoader.getSystemClassLoader());

3.利用Hessian序列化

public static byte[] serialize(Object obj) throws IOException {
        if (obj == null)
            throw new NullPointerException();

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(os);
        ho.writeObject(obj);
        return os.toByteArray();
    }

    public static Object deserialize(byte[] by, ClassLoader classLoader) throws IOException {
        if (by == null)
            throw new NullPointerException();

        ByteArrayInputStream is = new ByteArrayInputStream(by);
        ClassLoader old = null;
        if (classLoader != null) {
            old = Thread.currentThread().getContextClassLoader();
            // 切換當前執行緒classloader,保證動態載入的類不會報CNF
            Thread.currentThread().setContextClassLoader(classLoader);
        }
        HessianInput hi = new HessianInput(is);
        Object obj = hi.readObject();

        if (classLoader != null) {
            Thread.currentThread().setContextClassLoader(old);
        }
        return obj;
    }

在反序列時,修改其當前執行緒上下文類載入器,保證能載入到該類。

最近打算從頭開始寫一個RPC框架,歡迎有興趣的同學一塊交流
https://github.com/yangzhenkun/krpc