Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Dependency injection Spark microframework alatt

Szűrő megjelenítése

Dependency injection Spark microframework alatt

Elöljáróban fontos kihangsúlyozni, hogy a Spark-ban szándékosan nincs dependency injection, ezzel is spórolva a framework méretén és erőforrás-igényén. Kialakításának köszönhetően azonban kombinálva használható más keretrendszerekkel, így például a Spring-gel is. Természetesen nem lesz szükségünk a teljes Spring framework-re, mivel egyrészt a web réteg kiszolgálását a Spark végzi (embedded Jetty servlet engine segítségével), másrészt pedig dependency injection használatához a Spring Context (és vele a Core) modulja tökéletesen elégséges. Ezzel egyébként elkerüljük a Spark azon (személyes véleményem szerint hátborzongatóan rossz) megközelítését, hogy minden “függés” statikus metódusokkal operál.

Szóval amire szükségünk lesz:

Belépési pont

Mivel a Sparknak nincs szüksége sem web.xml descriptorra, sem WebApplicationInitializer implementációra, a Spring kontextusát azonban el kell indítani, egy standard main metódust kell megírnunk. Erre egyébként a Spark-nak natív formában is szüksége volna, mivel a route regisztrációkat a main-ből lehet inicializálni. A különbség ez esetben az lesz, hogy a Spring kontextusát építjük fel, az egyszerűség kedvéért AnnotationConfigApplicationContext-ként (de persze XML alapon is megoldható, kinek mi a preferáltja), illetve még pár apróságot el kell itt végezni.

public static void main(String[] args) {
 
	// Spring context felépítése annotációs konfigurációval
	ApplicationContext context =
		new AnnotationConfigApplicationContext(ApplicationContextConfig.class);
		
	// Spark servlet engine (Jetty) konfiguráció
	context.getBean(SparkServerConfiguration.class).configureSpark();
	
	// regisztrációk elindítása
	context.getBean(SparkRegistrationAgent.class).start();
}

Spark konfiguráció

Bár az embedded Jetty nem ismer túl sok finomhangolási lehetőséget (például AJP connector beállítására nem találtam lehetőséget), azért az alapvető beállításokat, így például a host címet, ahol a Jettynek figyelnie kell bejövő kérésekre, illetve annak portját természetesen meg lehet változtatni. Másra (például SSL beállítására) nem volt ezúttal szükségem, így ezekre nem térnék ki, a dokumentációban meg lehet találni a szükséges információkat. A beállítás azonban trükkös, mivel a Spark minden-static elméletén elindulva, a server beállításait is statikus metódusokon keresztül lehet elvégezni (a beállítások egy singleton példányon hajtódnak végre), így elvéve a lehetőséget a kényelmes dinamikus konfigurációtól. A megoldásra egy komponenst hoztam létre, mely property source-ból két értéket injektál (spark.host és spark.port), majd a kontextus felépítése után, a komponens configureSpark() metódusát meghívva beállítódik a két paraméter.

@Component
@PropertySource(SparkServerConfiguration.PROPERTY_SOURCE)
public class SparkServerConfiguration {
 
	static final String PROPERTY_SOURCE = "classpath:application.properties";
 
	@Value("${spark.port}")
	private int port;
 
	@Value("${spark.host}")
	private String host;
 
	public void configureSpark() {
		Spark.ipAddress(host);
		Spark.port(port);
	}
}

A második lépésben el kell indítani a route regisztrációkat. Fontos tudni, hogy ekkor még nem fut az embedded server, és azzal nem is kell külön foglalkozni, az első route regisztráció fogja ugyanis azt elindítani. Ezt egy gyűjtőbean-nel oldottam meg, mely az összes SparkRegistration implementációt megkeresi, és egyenként meghívja rajtuk a register() metódust. Nézzük hogyan történik a regisztráció.

Route és filter regisztrációk

A Sparkban minden route és filter a Spark osztály statikus metódusainak meghívásával valósítható meg. Míg a route regisztrációk a .get(), .post(), .put(), .patch() és .delete() metódusokkal lehetséges, a filterek a .before() (pre-processing filter) és .after() (post-processing filter) hívásokkal regisztrálhatóak. Route-ok esetében szükség lesz az útvonalra (nyilván), ami természetesen tartalmazhat paramétereket is (kettősponttal kezdve), illetve egy Route implementációt, ami funkcionális interfészként működik, tehát lambda kifejezésekkel is implementálható. Bemeneti paraméterei a Spark saját Request és Response példányai, kimenete bármi lehet. Filterek esetében hasonló a helyzet, annyi különbséggel a visszatérés nincs, nyilván a Request/Response példányokon operál. Útvonal nélkül megadva minden útvonalra érvényesek lesznek, de szűkíthető a filter aktiválása útvonalra és az Accept-Type header értékére. Mélyebben nem mennék bele, a dokumentációban minden részletesen le van írva.

