MegaDiablo

Четверг, 28.03.2024, 16:06
Приветствую Вас Гость

Регистрация
Вход

Каталог статей


Главная » Статьи » Программирование » Java

Создание pluggable решений при помощи Java

Создание pluggable решений при помощи Java.

В последнее время плагины (подключаемые модули) используются везде: в средах разработки, в браузерах, в файловых менеджерах и в медиа-плеерах. Сложно найти серьезное приложение, которое не предоставляло бы возможности себя расширить. Даже небольшой текстовый редактор Notepad++ в котором я сейчас набираю текст этой заметки позволяет подключать плагины.

Эта заметка посвящена тому, как разработать pluggable приложение при помощи Java.

На первый взгляд, для того чтобы позволить кому-то расширить ваше приложение нужно совсем не много: определить пару-тройку интерфейсов, накидать в classpath jar’ов с плагинами, файл с описанием плагинов, запустить приложение, обработать файл-описатель и инициализировать плагины.

Такой подход прост в реализации, но имеет ряд существенных недостатков.

1. Плагины должны всегда находиться в classpath приложения, а это не всегда возможно.
2. Если по каким-то причинам, в classpath оказались jar’ы в которых содержится несколько классов с одинаковыми именами, то поведение приложения станет непредсказуемым.
3. Плагины получают доступ к объектам "ядра” приложения. Это нежелательно: плагины должны жить в своей собственной "песочнице”.
4. Для того чтобы подключить новый плагин необходим перезапуск приложения. Это далеко не всегда допустимо. Если речь идет о разработке некоторой серверной архитектуры где плагины – это сервисы, перезагружать сервер ради добавления сервиса неоправданно.

Попробуем разобраться, как создать приложение, лишенное этих недостатков. Для того чтобы сделать это, необходимо реализовать механизм, который позволит подгружать нужные классы во время исполнения приложения и ограничивать их "зону видимости”. В Java таким механизмом являются Class Loader’ы.

Введение в Class Loader’ы.

Классический вопрос: как идентифицируется класс внутри JVM? Обычный ответ – при помощи полного имени: имя пакетов плюс имя самого класса. Этот ответ не совсем верный – в JVM вполне нормально могут существовать два разных класса с одинаковыми _полными_ именами. Класс в JVM однозначно определяется своим полным именем и ClassLoader’ом, который его загрузил.Изоляция ядра приложения.

ClassLoader’ы в Java имеют иерархическую структуру. Это означает, что loader-наследник "видит” все "свои” классы плюс классы loader’а-родителя.

К тому моменту, как JVM начинает исполнять метод main, существует уже три class loader’a.

1. Bootstrap class loader – вершина иерархии ClassLoader’ов. Именно он загружает классы Core Java.
2. Extension class loader – загружает библиотеки из lib/ext. Наследуется от Bootstrap.
3. System class loader – загружает классы из classpath. Наследуется от Extension.

Метод main, естественно инициализирован в System class loader’е. Что же происходит, когда вы пытаетесь обратиться к какому-нибудь классу, (создать экземпляр, или выполнить Class.forName(…) ) который содержится в библиотеках из lib/ext?

В этом месте автора статьи начали терзать смутные сомненья, а что будет если jar с исполняемым классом положить в lib/ext???

"Правильные” class loader’ы должны спросить у родительского loader’а есть ли у него такой класс, и только если родитель у себя этот класс не обнаружит – пытаться загрузить класс самостоятельно. То есть, происходит вот что:

1. System Class loader пробует загрузтиь класс, используя Extension CL.
2. Extension CL "просит” Bootstrap загрузить класс
3. Bootstrap не может загрузить класс, ведь ему видны только Core классы. А искомый класс лежит в библиотеке из lib/ext
4. Extension CL пытается загрузить класс. Ему это удается, и он возвращает класс в System Class Loader.
5. System CL не пытается что-нибудь загружать. Класс уже найден.

Угадайте, какой класс будет реально загружен, если в lib/ext и в classpath есть классы с одинаковым именем? Правильно, класс из lib/ext – на classpath никто и смотреть не будет. То же самое касается и порядка элементов в classpath – класс будет загружен из первого по очереди источника. То есть, если есть a.jar и b.jar и оба содержат класс com.foo то

[java]

java -cp a.jar;b.jar ...

[/java]

