IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

14. diel - Java server - Vylepšenie systému pluginov

Vitajte u záverečnej lekcie zo série o programovaní servera v Jave. Minule, v lekcii Java server - Propagácia lokálnou sieťou (3. časť) , sme dokončili propagáciu servera lokálnou sieťou. V dnešnom Java tutoriálu vylepšíme systém pluginov o načítanie externých pluginov. Ďalej pridáme prioritné inicializácii jednotlivých pluginov.

Načítanie externých pluginov

Príprava

Aby sme mohli načítať jednotlivé pluginy, musíme vytvoriť pravidlá, ktoré bude treba dodržať, aby náš systém správne jednotlivé pluginy rozpoznal. My budeme mať troch pravidlá:

  1. Jeden JAR súbor bude obsahovať práve jeden plugin
  2. Trieda reprezentujúci plugin musí implementovať rozhranie IPlugin
  3. V Manifestu pluginu musí byť prítomná informácie o celom názve triedy s pluginom

Implementácia

Všetky mágie sa bude odohrávať v triede PluginModule. Začneme vytvorením triedny konštanty PLUGIN_FILTER, ktorá bude typu FilenameFilter a PLUGIN_IDENTIFIER, ktorá bude typu String:

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

Filter nám zaistí, že až budeme prechádzať zložku s pluginy, budeme preberať iba JAR súbory. Druhá konštanta obsahuje kľúčovú hodnotu, ktorú budeme neskôr hľadať v manifeste, aby sme splnili náš 3. bod v požiadavkách.

Ďalej vytvoríme inštančný konštantu pluginsFolderPath, ktorá bude obsahovať cestu k priečinku s pluginmi. Túto konštantu inicializujeme v konstruktoru z parametra:

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

Pre načítanie jedného pluginu vytvoríme privátne metódu loadPlugin(), ktorá bude v parametri prijímať premennú typu File. Táto premenná reprezentuje súbor s pluginom:

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();
  }
}
Metóda je celkom zložitá, tak si ju pekne vysvetlíme riadok po riadku
1. Vytvoríme nový class loader, ktorý bude obsahovať cestu k pluginu
2. Zo súboru vytvoríme nový JarInputStream, pomocou ktorého budeme čítať obsah JAR súboru
3. Z jari získame Manifest
4. Z manifestu prečítame všetky atribúty
5. Nás konkrétne zaujíma atribút "Plugin-Class", ktorý vyžaduje náš systém
6. Pomocou metódy Class.forName() získame triedu (nie inštanciu) reprezentujúci plugin
7. Metódou newInstance() vytvoríme novú inštanciu pluginu. Volaním metódy asSubclass() hovoríme, že inštancia bude potomkom nejaké triedy (v našom prípade je to rozhranie IPlugin)
8. Vypíšeme do konzoly, že sme úspešne načítali plugin
9. Vrátime plugin zabalený do triedy Optional
Ak jeden z krokov zlyhá, zachytíme výnimku a vrátime prázdny Optional.

Pre lepšiu prehľadnosť kódu vytvoríme ďalšie privátne metódu loadExternalPlugins(), ktorá sa postará o priechod zložkou s pluginmi a volanie metódy loadPlugin(). Metóda bude prijímať jeden parameter typu MapBinder, pomocou ktorého sa budú jednotlivé pluginy registrovať:

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 metóde najskôr skontrolujeme, že existuje cesta k priečinku s pluginmi a že sa naozaj jedná o zložku, nie o súbor. Ďalej pomocou nášho filtra získame pole súborov, ktoré by mali reprezentovať naše pluginy. Pokiaľ bude zložka prázdna, nebudeme nič robiť. Blížime sa k najzaujímavejšie časti metódy. Pomocou volanie metódy Arrays.stream() získame stream z poľa pluginov. Metódou map() sa pokúsime načítať plugin. Ďalej vyfiltruje iba tie pluginy, ktoré sa podarilo načítať. Ďalším volaním metódy map() rozbalíme Optional a získame priamu referenciu na plugin. Nakoniec prejdeme všetky tieto referencie a zaregistrujeme ich k ostatným pluginom.

Teraz nám už zostáva len zavolať vyššie vytvorenú metódu. To vykonáme na konci metódy configure():

loadExternalPlugins(pluginBinder);