// ez az interfész teszi lehetővé a két framework kooperációját
// természetesen ez nem része
// sem a Spark, sem a Spring frameworknek
@FunctionalInterface
public interface SparkRegistration {
	void register();
}

Szóval a regisztrációk nem csinálnak mást, csak meghívják a request típusnak megfelelő metódust egy megadott útvonallal és a megfelelő Route implementációval. A példán ez egy lambda-kifejezésben fog realizálódni, illetve a regisztráció így egyúttal a controller szerepét is magán viseli. Nyilván amennyiben további logika szükséges, érdemes még egy controller szintet megkülönböztetni és a controllereket regisztrálni. Azonban ez esetben nem volt szükségem semmilyen további konverzióra, validálásra, effektíve semmire, ami tipikusan egy controllerben foglalna helyet, így közvetlenül a service hívásokat regisztráltam.

@Component
public class EntryByLinkControllerRegistration extends AbstractGetControllerRegistration {
 
	private static final String PATH_ENTRIES_BY_LINK = "/entries/link/:link";
 
	// ...
	
	@Override
	String path() {
		return PATH_ENTRIES_BY_LINK;
	}
 
	@Override
	Route route() {
		return (request, response) ->
			retrievalService.retrieve(extractParameter(request, PathParameter.LINK));
	}
}
abstract class AbstractGetControllerRegistration implements SparkRegistration {
 
	@Value("${spark.context-root}")
	private String contextRoot;
 
	@Override
	public void register() {
		Spark.get(pathWithContextRoot(), route());
		LOGGER.info("[GET {}] route registered.", pathWithContextRoot());
	}
	
	private String pathWithContextRoot() {
		return getContextRoot() + path();
	}
	
	// ...
}

Mivel a failover alkalmazás JSON-ban kell, válaszoljon, ezért egy filterre volt még szükség ami beállítja a response type-ot. Az alábbi példán tehát látható egy Spark.after() hívás, mely a JSON response type beállítására szolgál. Mivel a filterek és route-ok regisztrációjának sorrendje lényegtelen, a filterek is implementálhatják a SparkRegistration interface-t, és teljesen mindegy lesz, az “agent” milyen sorrendben hívja meg őket. Fontos azonban megjegyezni, hogy a Spring-gel ellentétben itt nincs automatikus response konverzió, így a típus beállításával még nem lesz JSON-ba konvertálva a válasz. Ahhoz, hogy a válasz konvertálva legyen, a regisztráció során kell megadnunk egy ResponseTransformer implementációt - ami egyébként egy standard Function-nek felel meg, így egy egyszerű objectMapper::writeValueAsString metódusreferencia is megfelel (feltéve persze, hogy van egy példányunk a hívás helyén). Mivel a failover alkalmazás eleve JSON dokumentumokat tárol, ezt a konverziót is megspóroltam itt.

@Component
public class JSONResponseTypeFilter implements SparkRegistration {
 
	private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
 
	@Override
	public void register() {
		Spark.after(jsonContentTypeFilter());
	}
 
	private Filter jsonContentTypeFilter() {
		return (request, response) -> response.type(CONTENT_TYPE_APPLICATION_JSON);
	}
}

Mint az látható volt a fenti példákon, minden függés Spring dependency injection segítségével lett injektálva, így teljesen elkerültük a statikus metódus hívásokat. Természetesen a Spark saját konfigurációs és regisztrációs hívásait nem kerülhetjük ki, de ez nem is volt cél. Azonban sikerült azokat elwrapelni, így létrehozva egy jól strukturált kódszerkezetet, mely lehetőséget biztosít a Spark dinamikus konfigurálására, azonban megőrzi annak egyszerűségét, de kezünkbe adja a Spring adta dependency injectiont is. Az alkalmazás bootup ideje alig 3-4 másodperc, a futó alkalmazás pedig 30-60 MB memóriát fogyaszt készenlétben, melyben már a failover tükrözési job is jelen van.

A failover alkalmazás forrása

Spark microframework

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.