будет использовать класс из a.jar, а

[java]

java -cp b.jar;a.jar ...

[/java]

из b.jar.

Повторим еще раз. Class Loader видит "свои” классы, и классы своих "предков”. Ни классы из Class Loader’ов потомков, ни, тем более, классы из "параллельных” CL он видеть не будет. Более того, для JVM – это _разные_ классы.

При попытке привести один класс к другому, в таком случае, JVM честно выдаст ClassCastException. Нисмотря на то, что имена классов одинаковые.

"Песочница” для плагинов – шаг первый.

Чтобы изолировать плагины друг от друга достаточно загружать их в отдельных ClassLoader’ах. Этим и займемся.

Перед тем, как начинать, определим public интерфейс, который должны будут наследовать все плагины.

[java]

public interface Plugin {
public void invoke();
}

[/java]

Для того чтобы протестировать концепцию этого вполне хватит. В реальном приложении, естественно, лучше добавить пару-тройку hook методов и дать плагину хоть одним глазком взглянуть на среду, в которой он исполняется, иначе от него будет мало толку.

Этот интерфейс понадобится при разработке plugin’ов. Поэтому я положу его в отдельный jar c названием plugin-api и буду включать в проекты-плагины. Кроме того, неплохо бы создать тестовый плагин. Ничего оригинального, естественно, он делать не будет.

[java]

public class HelloPlugin implements Plugin {

public void invoke() {
System.out.println("Hello world");
}
}

[/java]

Плагин я создаю в отдельном проекте, чтобы основное приложение не "увидело” классы плагина раньше Runtime’а.

Теперь осталось запаковать плагин в отдельный jar и положить в папку plugins основного приложения. Тут я немного "смухлюю”. Приложение будет знать о том, какой класс необходимо инициализировать для запуска плагина, но как только мы протестируем простейший вариант загрузки, мы устраним это безобразие и вынесем имя класса в файл-описание.

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

[java]

package com.juriy.hello;

import com.juriy.plug.Plugin;

public class HelloPlugin implements Plugin {
public void invoke() {
System.out.println("That's the second plugin");
}
}

[/java]

Теперь, когда все готово, запустим плагины из основного приложения. При этом каждый плагин будет запущен в своем Class Loader’е.

Писать собственный CL не придется, вполне подойдет существующий. Класс URLClassLoader отлично справляется с задачей. Чтобы создать новый экземпляр URLClassLoader в конструктор этого класса нужно передать массив url’ов (папок и jar-файлов) и, указать объект ClassLoader, который URLClassLoader будет считать своим родителем. Если родителя явно не передавать, URLClassLoader будет пронаследован от _текущего_ CL. А если передать null – то от Bootstrap CL. На этот нюанс следует обращать внимание.

Получаем массив jar-файлов из папки plugins
[java]

 File pluginDir = new File("plugins");

File[] jars = pluginDir.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(".jar");
}
});

[/java]

Для каждого файла из папки создаем отдельный URLClassLoader и получаем объект типа Class по имени.

[java]

 Class[] pluginClasses = new Class[jars.length];

for (int i = 0; i < jars.length; i++) {
try {
URL jarURL = jars[i].toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarURL});
pluginClasses[i] = classLoader.loadClass("com.juriy.hello.HelloPlugin");

} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

[/java]

Создаем по объекту из каждого класса и вызываем метод invoke():

