Java自定义类加载器问题

时间:2017-12-01 20:47:06

标签: java serialization classloader dynamic-class-loaders

我正在从客户端向服务器端发送一个Class对象。每次服务器需要加载客户端发送的Class对象,而不是通过父委派模型重用它(在第一次迭代期间加载时)。

我正在尝试在服务器端使用自定义类加载器,loadClass(String)只调用findClass()而不是使用父层次结构进行检查。 为实现这一目标,我正在做以下事情:

  1. 通过读取客户端上的.class文件生成byte [],如下所示:
  2. Class cl = com.example.XYZ.class;
    String path = cl.getName().replace('.', '/') + ".class";
    InputStream is = cl.getClassLoader().getResourceAsStream(path);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    int data = -1;
    while((data=is.read())!=-1)
      bos.write(data);
    byte[] classBinaryData = bos.toByteArray();
    

    我正在向服务器端发送classBinaryData

    1. 在服务器端,每次检索byte[]时,通过匹配MD5校验和来验证它是否与客户端相同,然后我创建自定义类加载器的新实例并传递字节数组它可用于从defineClass内调用findClass
    2. 但是,我得到了其中一个错误(取决于我从.class创建byte []的方式)

      Incompatible magic value ..... in class file <Unknown>

      OR

      com/example/XYZ (wrong name: com/example/XYZ)来自defineClass

      我需要帮助来弄清楚我的方法/代码中的错误。

4 个答案:

答案 0 :(得分:3)

你的byte []生成代码看起来很好。

当我使用从代码生成的字节数组加载具有以下类加载器代码的类时,它能够成功加载类。

class CustomClassLoader extends ClassLoader {

    public Class loadTheClass(String name, byte[] bytes) {

        return defineClass(name, bytes, 0, bytes.length);
    }
}

像这样使用这个类加载器

CustomClassLoader ccl = new CustomClassLoader();
        Class cz = ccl.loadTheClass("com.example.XYZ", classBinaryData);
        Object o = cz.newInstance();
  1. 我认为在服务器端加载类时,必须在名称中使用'.'而不是'/'
  2. 并确保您的其他代码中的字节数组数据不会更改。

答案 1 :(得分:1)

您的代码看起来很好。你的错误在其他地方。

在某种程度上,您将从类加载器返回错误的类文件。

第一个错误意味着字节数组完全乱码;前4个字节是错误的。您可以轻松地检查它们(它们必须是0xCAFEBABE),以便更早地捕获此错误。

我认为,另一个错误意味着您正在返回不同于所请求的类的定义。

答案 2 :(得分:1)

1。缺少点符号

  

com / example / XYZ(错误名称:com / example / XYZ)来自defineClass

您应该使用点表示法,即 com.example.XYZ

Class clazz = classLoader.loadCustomClass("com.example.XYZ", bytes);

2。无效的幻数(损坏的类别字节)

  

类文件中不兼容的魔法值

您收到上述错误,因为类字节数组的开头已损坏。它通过抛出 java.lang.ClassFormatError 来抱怨不兼容的魔法值。当类加载器在类字节的开头没有找到 0xCAFEBABE (幻数)时,通常会发生这种情况。

