Описываю создание собственного 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 мы начинаем работать с загруженным классом.
Результат работы приведенного выше кода выглядит следующим образом:
Здесь приведена загрузка простого класса с простыми методами, однако ничего не мешает загрузить туда более емкие классы.
Если будет нужно, опубликую, как взаимодействовать с внешним процессом так, чтобы получать от него сообщение об ошибке в виде потока байт.
В данном случае этим процессом является компилятор javac, который возвращает ошибку, если компилируемый код написан неправильно.