IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 14 - Java server - Vylepšení systému pluginů

V předchozím kvízu, Kvíz - Propagace lokální sítí v Javě, jsme si ověřili nabyté zkušenosti z předchozích lekcí.

V dnešním Java tutoriálu vylepšíme systém pluginů o načítání externích pluginů. Dále přidáme prioritní inicializaci jednotlivých pluginů.

Načítání externích pluginů

Příprava

Abychom mohli načítat jednotlivé pluginy, musíme vytvořit pravidla, která bude třeba dodržet, aby náš systém správně jednotlivé pluginy rozpoznal. My budeme mít tří pravidla:

  1. Jeden JAR soubor bude obsahovat právě jeden plugin
  2. Třída reprezentující plugin musí implementovat rozhraní IPlugin
  3. V Manifestu pluginu musí být přítomná informace o celém názvu třídy s pluginem

Implementace

Všechna magie se bude odehrávat ve třídě PluginModule. Začneme vytvořením třídní konstanty PLUGIN_FILTER, která bude typu FilenameFilter a PLUGIN_IDENTIFIER, která bude typu String:

private static final FilenameFilter PLUGIN_FILTER = (file, name) -> name.contains(".jar");
public static final String PLUGIN_IDENTIFIER = "Plugin-Class";

Filtr nám zajistí, že až budeme procházet složku s pluginy, budeme načítat pouze JAR soubory. Druhá konstanta obsahuje klíčovou hodnotu, kterou budeme později hledat v manifestu, abychom splnili náš 3. bod v požadavcích.

Dále vytvoříme instanční konstantu pluginsFolderPath, která bude obsahovat cestu ke složce s pluginy. Tuto konstantu inicializujeme v konstruktoru z parametru:

private final String pluginsFolderPath;
PluginModule(String pluginsFolderPath) {
    this.pluginsFolderPath = pluginsFolderPath;
}

Pro načtení jednoho pluginu vytvoříme privátní metodu loadPlugin(), která bude v parametru přijímat proměnnou typu File. Tato proměnná reprezentuje soubor s pluginem:

private Optional <IPlugin> loadPlugin(File pluginFile) {
  try {
    final ClassLoader loader = URLClassLoader.newInstance(new URL[] {pluginFile.toURI().toURL()});
    final JarInputStream jis = new JarInputStream(new FileInputStream(pluginFile));
    final Manifest mf = jis.getManifest();
    final Attributes attributes = mf.getMainAttributes();
    final String pluginClassName = attributes.getValue(PLUGIN_IDENTIFIER);
    final Class << ? > clazz = Class.forName(pluginClassName, true, loader);
    final IPlugin plugin = clazz.asSubclass(IPlugin.class).newInstance();
    System.out.println("Přidávám plugin: " + plugin.getName());
    return Optional.of(plugin);
  } catch (Exception e) {
    return Optional.empty();
  }
}
Metoda je celkem složitá, tak si ji pěkně vysvětlíme řádek po řádku
1. Vytvoříme nový class loader, který bude obsahovat cestu k pluginu
2. Ze souboru vytvoříme nový JarInputStream, pomocí kterého budeme číst obsah JAR souboru
3. Z JARu získáme Manifest
4. Z manifestu přečteme všechny atributy
5. Nás konkrétně zajímá atribut "Plugin-Class", který vyžaduje náš systém
6. Pomocí metody Class.forName() získáme třídu (ne instanci) reprezentující plugin
7. Metodou newInstance() vytvoříme novou instanci pluginu. Voláním metody asSubclass() říkáme, že instance bude potomkem nějaké třídy (v našem případě je to rozhraní IPlugin)
8. Vypíšeme do konzole, že jsme úspěšně načetli plugin
9. Vrátíme plugin zabalený do třídy Optional

Pokud jeden z kroků selže, zachytíme výjimku a vrátíme prázdný Optional.

Pro lepší přehlednost kódu vytvoříme další privátní metodu loadExternalPlugins(), která se postará o průchod složkou s pluginy a volání metody loadPlugin(). Metoda bude přijímat jeden parametr typu MapBinder, pomocí kterého se budou jednotlivé pluginy registrovat:

private void loadExternalPlugins(MapBinder <String, IPlugin> pluginBinder) {
  final File pluginsFolder = new File(pluginsFolderPath);
  if (!pluginsFolder.exists() || !pluginsFolder.isDirectory()) {
    return;
  }

  final File[] plugins = pluginsFolder.listFiles(PLUGIN_FILTER);
  if (plugins == null) {
    return;
  }

  Arrays.stream(plugins)
    .map(this::loadPlugin)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .forEach(plugin -> pluginBinder.addBinding(plugin.getName()).to(plugin.getClass()).asEagerSingleton());
}

V metodě nejdříve zkontrolujeme, že existuje cesta ke složce s pluginy a že se opravdu jedná o složku, nikoliv o soubor. Dále pomocí našeho filtru získáme pole souborů, které by měly reprezentovat naše pluginy. Pokud bude složka prázdná, nebudeme nic dělat. Blížíme se k nejzajímavější části metody. Pomocí volání metody Arrays.stream() získáme stream z pole pluginů. Metodou map() se pokusíme načíst plugin. Dále vyfiltrujeme pouze ty pluginy, které se podařilo načíst. Dalším voláním metody map() rozbalíme Optional a získáme přímou referenci na plugin. Nakonec projdeme všechny tyto reference a zaregistrujeme je k ostatním pluginům.

Nyní nám už zbývá pouze zavolat výše vytvořenou metodu. To provedeme na konci metody configure():

loadExternalPlugins(pluginBinder);

