Создание собственного 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 file = new File(javac);
        if(!file.isFile())
            throw new FileNotFoundException("Компилятор по адресу "+ file.getAbsolutePath() +" недоступен ");
        System.out.println("Компилируем " + file.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, который возвращает ошибку, если компилируемый код написан неправильно.

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

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