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á:
- Jeden JAR súbor bude obsahovať práve jeden plugin
- Trieda reprezentujúci plugin musí implementovať rozhranie
IPlugin
- 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ódyasSubclass()
hovoríme, že inštancia bude potomkom nejaké triedy (v našom prípade je to rozhranieIPlugin
) - 8. Vypíšeme do konzoly, že sme úspešne načítali plugin
- 9. Vrátime plugin zabalený do triedy
Optional
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 programuRUNTIME
- 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ácieCONSTRUCTOR
- obmedzenia na konštruktorFIELD
- obmedzenia na premennéLOCAL_VARIABLE
- obmedzenia na lokálne premennéMETHOD
- obmedzenia na metódyPACKAGE
- obmedzenia na balíčkyPARAMETER
- obmedzenia na parametre metódTYPE
- 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