Nakoniec sa presunieme do triedy Server, kde budeme musieť upraviť konštruktor PluginModule. Konštruktor triedy PluginModule prijíma vo svojom parametri cestu k priečinku s pluginmi. Zatiaľ žiadna taká zložka neexistuje, preto odovzdáme konstruktoru len prázdny reťazec:

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

Prioritné inicializácia pluginov

V druhej časti dnešnej lekcie implementujeme prioritné inicializáciu pluginov. V budúcnosti sa môže stať, že niektoré pluginy bude treba načítať prioritne pred ostatnými. Doteraz sme nemali žiadnu kontrolu nad tým, v akom poradí sa pluginy budú načítať.

Konfigurácia anotáciami

Prioritu inicializácia pluginu budeme nastavovať v anotácii PluginConfiguration. Anotáciu vytvoríme v balíčku plugins.

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

Anotácií @Retention nastavíme, na akej úrovni bude naša anotácie použiteľná. K dispozícii sú tri možnosti:

  • SOURCE - anotácia je dostupná iba v zdrojovom kóde, počas kompilácie je odstránená
  • CLASS - kompilátor anotáciu zachová, ale nebude dostupná za behu programu
  • RUNTIME - kompilátor anotáciu zachová a bude dostupná za behu programu

Anotácií @Target nastavíme, k akému typu členu bude anotáciu možné nastaviť. K dispozícii sú možnosti:

  • ANNOTATION_TYPE - dostupné iba pre iné anotácie
  • CONSTRUCTOR - obmedzenia na konštruktor
  • FIELD - obmedzenia na premenné
  • LOCAL_VARIABLE - obmedzenia na lokálne premenné
  • METHOD - obmedzenia na metódy
  • PACKAGE - obmedzenia na balíčky
  • PARAMETER - obmedzenia na parametre metód
  • TYPE - obmedzenia na triedu, rozhranie, anotáciu, alebo zoznam

Anotácií @Documented hovoríme, že ak by sme tvorili javadoc, tak táto anotácia bude zahrnutá do dokumentácie.

Nákupný pluginov

Pre porovnanie pluginov podla ich priority vytvoríme novú triedu PriorityPluginComparator, ktorá bude implementovať rozhranie Comparator. Toto rozhranie bude typizovanej na rozhraní IPlugin.

public class PriorityPluginComparator implements Comparator<IPlugin> {}

Rozhranie nám predpisuje implementovať jedinú metódu 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 prvej časti metódy pomocou volania getAnnotation() získame buď našej anotáciu PluginConfiguration, alebo null, ak anotácia nie je prítomná. Anotácia získame pre oboch porovnávanú pluginy. Pokiaľ ani jeden plugin nemá anotáciu, sú si rovné, teda vrátime hodnotu 0. Ak aspoň jeden plugin anotáciu obsahuje, je prečítaná jej priorita. Nakoniec sa vráti nákupný priorít z prečítaných anotácií.

Triedenie pluginov

Presunieme sa do triedy Server, kde sa inicializujú pluginy. Pridáme privátne metódu getSortedPlugins(), ktorá nám vráti zoradenú kolekciu pluginov podľa priority, od najvyššej po najnižšiu:

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

    return pluginList;
}

Štandardné komparátor radí hodnoty vzostupne. Ak chceme hodnoty zostupne, NIKDY by sme nemali upravovať samotný komparátor, ale využiť k tomu knižničný metódu reverse() z triedy Collections.

Na začiatku metódy initPlugins() získame zoradené pluginy zavolaním metódy getSortedPlugins() a uložíme si ich do lokálnej premennej pluginList. Touto premennou nahradíme vo všetkých troch cykloch zdrojovú kolekciu:

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 by sme mali hotovú prioritné inicializáciu pluginov a vlastne aj prvú polovicu seriálu za sebou. Ak ste došli až sem, tak vám blahoželám. Svoje názory a pripomienky píšte do komentárov dole pod článkom. V budúcej lekcii, Java chat - Klient - Zoznámenie sa s kostrou aplikácie , sa zameriame na implementáciu chatu.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 21x (159.94 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Java server - Propagácia lokálnou sieťou (3. časť)
Všetky články v sekcii
Server pre klientskej aplikácie v Jave
Preskočiť článok
(neodporúčame)
Java chat - Klient - Zoznámenie sa s kostrou aplikácie
Článok pre vás napísal Petr Štechmüller
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity