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:
- Jeden JAR soubor bude obsahovat právě jeden plugin
- Třída reprezentující plugin musí implementovat rozhraní
IPlugin
- 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 metodyasSubclass()
ří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ěnaCLASS
- kompilátor anotaci zachová, ale nebude dostupná za běhu programuRUNTIME
- 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é anotaceCONSTRUCTOR
- omezení na konstruktorFIELD
- omezení na proměnnéLOCAL_VARIABLE
- omezení na lokální proměnnéMETHOD
- omezení na metodyPACKAGE
- omezení na balíčkyPARAMETER
- omezení na parametry metodTYPE
- 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 22x (159.94 kB)
Aplikace je včetně zdrojových kódů v jazyce Java