[java]

 for (Class clazz : pluginClasses) {
try {
Plugin instance = (Plugin) clazz.newInstance();
instance.invoke();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

[/java]

Теперь смело запускаем приложение. В консоли должен отобразиться текст:

[java]

Hello world
That's the second plugin

[/java]

Как видите, плагины не вступили в конфликт - они попросту не обратили друг на друга внимания.

Дескрипторы.

Обращение к плагину по заранее известному имени - дело нехитрое. Добавим в плагины файл-дескриптор, в котором можно будет указать, какой файл следует вызвать. Кроме того, для наглядности, наше приложение будет GUI приложением, а плагины будут отображаться в виде кнопок. При нажатии на кнопку будет вызван метод invoke() соответствующего плагина.

Файл-дескриптор будет простым properties файлом. Что-то вроде такого:

[java]

main.class = com.juriy.hello.HelloPlugin
button.text = Plugin 1

[/java]

Эту информацию, как и объект plugin'а удобно держать в отдельном классе. В этом же классе будут жить методы по загрузке плагина.

[java]

public class PluginInfo {
private Plugin instance;

private String buttonText;

private JButton associatedButton;

public PluginInfo(File jarFile) throws PluginLoadException {
try {
Properties props = getPluginProps(jarFile);
if (props == null)
throw new IllegalArgumentException("No props file found");

String pluginClassName = props.getProperty("main.class");
if (pluginClassName == null || pluginClassName.length() == 0) {
throw new PluginLoadException("Missing property main.class");
}

buttonText = props.getProperty("button.text");
if (buttonText == null || buttonText.length() == 0) {
throw new PluginLoadException("Missing property button.text");
}

URL jarURL = jarFile.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarURL});
Class pluginClass = classLoader.loadClass(pluginClassName);
instance = (Plugin) pluginClass.newInstance();
} catch (Exception e) {
throw new PluginLoadException(e);
}
}

public Plugin getPluginInstance() {
return instance;
}

public String getButtonText() {
return buttonText;
}

private Properties getPluginProps(File file) throws IOException {
Properties result = null;
JarFile jar = new JarFile(file);
Enumeration entries = jar.entries();

while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().equals("plugin.properties")) {
// That's it! Load props
InputStream is = null;
try {
is = jar.getInputStream(entry);
result = new Properties();
result.load(is);
} finally {
if (is != null)
is.close();
}
}
}
return result;
}

public void setAssociatedButton(JButton associatedButton) {
this.associatedButton = associatedButton;
}

public JButton getAssociatedButton() {
return associatedButton;
}
}

[/java]

Ну а в Main добавим отображение симпатичного фрейма, который будет рисовать по кнопке для каждого плагина. Кроме того оформим класс Main более "прилично". Вынесем всю логику из метода main в метод start и добавим пару полей.

[java]

 private Map plugins;

private JFrame mainFrame;

public MainApp() {

}

public void start() {
File pluginDir = new File("plugins");

File[] jars = pluginDir.listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(".jar");
}
});

plugins = new HashMap();

for (File file : jars) {
try {
plugins.put(file.getName(), new PluginInfo(file));
} catch (PluginLoadException e) {
e.printStackTrace();
}
}

mainFrame = new JFrame("Plugin test");
final JFrame frame = mainFrame;
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(150, 300);
frame.getContentPane().setLayout(new FlowLayout());

synchronized (plugins) {
for (PluginInfo pluginInfo : plugins.values()) {
final PluginInfo plugin = pluginInfo;
final JButton button = new JButton(pluginInfo.getButtonText());
plugin.setAssociatedButton(button);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
plugin.getPluginInstance().invoke();
}
});
frame.getContentPane().add(button);
}
}

frame.setVisible(true);
}

public static void main(String[] args) {
new MainApp().start();
}

[/java]

Изоляция ядра приложения.

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

Техника для изоляции ядра та же что и для изоляции плагинов - загрузить ядро в отдельном ClassLoader'е. Но при этом необходимо учитывать некоторые нюансы: есть набор классов, которые должны видеть и плагины и ядро: как минимум, к этим классам относится интерфейс Plugin. Не забывайте, мы не можем загрузить этот класс отдельно для каждого ClassLoader'а иначе с точки зрения JVM это будут разные классы, и попытка привести один класс к другому будет выкидывать ClassCastException. Также необходимо передать в плагины ссылку на те части приложения, которые они реально могут изменить.

Перепишем метод main так чтобы ядро приложения и плагины запускались в паралельных ClassLoader'ах. Для того, чтобы получить такой эффект я реализую механизм похожий на механизм загрузки Tomcat.

В первую очередь, в JVM будет загружаться не само приложение, а небольшой клacc Bootstrap. Bootstrap инициализирует новый ClassLoader - наследник Bootstrap CL, который будет содержать классы общие для ядра и плагинов. По аналогии с Tomcat этот class loader будет называться Commom CL. У него будет наследник - App CL, который будет содержать классы ядра. Все Class Loader'ы плагинов также будут наследниками Common.

Bootstrap
|
Common
/ / \
App Plug1 Plug2

Таким образом, выполняя немного модифицированный код из PluginInfo:

[java]

URLClassLoader classLoader = new URLClassLoader(new URL[]{jarURL},
getClass().getClassLoader().getParent());

