Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
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.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.