Проблема с созданием пользовательского загрузчика классов

Я пытаюсь создать собственный загрузчик классов, чтобы выполнить следующее:

У меня есть класс в пакете com.company.MyClass

Когда загрузчику классов предлагается загрузить что-либо в следующем формате:

com.company.[additionalPart].MyClass

Я бы хотел, чтобы загрузчик классов загружал com.company.MyClass, эффективно игнорируя [additionalPart] имени пакета.

Вот код, который у меня есть:

public class MyClassLoader extends ClassLoader {   

    private String packageName = "com.company.";
    final private String myClass = "MyClass";

    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> clazz = null;
        String className = name;

        // Check if the class name to load is of the format "com.company.[something].MyClass 
        if (name.startsWith(packageName)) {
            String restOfClass = className.substring(packageName.length());
            // Check if there is some additional part to the package name
            int index = restOfClass.indexOf('.');
            if (index != -1) {
                restOfClass = restOfClass.substring(index + 1);
                //finally, check if the class name equals MyClass 
                if (restOfClass.equals(myClass)) {
                    // load com.company.MyClass instead of com.company.[something].MyClass
                    className = packageName + myClass;
                    clazz = super.loadClass(className, true);
                }
            }
        }

        if (clazz == null) {
            // Normal clase: just let the parent class loader load the class as usual 
            clazz = super.loadClass(name);
        }

        return clazz;
    }

}

И вот мой тестовый код:

public class TestClassLoader {

    public void testClassLoader () throws Exception {
        ClassLoader loader = new MyClassLoader(this.getClass().getClassLoader());
        clazz = Class.forName("com.company.something.MyClass", true, loader );       
    }

    public static void main (String[] args) throws Exception {
        TestClassLoader tcl = new TestClassLoader();
        tcl.testClassLoader();
    }
}

MyClassLoader подбирает правильный класс (т.е. com.company.MyClass) и корректно возвращает его из loadClass (я прошел через код), однако в какой-то более поздний момент (т.е. после вызова loadClass из JVM) он выдает исключение следующим образом:

Исключение в потоке «основной» java.lang.ClassNotFoundException: com/company/something/MyClass в java.lang.Class.forName0 (собственный метод)

в java.lang.Class.forName(Class.java:247) в [мой код]

Теперь я понимаю, что некоторые из вас могут подумать: «Зачем кому-то это нужно делать? Должен быть лучший способ». Я уверен, что есть, но для меня это что-то образовательное, так как я хотел бы понять, как работают загрузчики классов, и получить более глубокое понимание процесса загрузки классов jvm. Так что, если вы сможете не обращать внимания на бессмысленность процедуры и пошутить надо мной, я буду вам очень признателен.


person yspotts    schedule 02.08.2010    source источник


Ответы (2)


Ваш вопрос

Это чистая спекуляция. Имя класса хранится в байтовом коде Java. Таким образом, классы, которые вам удастся загрузить с помощью этой техники, будут дефектными. Это, вероятно, путает систему.

ClassLoader, вероятно, хранит ссылку на com.company.something.MyClass, но сам класс, вероятно, хранит ссылку на com.company.MyClass. (Вероятно, я использую много, потому что я не знаю наверняка.) Вероятно, все работает нормально, пока вы не используете класс MyClass для чего-то. Тогда несоответствие создает проблемы. Итак, когда выбрасывается это исключение?

Если вам интересно узнать, как работают загрузчики классов, вы можете использовать javap, чтобы получить байт-код. Это также позволит вам проверить мою гипотезу.

Если моя гипотеза верна, то решением будет исправление байт-кода. Есть несколько пакетов, которые позволяют создавать байт-код. Скопируйте класс, измените имя скопированного класса, а затем загрузите его.

В сторону

Хотя это не имеет отношения к вашему вопросу: я считаю, что приведенное ниже излишне сложно (и это не работает на com.company.something.somethingelse.MyClass).

        // Check if the class name to load is of the format "com.company.[something].MyClass 
    if (name.startsWith(packageName)) {
        String restOfClass = className.substring(packageName.length());
        // Check if there is some additional part to the package name
        int index = restOfClass.indexOf('.');
        if (index != -1) {
            restOfClass = restOfClass.substring(index + 1);
            //finally, check if the class name equals MyClass 
            if (restOfClass.equals(myClass)) {
                // load com.company.MyClass instead of com.company.[something].MyClass
                className = packageName + myClass;
                clazz = super.loadClass(className, true);
            }
        }

Почему бы и нет?

//Check if the class name to load is of the format "com.com        // Check if the class name to load is of the format "com.company.[something].MyClass"
if ( ( name . startsWith ( packageName ) ) && ( name . endsWith ( myClass ) ) )
person emory    schedule 02.08.2010
comment
Большое спасибо за ваш ответ. Я подозреваю, что ваш ответ, по крайней мере, на правильном пути. Я решил пойти по пути редактора байт-кода. Я начал играть с asm, и я думаю, что у меня есть решение, которое будет работать. Мне все равно понадобится загрузчик классов, и я создам новый класс на лету. - person yspotts; 03.08.2010
comment
Да, и насчет того, о чем вы упомянули: я пытался конкретно выделить, где в имени пакета был только один дополнительный элемент (чтобы com.company.something.somethingelse.MyClass не совпадал по дизайну). Не спрашивай :) - person yspotts; 04.08.2010

Я не думаю, что вы действительно можете сделать это через загрузчик классов. Теоретически, если какой-то другой класс пытается загрузить класс, который, как он предполагает, называется «com.mycompany.foo.MyClass», тогда уже слишком поздно, у кого-то уже есть класс с байт-кодом, ссылающимся на «com.mycompany.foo», и этот класс уже загружен .

Переупаковывать намного проще на статическом уровне, используя что-то вроде ASM для переупаковки всего кода во время сборки. Вы, конечно, должны изменить как сам пакет классов, так и все классы, которые я ссылаюсь на этот пакет.

Если вы используете Maven, проверьте плагин тени. Если нет, я, кажется, вспоминаю инструмент под названием JarJar.

Конечно, вы можете выполнять такие манипуляции с байтовым кодом во время выполнения с помощью javaagent и ClassTransformer. Код для плагина maven-shade-plugin на самом деле довольно маленький — если вы схватите его и вырвете части maven, у вас, вероятно, что-то заработает через 2-3 дня.

person David Blevins    schedule 02.08.2010
comment
Спасибо за ответ. javaagent было бы здорово попробовать - в моем случае это не сильно поможет, но, похоже, это хорошая вещь для изучения. На самом деле я нашел jarjar совсем недавно и посмотрел исходный код. У меня есть несколько хороших идей о том, как продолжить работу над проектом, используя asm. - person yspotts; 03.08.2010