Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Dinamikus bean regisztrálás és manuális bean qualifier

Szűrő megjelenítése

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

Kommentek

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

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