这是一个简单的例子,您可以通过它重新创建错误。

  • 在此示例中,com.basaki.model.Book类文件保存为Base64编码的字符串。
  • 方法testLoadingClassWithCorrectMagicNumber尝试在将其解码为字节数组后从Base64编码的字符串加载该类。它正常加载而没有任何事故。
  • 在方法testLoadingClassWithIncorrectCorrectMagicNumber中,通过将第一个字符从c替换为b,字节数组(在解码Base64字符串之后)已损坏。现在,而不是幻数 0xCAFEBABE ,它是 0xBAFEBABE 。类加载器现在在尝试加载损坏的二进制数组时抛出以下异常

      

    java.lang.ClassFormatError:类文件中的不兼容的魔法值3137256126 com / basaki / model / Book

    public class LoadingBookFromBinaryArrayTest {
    
        private static class MyCustomClassLoader extends ClassLoader {
    
            public Class loadCustomClass(String name, byte[] bytes) {
                return defineClass(name, bytes, 0, bytes.length);
            }
        }
    
        public static String BOOK_CLAZZ = "yv66vgAAADQAHQoABQAYCQAEABkJAAQAGgcAGwcAHAEABXRpdGxlAQASTGphdmEvbGFuZy9TdHJpbmc7AQAGYXV0aG9yAQAGPGluaXQ-AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMY29tL2Jhc2FraS9tb2RlbC9Cb29rOwEACGdldFRpdGxlAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAhzZXRUaXRsZQEAFShMamF2YS9sYW5nL1N0cmluZzspVgEACWdldEF1dGhvcgEACXNldEF1dGhvcgEAClNvdXJjZUZpbGUBAAlCb29rLmphdmEMAAkACgwABgAHDAAIAAcBABVjb20vYmFzYWtpL21vZGVsL0Jvb2sBABBqYXZhL2xhbmcvT2JqZWN0ACEABAAFAAAAAgACAAYABwAAAAIACAAHAAAABQABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAMADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAQALAAAALwABAAEAAAAFKrQAArAAAAACAAwAAAAGAAEAAAAJAA0AAAAMAAEAAAAFAA4ADwAAAAEAEgATAAEACwAAAD4AAgACAAAABiortQACsQAAAAIADAAAAAoAAgAAAA0ABQAOAA0AAAAWAAIAAAAGAA4ADwAAAAAABgAGAAcAAQABABQAEQABAAsAAAAvAAEAAQAAAAUqtAADsAAAAAIADAAAAAYAAQAAABEADQAAAAwAAQAAAAUADgAPAAAAAQAVABMAAQALAAAAPgACAAIAAAAGKiu1AAOxAAAAAgAMAAAACgACAAAAFQAFABYADQAAABYAAgAAAAYADgAPAAAAAAAGAAgABwABAAEAFgAAAAIAFw==";
    
        @Test
        public void testLoadingClassWithCorrectMagicNumber() throws IllegalAccessException, InstantiationException, DecoderException {
            byte[] bytes = Base64.getUrlDecoder().decode(BOOK_CLAZZ);
            MyCustomClassLoader classLoader = new MyCustomClassLoader();
            Class clazz = classLoader.loadCustomClass("com.basaki.model.Book", bytes);
        }
    
        @Test(expected = ClassFormatError.class)
        public void testLoadingClassWithIncorrectCorrectMagicNumber() throws IllegalAccessException, InstantiationException, DecoderException {
            byte[] bytes = Base64.getUrlDecoder().decode(BOOK_CLAZZ);
            String hex = Hex.encodeHexString(bytes);
            System.out.println(hex);
    
            // changing magic number 0xCAFEBABE to invalid 0xBAFEBABE
            String malHex = "b" + hex.substring(1, hex.length());
            System.out.println(malHex);
            byte[] malBytes = Hex.decodeHex(malHex.toCharArray());
    
            MyCustomClassLoader classLoader = new MyCustomClassLoader();
            Class clazz = classLoader.loadCustomClass("com.basaki.model.Book", bytes9);
        }
    }
    

答案 3 :(得分:0)

如上所述,除了收集字节数组之外,问题似乎还在其他地方。有可能在服务器端没有正确处理字节。我创建了一个非常简单的示例,它与您正在执行的操作类似,但它显示了我如何发送和接收类字节数组。