[/java]

мы будем получать Class Loader производный от Common и ссылок на Core классы в нем не будет.

Итак, Bootstrap тоже будет простым и бесхитростным:

[java]

 public static void main(String[] args) throws Exception{

File commonsDir = new File("commons");

File[] entries = commonsDir.listFiles();
URL[] urls = new URL[entries.length];

for (int i = 0; i < entries.length; i++) {
urls[i] = entries[i].toURI().toURL();
}

URLClassLoader commonsLoader = new URLClassLoader(urls, null);

URL binDirURL = new File("bin").toURI().toURL();
URLClassLoader appLoader = new URLClassLoader(new URL[]{binDirURL}, commonsLoader);

Class appClass = appLoader.loadClass("com.juriy.plug.MainApp");
Object appInstance = appClass.newInstance();
Method m = appClass.getMethod("start");
m.invoke(appInstance);
}

[/java]

Файл plugin-api.jar ушел в папку commons. В эту папку теперь можно положить любые библиотеки - они будут видны и плагинам и ядру одновременно.

Обратите внимание, в последнем блоке мы использовали reflection API. Только так можно работать с методами класса из другого ClassLoader'а.

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

Окружение плагина как правило называют его контекстом. Создадим интерфейс PluginContext через который будем передавать плагину ссылки на его собственную кнопку и на главный фрейм.

[java]

public interface PluginContext {
public JButton getButton();

public JFrame getFrame();
}

[/java]

В интерфейс Plugin добавим метод init, который на вход будет принимать PluginContext.

Осталось "внедрить" context в плагин. Сделаем это сразу после инициализации кнопки.

[java]

plugin.getPluginInstance().init(new PluginContext() {

public JButton getButton() {
return button;
}

public JFrame getFrame() {
return frame;
}
});

[/java]

Теперь из плагина можно обратиться к основному приложению.

[java]

public class HelloPlugin implements Plugin {

private PluginContext pc;

public void invoke() {
System.out.println("That's the second");
pc.getButton().setText("Other text");
}

public void init(PluginContext pc) {
this.pc = pc;
}
}

[/java]

Динамическая загрузка/выгрузка плагинов.
Этот раздел выйдет совсем маленьким - он тут скорее "для полноты картины". Имея уже созданную инфраструктуру, дописать код для добавления/удаления плагинов совсем не сложно. Все что нужно сделать - проверить существования плагина в реестре (роль реестра у нас выполняет Map plugins). Загрузить/удалить плагин и отобразить/удалить нужную кнопку. Тут я приведу пример метода remove. Метод add абсолютно аналогичен.

[java]

 public void removePluginByName(String jarName) throws PluginLoadException {
if (!plugins.containsKey(jarName)) {
throw new PluginLoadException(jarName + " not loaded");
}

PluginInfo pluginInfo = plugins.get(jarName);

mainFrame.remove(pluginInfo.getAssociatedButton());
mainFrame.validate();
mainFrame.repaint();

synchronized (plugins) {
plugins.remove(jarName);
}
}

[/java]

Снова повторюсь, в реальном приложении плагину может понадобиться выполнить дополнительные действия по закрытию ресурсов. Поэтому неплохо иметь shutdown hook - еще один метод в интерфейсе Plugin, который будет вызываться непосредственно перед завершением работы плагина.

Заключение.
В этой заметке я показал, как написать простое Java приложение, которое поддерживает плагины. При помощи механизма ClassLoader'ов нам удалось изолировать плагины друг от друга и от ядра приложения. Плагины получили контролируемый доступ к ядру - они умеют изменять параметры кнопки и фрейма.

Конечно, эта заметка только набросок - в реальном приложении вам может понадобиться больше возможностей и функционала: к примеру, взаимодействие между плагинами, корректная обработка ошибок, load on demand или что-то еще.

Надеюсь, эта заметка дала вам общее представление, каким образом можно разработать простое расширяемое приложение.



Источник: http://voituk.kiev.ua/2008/01/14/java-plugins/
Категория: Java | Добавил: MegaDiablo (23.01.2011)
Просмотров: 4128 | Рейтинг: 5.0/1
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Меню сайта
Категории раздела
Android [4]
Web [2]
Java [1]
JavaScript [1]
Форма входа
Поиск
Наш опрос
Оцените мой сайт
Всего ответов: 24
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0