среда, 16 ноября 2011 г.

Простой способ создания Java-приложения с поддержкой плагинов

Все мы знаем, что "монолитное" приложение легче писать только в том случае, если оно небольшое. Если мы планируем расширение приложения, (особенно если делать это будут сторонние разработчики) стоит сразу задуматься о модульной структуре. "Но плагины, это же так сложно!" - думал я раньше. И ошибался. Плагины в java-приложении - это удивительно просто. Чтобы проиллюстрировать это сделаем простое Swing приложение с поддержкой плагинов.

Давайте посмотрим, как такое приложение может быть организовано:



Тут Plugin API Library - библиотека, включающая в себя набор интерфейсов, определяющих методы, котрорые ядро приложения будет использовать для доступа к функционалу плагина. В нашем, простейшем, случае она содержит только один интерфейс:

package net.multipi.jd.api;

import javax.swing.JButton;

public interface Plugin {

  public JButton getButton();
}
Эту библиотеку мы включим во все плагины и поставим разработчикам плагинов только одно условие: их библиотека должна содержать класс, реализующий наш интерфейс. Ни название класса ни имя пакета значения не имеют. Также нам безразлична внутренняя структура плагина: он может иметь сколько угодно классов и пакетов.
Сделаем простую Plugin Library: она по клику на кнопку выведет на экран MessageBox. Опять только один класс:

package net.multipi.jd.test;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import net.multipi.jd.api.Plugin;

public class PluginImpl implements Plugin {

  @Override
  public JButton getButton() {
    JButton button = new JButton("Test");
    button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent ae) {
        JOptionPane.showMessageDialog(null, "Message from plugin");
      }
    });
    return button;
  }
}
А теперь самое интересное: как плагины "включаются" в приложение? 
Для загрузки плагинов мы в модуле Application core используем такой код:

package net.multipi.jd.plugin;

import java.io.File;
import java.io.FileFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import net.multipi.jd.api.Plugin;

public class PluginFactory {

  public static ArrayList<Plugin> getPlugins() {
    ArrayList<Plugin> rez = new ArrayList<Plugin>();
    File pluginDir = new File("plugins");
    File[] jars = pluginDir.listFiles(new FileFilter() {
      @Override
      public boolean accept(File file) {
        return file.isFile() && file.getName().endsWith(".jar");
      }
    });
    for (int i = 0; i < jars.length; i++) {
      try {
        URL jarURL = jars[i].toURI().toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{jarURL});
        JarFile jf = new JarFile(jars[i]);
        Enumeration<JarEntry> entries = jf.entries();
        while (entries.hasMoreElements()) {
          String e = entries.nextElement().getName();
          if (!e.endsWith(".class")) continue;
          e = e.replaceAll("/", ".");
          e = e.replaceAll(".class", "");
          Class<?> plugCan = classLoader.loadClass(e);
          Class<?>[] interfaces = plugCan.getInterfaces();
          for (Class interf : interfaces) {
            if (interf.getName().endsWith(".Plugin")) {
              Class c = classLoader.loadClass(plugCan.getName());
              Object inst = c.newInstance();
              rez.add((Plugin)inst);
            }
          }
        }
      } catch (Exception e) {
        e.printStackTrace(System.err);
      }
    }
    return rez;
  }
}
Рассмотрим подробнее, что тут происходит.
Во-первых, мы вычитываем список .jar файлов из директории "plugins" в массив File[] jars. 
Во вторых, для каждого найденного файла мы создаём отдельный URLClassLoader. Зачем? А представьте себе, что разработчики, не зная о существовании друг друга, случайно использовали одинаковые имена пакетов и классов в своих плагинах. Если все плагины будут загружены одним ClassLoader-ом, неизбежен конфликт. Разные ClassLoader-ы - наша защита от таких коллизий.
В-третьих, нам нужно найти стартовый класс плагина, но сначала найдём все классы в jar. Для этого мы используем класс JarFile, позволяющий получить список содержимого дл каждой из библиотек. Все найденные файлы с расширением .class мы загружаем нашим UrlClassLoader-ом и получаем для каждого из них список реализуемых интерфейсов. Класс, который реализует интерфейс Plugin и будет нашим искомым "стартовым" классом. Получаем его инстанс и добавляем в список.
Теперь, когда плагины загружены осталось вызвать у каждого метод для получения кнопки (объявленный в API-интерфейсе) и отрисовать форму приложения:

package net.multipi.jd;
 
import java.awt.FlowLayout;
import java.awt.Toolkit;
import java.util.ArrayList;
import javax.swing.UIManager;
import net.multipi.jd.api.Plugin;
import net.multipi.jd.plugin.PluginFactory;
 
public class MainJFrame extends javax.swing.JFrame {
 
    public MainJFrame() {
        try {
            javax.swing.UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ex) {
            ex.printStackTrace(System.err);
        }        
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setSize(400, 300);
 getContentPane().setLayout(new FlowLayout());
        createDesktop();
    }
 
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new MainJFrame().setVisible(true);
            }
        });
    }
 
    private void createDesktop() {
        ArrayList<Plugin> plugins = PluginFactory.getPlugins();
        for (Plugin p : plugins) {
            this.getContentPane().add(p.getButton());
        }
    }
}
  

В результате получаем:

3 комментария:

  1. Спасибо. Но хотелось бы увидеть более реалистичный пример применения плагинов. Как именно они могут использоваться в реальном приложении?

    ОтветитьУдалить
  2. не-а, не работает.

    java.lang.ClassCastException: net.multipi.jd.test.PluginImpl cannot be cast to net.multipi.jd.api.Plugin

    ОтветитьУдалить