package org.valhalla.classloader;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class RemoteClassLoader extends ClassLoader {

    private Socket socket;
    private DataOutputStream dos;
    private DataInputStream dis;

    public RemoteClassLoader(Socket socket, ClassLoader parent) {
        super(parent);
        this.socket = socket;
        OutputStream os;
        InputStream is;
        try {
            os = socket.getOutputStream();
            is = socket.getInputStream();
        } catch (IOException e) {
            throw new RuntimeException("Unable to get Socket output stream", e);
        }
        dos = new DataOutputStream(os);
        dis = new DataInputStream(is);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> clz = null;
        System.out.println("Looking up class: " + name);
        synchronized(this.getClassLoadingLock(name)) {
            try {
                System.out.println("Sending request for class: " + name);
                dos.writeUTF(name);
                boolean success = dis.readBoolean();
                System.out.println("Action was " + success);
                if (success) {
                    // Get bytes;
                    System.out.println("Reading size of class file");
                    int len = dis.readInt();
                    System.out.println("Size of class is " + len);
                    byte data[] = new byte[len];
                    int cur, size = 0;
                    for (cur = 0 ; cur < len ;  cur += size) {
                        size = dis.read(data, cur, len - cur);
                        System.out.println("Read size: " + size);
                    }
                    System.out.println("Completed reading class file for class " + name);
                    return defineClass(name, data, 0, len);
                }
            } catch (IOException e) {
                throw new ClassNotFoundException("Class: " + name + " was not found", e);
            }
        }
        return clz;
    }

    public void close() {
        try {
            if (socket != null && socket.isClosed() == false) {
                this.socket.close();
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

该类将读取字节数组并将其加载到代码的服务器端。请注意,我使用一个简单的协议来确定通过线路发送的字节数,并确保我已读取正确的字节数。

以下是将通过网络发送信息的客户端代码。它是你上面提到的扩展。

package org.valhalla.client;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.net.Socket;

public class ClientConnection {

    private Socket socket;

    public ClientConnection(Socket socket) {
        this.socket = socket;
    }

    public void process() {
        try {
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            String name = null;
            while ((name = dis.readUTF()) != null && name.length() > 0) {
                System.out.println("Looking up class: " + name);
                InputStream resource = ClassLoader.getSystemResourceAsStream(name.replace('.', '/') + ".class");
                if (resource == null) {
                    System.out.println("Class not found: " + name);
                    dos.writeBoolean(false);
                    continue;
                }
                System.out.println("Found class: " + name);
                try {
                    byte buf[] = new byte[1024];
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int size = 0;
                    while ((size = resource.read(buf)) > 0) {
                        bos.write(buf, 0, size);
                    }
                    byte clz[] = bos.toByteArray();
                    dos.writeBoolean(true);
                    System.out.println("Sendding class size: " + clz.length);
                    dos.writeInt(clz.length);
                    System.out.println("Sending class bytes");
                    dos.write(clz);
                    System.out.println("Sent class bytes");
                } catch (Throwable t) {
                    t.printStackTrace();
                    dos.writeBoolean(false);
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (socket != null && socket.isClosed() == false) {
                try { socket.close(); } catch(Throwable t) {}
            }
        }
    }

}

如您所见,它只是向服务器发送一些信息,让它知道预计会传输多少数据。以下类可与上述类一起使用,以显示其工作原理。

package org.valhalla.classloader;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

public class RemoteClassLoaderServer {

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("syntax error:  missing port");
            System.exit(1);
        }
        int port = 0;
        try {
            port = Integer.parseInt(args[0]);
        } catch(NumberFormatException nfe) {
            System.out.println("Invalid port number: " + args[1]);
            System.exit(2);
        }

        if (port < 0) {
            System.out.println("Port cannot be negative: " + port);
        }

        ServerSocket server = null;

        try {
            server = new ServerSocket(port);
        } catch (IOException e) {
            System.out.println("Unable to create server socket for port: " + port);
            System.exit(3);
        }

        Socket s = null;
        try {
            s = server.accept();
            InputStream is = s.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            System.out.println("Waiting for class name");
            String name = dis.readUTF();
            System.out.println("Received class name: " + name);
            RemoteClassLoader rcl = new RemoteClassLoader(s, RemoteClassLoaderServer.class.getClassLoader());
            System.out.println("Finding class: " + name);
            Class<?> clz = rcl.loadClass(name);
            Method m = clz.getMethod("main", String[].class);
            System.out.println("Executing main method");
            m.invoke(null, new Object[] { new String[0] });
            System.out.println("done");
            new DataOutputStream(s.getOutputStream()).writeUTF("");
        } catch (Throwable e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            if (s != null && s.isClosed() == false) {
                try { s.close(); } catch(Throwable t) {}
            }
        }
    }

}

以下是客户端类

package org.valhalla.client;

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class ClientMain {

    public static void main(String[] args) {
        int port = Integer.parseInt(args[0]);
        try {
            Socket socket = new Socket("localhost", port);
            System.out.println("Opened socket at port: " + port);
            String name = Main.class.getName();
            new DataOutputStream(socket.getOutputStream()).writeUTF(name);
            System.out.println("Sent Class name: " + name);
            ClientConnection conn = new ClientConnection(socket);
            conn.process();
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

此类将在服务器端运行。

package org.valhalla.client;

public class Main {

    public static void main(String args[]) {
        Client client = new Client();
        client.execute();
    }

}

这个班。

package org.valhalla.client;

public class Client {

    public void execute() {
        System.out.println("######### We are calling the Client class execute method #####");
    }

}

希望这有帮助。