Java custom class loader issue

青春壹個敷衍的年華 提交于 2019-12-06 04:49:33

问题


I'm sending a Class object from client to server side. Every time the server needs to load the Class object sent by the client instead of reusing it by parent delegation model (when it was loaded during the 1st iteration).

I'm trying to use a custom class loader on the server side whose loadClass(String) simply calls findClass() instead of checking with parent hierarchy. To achieve this, I'm doing following:

  1. Generate byte[] by reading the .class file on the client side as following:
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();

I'm sending classBinaryData to the server side.

  1. On the server side, every time I retrieve the byte[], verify if it's the same as on client side by matching MD5 checksum, then I create a new instance of my custom class loader and pass the byte array so it could be used in calling defineClass from within findClass.

However, I'm getting either of the errors (depending on the way I create byte[] out of .class)

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

OR

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

I need help in figuring out the mistake in my approach/code.


回答1:


Your byte[] generation code looks fine.

When I used the byte array generated form your code to load the class with following class loader code, it was able to load the class successfully.

class CustomClassLoader extends ClassLoader {

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

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

Using this classloader like this

CustomClassLoader ccl = new CustomClassLoader();
        Class cz = ccl.loadTheClass("com.example.XYZ", classBinaryData);
        Object o = cz.newInstance();
  1. I think you must use '.' instead of '/' in the name when you are loading the class at server side.
  2. And ensure that the byte array data is not changed in your other code.



回答2:


Your code looks fine. Your error is somewhere else.

You are, in some way, returning bad class files from your class loader.

The first error means the byte array is totally garbled; the first 4 bytes are wrong. You can check them easily (they have to be 0xCAFEBABE), to catch this error earlier.

The other error, I think, means that you are returning the definition of a different class than was requested.




回答3:


1. Missing Dot Notation

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

You should be using dot notation, i.e., com.example.XYZ

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

2. Invalid Magic Number (Corrupt Class Bytes)

Incompatible magic value ..... in class file

You are getting the above error because the start of the class byte array is corrupted. It's complaining about Incompatible magic value by throwing a java.lang.ClassFormatError. It usually happens when the class loader doesn't find 0xCAFEBABE (magic number) at the beginning of the class bytes.

Here is a simple example by which you can recreate the error.

  • In this example, the com.basaki.model.Book class file is saved as a Base64 encoded string.
  • The method testLoadingClassWithCorrectMagicNumber tries to load the class from the Base64 encoded string after decoding it to a byte array. It loads normally without any incident.
  • In method testLoadingClassWithIncorrectCorrectMagicNumber, the byte array (after the Base64 string is decoded) is corrupted by replacing the first character from c to b. Now instead of the magic number being 0xCAFEBABE, it is 0xBAFEBABE. The class loader now throws the following exception while trying to load the corrupt binary array,

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



回答4:


As has been stated above the issue seems to be somewhere else besides the gathering of the byte array. It is possible that the bytes are not being processed properly on the server side. I've created a fairly simple example that is similar to what you are doing but it shows how I send and receive the class byte array.

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

This class will read the byte array and load it within the server side of the code. Note that I use a simple protocol to determine how many bytes are being sent over the wire and insure that I have read the correct amount of bytes.

Here is the client side code that will send the information over the wire. It is an extension of what you mentioned above.

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

}

As you see it just sends some information to the server that lets it know how much data is expected to be transferred. The following classes can be used with the above classes to show how this works.

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

}

Here are the client side 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();
        }
    }

}

This class will be run within the server side.

package org.valhalla.client;

public class Main {

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

}

with this class.

package org.valhalla.client;

public class Client {

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

}

Hope this helps.



来源:https://stackoverflow.com/questions/47601251/java-custom-class-loader-issue

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!