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

Рубрика: Java | 14 January 2008, 13:31 | juriy

В последнее время плагины (подключаемые модули) используются везде: в средах разработки, в браузерах, в файловых менеджерах и в медиа-плеерах. Сложно найти серьезное приложение, которое не предоставляло бы возможности себя расширить. Даже небольшой текстовый редактор 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 или что-то еще.

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

Исходники: plugins.zip

Комментариев: 52

52 Responses to “Создание pluggable решений при помощи Java.”

Комментарии:

  1. dimat

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

  2. Egor

    Спасибо большое за статью, очень полезно

  3. Chabster

    > Плагины получили контролируемый доступ к ядру
    А что помешает интроспекцией получить доступ ко всему остальному?

    Ты не коснулся очень важной темы в отношении загрузчиков классов – защита и домены защиты.

  4. juriy

    Да, тему защиты я не трогал по одной простой причине – я в ней недостаточно хорошо разобрался, чтобы её описывать. Но я думал о ней немного в другом свете: “ужесточение” песочницы. К примеру, не дать плагину открывать сетевые подключения. Или давать, только спросив разрешения пользователя.

    Если можно, немного подробнее про интроспекцию, я не совсем понял как при помощи этого API можно “задеть” ядро.

  5. Chabster

    Передавая плагину (сайту) интерфейс на свой объект ты фактически передаешь сам объект со всеми его ссылками на другие объекты “ядра”. Через интроспекцию можно до них добраться.

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

    public JButton getButton() {
    return button;
    }

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

    Класс, хоть и анонимный, но содержит неявную ссылку на екземпляр класса контейнера.

    Вообще, code access security (CAS) – одна из самых сложных (и неосвоенных) тем в управляемых языках. В .NET CAS порядком сложнее. При этом 90%-в разработчиков не знает, что это вообще такое)

  6. Enterit

    To Chabster – респект за вопрос. Мне тоже очень интересно послушать что-то внятное по этой теме. Я думаю, это ключевой вопрос для многих расширяемых решений.

  7. juriy

    Chabster,

    > Класс, хоть и анонимный, но содержит неявную ссылку на екземпляр класса контейнера.

    Как её (эту ссылку) получить? Можешь кинуть пару строк кода, которые, к примеру, выгрузят произвольный плагин из “реестра”. Действительно, очень интересно.

  8. Chabster

    Пару дней томитесь в ожидании) потом запостю.

  9. Andrey

    Я так понимаю что интроспекцию в данном случае можно заюзать точно так же как и рефлекшн(почему то думается что интроспекция через рефлекш как раз и работает).
    Я думаю что PluginContext может оградить плагин от kernel обьектов с помощью того, что он будет обращаться к kernel обьектам(и наоборот) с помощью методов, модификаторы доступа которых не позволят плагину выйти напрямую на обьекты ядра. По крайней мере я бы так сделал.

  10. Andrey

    Фигню написал :)

  11. Andrey

    Вобщем как я думаю если в PluginContext все ссылки на обьекты ядра помечены как private, то плагин не сможет к ним добраться никак. Единснвенная лазейка которую я вижу это вызвать у обьекта класса PluginContext getClass().getClassLoader() и получить таким образом достук к его класс лоадеру, поэтому видимо класс PluginContext нужно загружать тоже каким то изолированным класс лоадером, и общатся с обьектами ядра через интерфейсы которые этому класс лоадеру доступны.

  12. Chabster

    Вам повезло, я заболел… :(

    Это плагин, который загружается вторым.

    package com.juriy.hello;

    import java.lang.reflect.Field;
    import java.lang.reflect.Method;

    import com.juriy.plug.Plugin;
    import com.juriy.plug.PluginContext;

    public class HelloPlugin implements Plugin {

    private PluginContext pc;

    public void invoke() {
    System.out.println(“Plugin 1″);
    pc.getButton().setText(“Plugin 1″);
    }

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

    try {
    final Class pcClass = pc.getClass();
    Field parentThisField = pcClass.getDeclaredField(“this$0″);
    parentThisField.setAccessible(true);
    Object parentObject = parentThisField.get(pc);

    System.out.println(“Going to use ” + parentObject.getClass().getName() + “class to fuck things up…”);

    final Method removePluginByNameMethod = parentObject.getClass().getDeclaredMethod(“removePluginByName”, String.class);
    removePluginByNameMethod.setAccessible(true);
    removePluginByNameMethod.invoke(parentObject, “hello.jar”);

    }
    catch (Exception ex) {
    ex.printStackTrace();
    return;
    }

    }
    }

  13. Chabster

    Andrey:
    причем два раза фигню написал)))

  14. Andrey

    2Chabster

    Согласен, я пропустил этот трик. Но даже дефолтный SecurityManager запрещает это делать. То есть достаточно где нибуть в инициализации проги прописать:

    System.setSecurityManager(new SecurityManager());

    и этот трик уже не заработает.

  15. Chabster

    Andrey: Да, не заработает. Правда, приложение в целом…
    access denied (java.io.FilePermission plugins read)

    А если плагину нужна интроспекция для функционирования, что делать будешь? Это раз.
    И два – как коду плагина запретить интроспекцию исключительно переданных объектов?

    Я щас не готов разглагольствовать на эту тему. Возможно, дойдут руки начеркать в свой блог. Хоть я и не люблю жабу…

  16. Andrey

    2 Chabster:

    Интроспекция – это работа с public полями и методами и дефолтный секьюрити менеджер ее ни в коей мере не ограничивает.
    Очевидно что ексепшн описанный тобой легко убирается с помощью создания соответствующей политики безопасности.
    В посте, который ты видимо из-за завышенного самомнения назвал фигней, я предложил сделать ссылку на обьекты ядра private в PluginContext . Вместе с правильно сконфигурированным секьюрити менеджером это полностью решило бы проблему, описанную в приведенном тобой примере(триком я там считаю вызов метода setAccessible(true)). Я не исклучаю что могут найтись другие трики , поэтому видимо лудше поднапрячься и создать(тому кому это действително нужно) политику безопасности максимално ограничиваушчую плагины, иначе плагин может например отослать по мылу файл с хешами паролей из локальной файловой системы, и доступ к обьектам ядра будет казаться сущим пустяком.

  17. Chabster

    Андрюша, интроспекция появилась задолго до бинов или жабы вообще. http://www.ibm.com/developerworks/library/l-pyint.html

    На этой ноте моя переписка с тобой прекращается. Спасибо за внимание)

  18. Juriy

    Chabster,

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

    Ушел читать про SecurityManager’ы.

  19. Vadim Voituk

    Согласен с пойледним постом Андрея – предложенный механизм полностью “защитит” приложение от плугина.
    Осталось дело за малым – привести пример такой конфигурации SecurityManager-а (я себе пока слабо её представляю).

  20. Andrey

    Как Proof of Concept у меня заработал следующий вариант:

    run.bat:

    java -Djava.security.manager -Djava.security.policy=./plugins.policy -cp bin com.juriy.plug.Bootstrap

    plugins.policy:

    grant codeBase “file:/C:/downloads/Plugins/PlugApp/plugins/-” {
    };

    grant codeBase “file:/C:/downloads/Plugins/PlugApp/bin/-” {
    permission java.security.AllPermission;
    };

  21. Vlad

    Метод removePluginByName на самом деле же не выгружает плугин?
    Есть ли возможность убрать конкретный класс из памяти?

  22. Andrey

    Насколько я знаю, когда загружается новый класс, то старый выгружается, если на него нету каких то ссылок(например не существует обьектов этого класса). Читал об этом где то в инете. Если попробовать погуглить по фразе java unload class, я думаю можно найти такую инфу.

  23. Enterit

    Нельзя в Java классы выгружать!
    Можно только позволить Garbage Collector-у это сделать, если обнулить все ссылки на класслоадер этого класса. И не факт, что он тут же кинется его выгружать. Хотя, обнулить класслоадер может быть достаточно для ваших целей.

    Да, теоретически, можно изменить класс на лету (Эклипс, например, в дебаге умеет подхватывать изменённый класс во время выполнения программы), но это у же другая история…

  24. Juriy

    Да, все правильно. И еще, не следует забывать, что любой объект, загруженный Class Loader’ом содержит на него ссылку. Так что пока будет жив хотя-бы один объект – CL тоже будет жить.

    Ситуация абсолютно аналогична ситуации с обычными объектами (а ClassLoader, по сути, таким и является). Можно обнулить ссылку и подождать, пока сработает GC, можно попросить VM постараться запустить GC как можно быстрее. Ни один из указанных методов не сможет гарантировать моментальной выгрузки.

    >> Эклипс, например, в дебаге умеет подхватывать изменённый класс во время выполнения программы

    Есть подозрение, что это возможность VM а не IDE.

  25. Enterit

    > Есть подозрение, что это возможность VM а не IDE.
    Естественно VM. Эклипс лишь её умело пользует.

  26. Samolisov Pavel

    Большое спасибо за статью! Очень помогла понять многие моменты, особенно при работе с класслоадерами (в частности понятие парент-класслоадера). Разобрался как изолировать ядро от плагинов, правда завязал все это через Guice (IoC-контейнер от гугла). Еще раз, спасибо.

  27. F@N

    Люди я столкнулся с такой проблемой – делаю свинговое приложение, на кнопку повесил заполнение таблицы в базе данных и выборку нужных данных в JasperReport? для печати, всё пашет супер… но проблемма в том что когда я розпечатываю отчёт, отчётзакрывается и закрывает мою прогу, или если я просто закрываю сформированный отчёт, он тоже закрывает мою прогу… а это довольно плохо… как с этим боротся?

  28. scof

    2Chabster
    спасибо за информацию, только твое сообщение от January 16th, 2008 at 1:58 am очень неудачное. Непонятно, что ты хотел сказать этой ссылкой… 2002 год, Питон… Эта ссылка не подтверждает твои слова. Есть подозрение, что интроспекшен появился все-таки вместе с нелюбимой тобой явой.

  29. Pavel

    По ссылке на исходники ничего не находит :( Можно снова исходники выложить?

  30. Vadim Voituk

    Pavel,
    К сожалению после последнего краша данных на сервере мы потеряли все изображения и архивы блога.

  31. Juriy

    У меня где-то есть бекапы этих исходников, я напишу когда смогу восстановить их.

  32. Dimon

    Исходники умерли…

  33. Juriy

    Они не пережили одного из падений сервера. Постараюсь завтра восстановить.

  34. Alex

    Отличная стаnья, жаль исходников нет :(
    Был бы признателен еслиб кто то их востановил :)

  35. игоръ

    Здравствуйте! Очень жаль,что исходников нет(( Их не удалось восстановить?
    Дело в том что Java не может найти класс PluginLoadException. Может быть хотя бы его получиться выложить?
    Спасибо!

  36. BeCase

    2игоръ:
    Вот простая реализация PluginLoadException:
    public class PluginLoadException extends Exception{
    public PluginLoadException(String s){
    super(s);
    }
    public PluginLoadException(Exception e){
    super(e);
    }
    }
    Если это ещё актуально :).

  37. Bogdan

    Juriy, отличная статья, спасибо. Разобрался в этом вопросе практически с нуля. Так как исходников нет, копировал из статьи, благо, что всё подробно.

    Заметил опечатку.
    В Bootstrap, в методе main, когда инициализируем:
    URLClassLoader appLoader = new URLClassLoader(new URL[]{binDirURL}, commonsLoader);
    переменная “binDirURL” указывает не на джарник, а на папку. Из-за этого случается “ClassNotFoundException”.

    За эту опечатку отдельное спасибо, благодаря ей перечитал кучу всего о ClassLoader’ах и тп ;)

  38. proxima

    Отличная статья: масса важной информации в одном месте, которая позволяет сразу продумывать логику приложения. БиГ РеспекТ!

  39. Vitaly

    Здравствуйте Юрий,

    Прочитал вашу статью “Создание pluggable решений при помощи Java”. Очень понравилась, но вы в ней не затрагиваете вопросы взаимодействия между плагинами. А это очень важно. Как расширить функциональность одного плагина в другом? Как воспользоваться функциональностью, реализованной в некотором плагине? Не планируете ли вы написать дополнение к вашей статье? Очень хотелось бы увидеть.

    С уважением, Виталий.

  40. Bogdan

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

    Спасибо, Богдан.

  41. Дима

    Дяденьки, у кого сохранились исходники, ну или хотя бы структуры проектов, что где лежит

  42. BeCase

    Ну вообще-то в статье достаточно подробно изложено, что нужно сделать, так что можно и без исходников обойтись.

  43. Дима

    хорошо, тогда хотя бы структуру проектов, раз уж так сложно

  44. Juriy

    Привет всем,

    в действительности, сложно. Потому что после одного из крайне неудачных падений сервера мы потеряли всю статику на блоге (картинки, исходники, и.т.п.) поэтому, для нас это будет тот же кусок работы – перечитать заметочку и по памяти восстановить проект.

    Если кто-то будет восстанавливать проект по коду из заметки, вы сделаете огромное отдолжение, если поделитесь работающим кодом с остальными читателями (можно просто кинуть архив авторам этого блога). Мы, в свою очередь, обязательно разместим ссылку на ваш блог/сайт/профить LinkedIn в этой заметке.

  45. Juriy

    Vitaliy, Bogdan,

    Спасибо за отзывы!

    Эта заметка, скорее обзор подходов и методик работы с плагинами, а не “коммерческая реализация” части движка. В ближайшее время я не планирую писать продолжение. К сожалению, времени на то, чтобы поддерживать блог совсем не осталось. Да и ко всему прочему, сейчас я существенно больше времени трачу на JavaScript чем на Java :)

    Что касается Groovy – это отличный язык для разработки всех тех приложений, которые можно разрабатывать на Java. С Groovy вы получаете несколько существенных приимуществ. Если вы используете Groovy, обратите внимание на Groovy++ – http://code.google.com/p/groovypptest/ Думаю, будет довольно интересно.

    // Juriy

  46. Vitaly

    Юрий, я сейчас уже почти доделал свой платинный движок. Там есть возможности для взаимодействия между плагинами. Если интересно потом выложу все на свой будущий блог. А вообще лучше не создавать велосипедов и юзать какую-нибудь из OSGI -реализаций (Apache Felix или Equinox). Там все уже есть.

  47. Juriy

    Про велосипеды, полностью согласен. Но всё же понимать, как этот самый велосипед устроен, никогда не помешает :)

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

  48. Дима

    Здравствуйте, Юрий. Проделал я, в общем, все. Вроде все как посте. Дописал метод addPlugin. Ссылка с моего one.ubuntu
    http://ubuntuone.com/p/Jcg/

  49. Дима

    забыл… в архиве готовые проекты Eclipse

  50. Juriy

    Большое спасибо, сегодня-завтра обновим пост, добавим туда ваши исходники.

Leave a Reply