Java 自定义 class 加载程序问题

Java custom class loader issue

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

我正在尝试在服务器端使用自定义 class 加载程序,其 loadClass(String) 只是调用 findClass() 而不是检查父层次结构。 为此,我正在执行以下操作:

  1. 通过在客户端读取.class文件生成字节[],如下所示:
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 校验和验证它是否与客户端相同,然后我创建一个新的自定义 class 加载器实例并传递字节数组,以便它可以用于从 findClass.
  2. 中调用 defineClass

但是,我遇到了其中一个错误(取决于我从 .class 中创建 byte[] 的方式)

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

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

我需要帮助来找出我 approach/code 中的错误。

您的代码看起来不错。你的错误在别的地方。

你在某种程度上从你的 class 加载程序返回错误的 class 文件。

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

另一个错误,我认为,意味着您返回的定义与请求的 class 不同。

您的 byte[] 生成代码看起来不错。

当我使用从您的代码生成的字节数组通过以下 class 加载程序代码加载 class 时,它能够成功加载 class。

class CustomClassLoader extends ClassLoader {

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

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

像这样使用这个 classloader

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

1。缺少点表示法

com/example/XYZ (wrong name: com/example/XYZ) coming from defineClass

您应该使用点符号,即 com.example.XYZ

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

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

Incompatible magic value ..... in class file

您收到上述错误是因为 class 字节数组的 开头已损坏 。它通过抛出 java.lang.ClassFormatError 来抱怨 Incompatible magic value。当 class 加载程序在 class 字节的开头找不到 0xCAFEBABE(幻数)时,通常会发生这种情况。

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

  • 在此示例中,com.basaki.model.Book class 文件保存为 Base64 编码字符串。
  • 方法 testLoadingClassWithCorrectMagicNumber 尝试在解码为字节数组后从 Base64 编码的字符串中加载 class。它正常加载,没有任何意外。
  • 在方法 testLoadingClassWithIncorrectCorrectMagicNumber 中,通过将第一个字符从 c 替换为 b,字节数组(在解码 Base64 字符串之后)被破坏。现在,幻数不再是 0xCAFEBABE,而是 0xBAFEBABE。 class 加载程序现在在尝试加载损坏的二进制数组

    时抛出以下异常

    java.lang.ClassFormatError: Incompatible magic value 3137256126 in class file 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);
        }
    }
    

如上所述,除了收集字节数组之外,问题似乎出在其他地方。服务器端可能未正确处理字节。我创建了一个相当简单的示例,它与您正在做的类似,但它显示了我如何发送和接收 class 字节数组。

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

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

这是将通过网络发送信息的客户端代码。它是您上面提到的内容的扩展。

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) {}
            }
        }
    }

}

如您所见,它只是向服务器发送一些信息,让服务器知道需要传输多少数据。以下 classes 可与上述 classes 一起使用,以说明其工作原理。

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) {}
            }
        }
    }

}

这里是客户端classes

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

}

这个class将在服务器端运行。

包 org.valhalla.client;

public class Main {

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

}

用这个 class.

包 org.valhalla.client;

public class Client {

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

}

希望对您有所帮助。