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
Dinamikus bean regisztrálás és manuális bean qualifier
Az első és egyben legkisebb probléma a kliens általánosítása volt - szerencsére már eleve úgy terveztem meg, hogy rugalmasan kezelhető legyen, így csak némi finomhangolásra volt szükség. Bizonyos esetekben az API támaszkodott backend-specifikus model osztályokra, melyek így értelemszerűen akadályozták volna a többi service-hez való kliensek implementációját. A probléma sokkal inkább ott kezdődött, hogy a Bridge konfigurációja csupán egy service-szel való kommunikációt támogatott (röviden, csak egy service URL-t lehetett beállítani, ahhoz inicializálódott a Jersey). Nyilván így a kliens implementációk sem tudtak arról dönteni, milyen service-szel is akarnak kommunikálni a Bridge-en keresztül.
A probléma megoldása elsőre kézenfekvőnek tűnt: az annotációs konfigurációról központosított konfigurációra átállással létrehozhatóak az egyedi BridgeClient bean-ek, mindegyik a saját service URL-jével, majd qualifierekkel a kliens implementációk tudtára hozható, melyik számára melyik Bridge bean lesz a megfelelő. Az ötletet szinte azonnal elvetettem annak teljes rugalmatlansága miatt - gyakorlatilag egy új service támogatásához a Bridge konfigurációjának módosítására lenne szükség ez esetben, amit nyilván nem akartam. Sokkal inkább gondolkodtam olyan megoldásban, ami lehetővé teszi a Bridge konfigurálását mindössze az integráló alkalmazás konfigurációjának módosításával. Ez azonban felvetette azt a problémát, hogy a BridgeClient beaneket dinamikusan kellene regisztrálnom a kontextusba, méghozzá az alkalmazás konfigurációjának megfelelően.
Mielőtt bemutatnám a probléma megoldását, szót kell ejtenem a Spring két alapkövét képező eszközről, mivel fontos szerepet játszanak majd a megvalósításban. Ezek az eszközök a BeanFactory és a BeanFactoryPostProcessor. A BeanFactory interface implementációinak felelőssége a kontextusban az élő bean instance-ok kezelése - ha nagyon le akarom egyszerűsíteni a dolgot, valójában ennél azért sokkal többet tesznek az implementációk. Vegyük például az AnnotationConfigApplicationContext osztályt, mely egy BeanFactory implementáció, és az ő feladata az annotációs konfigurációra épülő Spring alkalmazás kontextusának teljeskörű kezelése. A dinamikus bean regisztráció miatt szükségünk lesz arra, hogy elérjük az alkalmazás BeanFactory instance-át. Szerencsére ez könnyen megvalósítható, bármilyen bean-be beinjektálható a ConfigurableListableBeanFactory, mely a BeanFactory egy kiterjeszése és olyan metódusokat biztosít, melyekkel manipulálható az alkalmazás kontextusa. A másik eszköz, a BeanFactoryPostProcessor szerepe nem a bean instance-ok, hanem a bean definíciók kezelésében valósul meg. Használatának például ott van jelentősége, ha egy bean definíciójában konfigurációs property-kből származó dinamikus értékek is megjelennek. BeanFactoryPostProcessor segítségével a placeholderek valós értékekre cserélhetőek még az adott bean létrehozása előtt. Ez a viselkedés pedig pont jól jött a probléma megoldása során, bár míg a BeanFactory használata megkerülhetetlennek bizonyult, a BeanFactoryPostProcessor-ral már csak egy kényelmi extra került a megoldásba.
A megoldás tehát két lépésből áll: először is dinamikusan létre kell hozni a BridgeClient bean-eket a konfiguráció alapján és azokat elhelyezni manuálisan a kontextusban, másodszor pedig el kell végezni a BridgeClient beanek manuális hozzárendelését a rajtuk dependáló service-ekhez. Ezutóbbi, mint azt említettem, amolyan kényelmi lépés, a beanek létrehozása után egyszerű Qualifier-ekkel is elvégezhető az összerendelés, de egy kényelmesebb és egyben szigorúbb megoldást kerestem - erről majd később. A BridgeClient beanek létrehozásáért ugyancsak egy bean felel, mely a BridgeClientRegistration nevet kapta.
@Component
@ConfigurationProperties("bridge")
class BridgeClientRegistration {
// ...
@PostConstruct
public void initRegistry() {
Optional.ofNullable(clients)
.orElse(Collections.emptyMap())
.entrySet().stream()
.forEach(entry -> {
// a konkrét BridgeClient létrehozása az aktuális konfigurációs rekord alapján
BridgeClient bridgeClient = bridgeClientFactory
.createBridgeClient(entry.getValue());
// a bean regisztálása ...
configurableListableBeanFactory.registerSingleton(entry.getKey(), bridgeClient);
// ... és inicializálása
// (esetleges callbackek, például a PostConstruct meghívása)
configurableListableBeanFactory.initializeBean(bridgeClient, entry.getKey());
});
}
Fontos, hogy ez a komponens függ a ConfigurableListableBeanFactory-től (egészen pontosan a DefaultListableBeanFactory implementációtól), mely szerencsére a saját bean-jeink létrehozásakor már rendelkezésre áll. Nem is történhetne ez másképp, hiszen ez a komponens már az alkalmazás indulásakor létrejön, méghozzá maga a framework hozza létre. Feladata a bean definíciók kezelése, illetve képes definíció nélkül is a kontextushoz adni bean-eket. A fentebb említett BridgeClientRegistration komponens pontosan ezt teszi:
- felolvassa az alkalmazás konfigurációjának releváns részét (ezek a bridge.clients.xyz.host-url formátumú property-k, ahol az xyz a BridgeClient bean neve lesz),
- majd létrehoz a megadott beállításokkal egy BridgeClient bean-t,
- végül elhelyezi a kontextusban és inicializálja.
Ezután a BridgeClient bean már él, megtalálható a kontextusban, más bean-ek hivatkozhatnak rá - @Autowired
illetve @Qualifier
annotációkra lesz csupán szükség, nyilván a qualifier értéke az említett xyz érték lesz. Bár ezzel a módszerrel tetszőleges számú BridgeClient bean létrehozható, egy óriási szépséghibája azért akad: A Spring intelligens jószág és ügyesen fel tudja építeni a függésfát, aztán a megfelelő sorrendben létrehozza a bean-eket. Persze, csak ha nem próbáljuk hülyeségre utasítani, és minden szükséges komponenst oda is adunk neki. Ez esetben ezutóbbi kritérium nem teljesül, hiszen a BridgeClient bean-eknek nem létezik definíciója az alkalmazás indulásakor, tehát a Spring nem tud velük számolni a függésfa felépítésekor. Ennek megfelelően kicsi az esélye hogy a BridgeClientRegistration bean hamarabb jön létre, mint bármelyik service bean, márpedig előbbi nélkül a BridgeClient bean-ek még nem léteznek - jutalmunk a No bean definition error message lesz. A megoldás egyszerűen annyi, hogy az összes service, ami BridgeClient bean-en dependál, dependáljon a BridgeClientRegistration bean-en. Ezen a ponton már kezdtem borzolni a szemöldököm - jó lenne a megoldás, de kényelmetlen. Úgyhogy továbbmentem.
Első körben az annotáció-kupactól szerettem volna megszabadulni, hiszen minden service implementáció pontosan ugyanazt teszi: függ egy BridgeClient-től és többnyire csak attól, illetve a dinamikus regisztráció miatt függ a BridgeClientRegistration bean-től. Ez minden service esetében ugyanazt az annotáció-négyest jelentette volna: @Service
, @DependsOn
, @Autowired
(ez mondjuk elhagyható) és @Qualifier
. Ezt a négy annotációt vontam össze a @BridgeService
annotációval, melynek egy kötelező paramétere a client. Ez gyakorlatilag ugyanazt az értéket várja, mint a Qualifier várná.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Service
@DependsOn("bridgeClientRegistration")
public @interface BridgeService {
String client();
}
Ahhoz azonban, hogy ez így működőképes legyen, egyedi annotáció feldolgozásra volt szükség - itt jön képbe a BeanFactoryPostProcessor. Bár a BridgeService annotáció egy meta-annotáció a Service fölé, a client attribútum megköveteli az egyedi feldolgozó logikát - nyilván a Spring nem fogja tudni magától, hogy az az attribútum a Qualifier-t hivatott helyettesíteni. Erre szolgál az alább látható BridgeAssigmentBeanFactoryPostProcessor osztály.
@Component
public class BridgeAssignmentBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
// ...
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
// megkeressük az összes bean definíciót, amin megtalálható a BridgeService annotáció
Arrays.stream(beanFactory.getBeanNamesForAnnotation(BridgeService.class))
.forEach(beanName -> {
// felkérjük a bean-t implementáló class-t
Class<?> classToInspect = beanFactory.getType(beanName);
// az annotációból kinyerjük a client attribútumot
String clientName = extractClientName(classToInspect);
// illetve megkeressük a konstruktor megfelelő paraméterét
int bridgeClientConstructorParameterIndex =
findBridgeClientParameterInConstructor(classToInspect);
// végül a megfelelő indexre beszúrjuk a qualifier-t
beanFactory.getBeanDefinition(beanName)
.getConstructorArgumentValues()
.addIndexedArgumentValue(bridgeClientConstructorParameterIndex,
new RuntimeBeanReference(clientName));
});
}
private String extractClientName(Class<?> classToInspect) {
return classToInspect.getAnnotation(BridgeService.class).client();
}
private int findBridgeClientParameterInConstructor(Class<?> classToInspect) {
// elvileg csak egy konstruktor lehet, de... biztos ami biztos
Constructor<?>[] constructors = classToInspect.getConstructors();
if (constructors.length > 1) {
raiseException(String.format(TOO_MANY_CONSTRUCTORS, classToInspect.getName()));
}
// fontos, hogy a konstruktor tartalmazzon BridgeClient paramétert
// ha nincs ilyen, akkor rossz helyen használjuk az annotációt
int index = getBridgeClientParameter(constructors[0]);
if (index == -1) {
raiseException(String.format(BRIDGE_CLIENT_PARAMETER_NOT_FOUND,
classToInspect.getName()));
}
return index;
}
private int getBridgeClientParameter(Constructor<?> constructorToInspect) {
return Arrays.asList(constructorToInspect.getParameterTypes())
.indexOf(BridgeClient.class);
}
}
Mint azt korábban említettem, a factory post processorok még a bean-ek létrehozása előtt tevékenykednek, azok kezelik a bean definíciókat. Első lépésként a processor összeszedi mindazon bean definíciókat, melyeken szerepel a BridgeService annotáció. A lekérés eredménye egy bean-név tömb lesz, mely alapján felkérhetőek a tényleges bean definíciók. Ezután számos művelet végezhető a definíción, fontos persze, hogy ne essünk túlzásokba, hiszen az alkalmazás bean kontextusának teljes félrekonfigurálását okozhatjuk. Amire ezen a ponton szükség lesz, az a konstruktor megvizsgálása: szükségünk lesz a BridgeClient interface indexére a konstruktor paraméterei között. A definíció tartalmazza a konstruktor paramétereinek metainformációit, és azok index alapján módosíthatóak. Ha már tudjuk a megfelelő indexet, azon egy RuntimeBeanReference objektumot kell elhelyeznünk, benne a BridgeService annotáció client attribútumából kinyert BridgeClient bean névvel. Ez az egész művelet gyakorlatilag a Qualifier-rel való minősítésnek felel meg.
A megvalósítás ugyan kicsit körülményes, de utána ebből …
@Service
@DependsOn("bridgeClientRegistration")
class AttachmentBridgeServiceImpl implements AttachmentBridgeService {
private BridgeClient bridgeClient;
@Autowired
public AttachmentBridgeServiceImpl(@Qualifier("leaflet") BridgeClient bridgeClient) {
this.bridgeClient = bridgeClient;
}
// ...
}
… ez lesz - ami a service-ek implementálásánál mindenképp nagy segítséget jelent majd:
@BridgeService(client = "leaflet")
class AttachmentBridgeServiceImpl implements AttachmentBridgeService {
private BridgeClient bridgeClient;
@Autowired
public AttachmentBridgeServiceImpl(BridgeClient bridgeClient) {
this.bridgeClient = bridgeClient;
}
// ...
}
A Bridge modul kódja - mint mindig - az alábbi linken megtekinthető teljes formájában:
A Bridge teljes implementációja
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.