Nakonec se přesuneme do třídy Server, kde budeme muset upravit konstruktor PluginModule. Konstruktor třídy PluginModule přijímá ve svém parametru cestu ke složce s pluginy. Zatím žádná taková složka neexistuje, proto předáme konstruktoru pouze prázdný řetězec:

final Injector injector = Guice.createInjector(new ServerModule(), new PluginModule(""));

Prioritní inicializace pluginů

Ve druhé části dnešní lekce implementujeme prioritní inicializaci pluginů. V budoucnu se může stát, že některé pluginy bude potřeba načíst prioritně před ostatními. Dosud jsme neměli žádnou kontrolu nad tím, v jakém pořadí se pluginy budou načítat.

Konfigurace anotacemi

Prioritu inicializace pluginu budeme nastavovat v anotaci PluginConfiguration. Anotaci vytvoříme v balíčku plugins.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface PluginConfiguration {
    int DEFAULT_PRIORITY = 0;
    int priority() default DEFAULT_PRIORITY;
}

Anotací @Retention nastavíme, na jaké úrovni bude naše anotace použitelná. K dispozici jsou tři možnosti:

  • SOURCE - anotace je dostupná pouze ve zdrojovém kódu, během kompilace je odstraněna
  • CLASS - kompilátor anotaci zachová, ale nebude dostupná za běhu programu
  • RUNTIME - kompilátor anotaci zachová a bude dostupná za běhu programu

Anotací @Target nastavíme, k jakému typu členu bude anotaci možné nastavit. K dispozici jsou možnosti:

  • ANNOTATION_TYPE - dostupné pouze pro jiné anotace
  • CONSTRUCTOR - omezení na konstruktor
  • FIELD - omezení na proměnné
  • LOCAL_VARIABLE - omezení na lokální proměnné
  • METHOD - omezení na metody
  • PACKAGE - omezení na balíčky
  • PARAMETER - omezení na parametry metod
  • TYPE - omezení na třídu, rozhraní, anotaci, nebo výčet

Anotací @Documented říkáme, že pokud bychom tvořili JavaDoc, tak tato anotace bude zahrnuta do dokumentace.

Porovnání pluginů

Pro porovnání pluginů dle jejich priority vytvoříme novou třídu PriorityPluginComparator, která bude implementovat rozhraní Comparator. Toto rozhraní bude typované na rozhraní IPlugin.

public class PriorityPluginComparator implements Comparator<IPlugin> {}

Rozhraní nám předepisuje implementovat jedinou metodu compare():

@Override
public int compare(IPlugin o1, IPlugin o2) {
    final PluginConfiguration o1Configuration = o1.getClass().getAnnotation(PluginConfiguration.class);
    final PluginConfiguration o2Configuration = o2.getClass().getAnnotation(PluginConfiguration.class);

    if (o1Configuration == null && o2Configuration == null) {
        return 0;
    }

    final int o1Priority = o1Configuration == null ? PluginConfiguration.DEFAULT_PRIORITY : o1Configuration.priority();
    final int o2Priority = o2Configuration == null ? PluginConfiguration.DEFAULT_PRIORITY : o2Configuration.priority();

    return Integer.compare(o1Priority, o2Priority);
}

V první části metody pomocí volání getAnnotation() získáme buď naší anotaci PluginConfiguration, nebo null, pokud anotace není přítomna. Anotace získáme pro oba porovnávané pluginy. Pokud ani jeden plugin nemá anotaci, jsou si rovny, tedy vrátíme hodnotu 0. Pokud alespoň jeden plugin anotaci obsahuje, je přečtena její priorita. Nakonec se vrátí porovnání priorit z přečtených anotací.

Třídění pluginů

Přesuneme se do třídy Server, kde se inicializují pluginy. Přidáme privátní metodu getSortedPlugins(), která nám vrátí seřazenou kolekci pluginů podle priority, od nejvyšší, po nejnižší:

private List<IPlugin> getSortedPlugins() {
    final List<IPlugin> pluginList = new ArrayList<>(plugins.values());
    pluginList.sort(new PriorityPluginComparator());
    Collections.reverse(pluginList);

    return pluginList;
}

Standardní komparátor řadí hodnoty vzestupně. Pokud chceme hodnoty sestupně, NIKDY bychom neměli upravovat samotný komparátor, ale využít k tomu knihovní metodu reverse() ze třídy Collections.

Na začátku metody initPlugins() získáme seřazené pluginy zavoláním metody getSortedPlugins() a uložíme si je do lokální proměnné pluginList. Touto proměnnou nahradíme ve všech třech cyklech zdrojovou kolekci:

private void initPlugins() {
    final List<IPlugin> pluginList = getSortedPlugins();

    for (IPlugin plugin : pluginList) {
        plugin.init();
    }

    for (IPlugin plugin : pluginList) {
        plugin.registerMessageHandlers(eventBus);
    }

    for (IPlugin plugin : pluginList) {
        plugin.setupDependencies(plugins);
    }
}

Tím bychom měli hotovou prioritní inicializaci pluginů a vlastně i první polovinu seriálu za sebou. Pokud jste došli až sem, tak vám blahopřeji. Své názory a připomínky pište do komentářů dole pod článkem.

V příští lekci, Java chat - Klient - Seznámení se s kostrou aplikace, se zaměříme na implementaci chatu.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 20x (159.94 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Předchozí článek
Kvíz - Propagace lokální sítí v Javě
Všechny články v sekci
Server pro klientské aplikace v Javě
Přeskočit článek
(nedoporučujeme)
Java chat - Klient - Seznámení se s kostrou aplikace
Článek pro vás napsal Petr Štechmüller
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity