Создание собственного ClassLoader в Java

Описываю создание собственного ClassLoader в Java.

Собственные загрузчики классов нужны для реализации рефлексии в Java.

Мы загружаем файл-класс «на лету» и исполняем его методы.

Как загрузить Java класс на лету

Чтобы загрузить Java класс нам нужен подготовленный файл с байт-кодом, имеющий расширение .class, однако в приведенном ниже примере никаких заготовок я не использую, и создаю класс так же — «на лету», используя Java компилятор (javac) и класс Runtime в коде программы.

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Method;
import java.util.Properties;

public class Main {
    public static void main(String[] args) throws Exception {
        
        //Получаем доступ ко всем Properties
        Properties p = System.getProperties();
        //Получаем разделитель, используемый в операционной системе
        String sep = p.getProperty("file.separator");
        //Получаем путь к папке JRE
        String jrePath = p.getProperty("java.home");

        //Выполняем необходимые манипуляции для получения пути к файла javac (в моем случае javac.exe)
        int lastIndex = jrePath.lastIndexOf(sep);
        String javac = jrePath.substring(0, lastIndex) + sep + "bin" + sep + "javac";
        if(p.getProperty("sun.desktop").equals("windows"))
            javac+=".exe";
        else javac+=".sh";

        //Проверяем, существует ли такой файл (javac.exe)
        File jc = new File(javac);
        if(!jc.isFile())
            throw new FileNotFoundException("Компилятор по адресу "+ jc.getAbsolutePath() +" недоступен ");

        System.out.println("Компилируем " + jc.getAbsolutePath() + " " + file.getAbsolutePath());

        //Запускаем компилятор javac, чтобы получить байт код внешнего класса
        Process p1 = Runtime.getRuntime().exec(javac+" "+file.getAbsolutePath());

        //Если javac завершился с ошибкой, выбрасываем Exception (здесь он самописный)
        //т.к. мне необходимо было проверять синтаксис класса, который подключался.
        //Таким образом, если возникала ошибка компиляции, то процесс p1 мог вернуть текст
        //ошибки (поток байт) с помощью функции getErrorStream()
        if(p1.waitFor()!=0)
            try {
                throw new MyClassCompilationException("Ошибка компиляции", p1);
            } catch (MyClassCompilationException e) {
                e.printStackTrace();
                return;
            }

        //Здесь мы уже имеем созданный файл с байт-кодом
        System.out.println("Компиляция завершена");

        //Формируем абсолютный путь к файлу с байт-кодом
        int pointIndex = file.getAbsolutePath().lastIndexOf(".");
        String absulutePatch = file.getAbsolutePath().substring(0, pointIndex);

        //Объявляем MyClassLoader. Класс ClassLoader является абстрактным
        //поэтому необходимо его переопределить (каким образом, будет показано ниже)
        MyClassLoader loader = new MyClassLoader();

        //Объявляем переменную типа Class.
        Class cl = loader.findClass(absulutePatch);
        System.out.println(cl);

        //Получаем метод m1 из загруженного класса
        Method method = cl.getMethod("m1", new Class[] {String.class, int.class});
        System.out.println(method);
        //Выполняем метод m1. Нельзя забывать про метод newInstance(), если метод динамический.
        method.invoke(cl.newInstance(), new Object[]{"Test", 8});

        //Выполняем метод m2. Он статический, поэтому newInstance() в методе invoke писать не надо
        Method method2 = cl.getMethod("m2", new Class[]{String.class});
        method2.invoke(cl, "QWERRTY");
    }
}

Загружаемый класс выглядит таким образом:

public class Hello {
  public void m1(String text, int c) {
    for(int i = 0; i < c; i++) {
      System.out.println(text + " Hi"+i);
    }
  }

  public static void m2(String text) {
    System.out.println(text + " from static method");
  }
}

Класс MyClassLoader будет загружать созданный класс в статический контекст, чтобы мы могли обращаться к его методам.

MyClassLoader имеет следующий вид:

import java.io.*;

public class MyClassLoader extends ClassLoader {

    //Переопределяем метод findClass, которому надо передать путь к файлу с расширением .class
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //Проверяем, существует ли такой файл
        File f = new File(name+".class");
        if(!f.isFile())
            throw new ClassNotFoundException("Нет такого класса " + name);

        InputStream ins = null;

        try{
            //С помощью потока считываем файл в массив байт
            ins = new BufferedInputStream(new FileInputStream(f));
            byte[]b = new byte[(int)f.length()];
            ins.read(b);
            //С помощью функции defineClass загружаем класс
            Class c = defineClass("Hello", b, 0, b.length);
            return c;

        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException("Проблемы с байт кодом");
        }
        finally {
            try {
                ins.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

С первом блоке кода, начиная со строки 54 мы начинаем работать с загруженным классом.

Результат работы приведенного выше кода выглядит следующим образом:

ClassLoader

Здесь приведена загрузка простого класса с простыми методами, однако ничего не мешает загрузить туда более емкие классы.

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

В данном случае этим процессом является компилятор javac, который возвращает ошибку, если компилируемый код написан неправильно.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *