Oříšek v reflexní analýze generik

Minulý týden jsem řešil zajímavý problém s reflexí a došel jsem k závěru, že generiky v reflexním API jsou opravdu velká legrace. Prototypoval jsem myšlenku automatického generování implementací nad obecným kontejnerem - dejme tomu Map (což není pro účely tohoto článku zase až tak důležité), a došel jsem k potřebě správně číst generické informace z deklarací tříd. Právě této, na první pohled jednoduché, věci, bych chtěl věnovat dnešní článek.

Problém mohu ukázat na příkladě:

Chtěl bych mít jednotné API pro přístup k primárnímu klíči.


public interface IdAccessor {
   T getId();
   void setId(T id);
}

Pak mám obecného předka Album:


public abstract class Album implements IdAccessor {
   public abstract S getFeaturedMediaItem();
   public abstract List getMediaItems()
   //další metody
}

A konkrétní implementace (PhotoAlbum, VideoAlbum, AudioAlbum) atd.:


public abstract class PhotoAlbum extends Album {}

V AOP proxy mám následně přístup k reflexnímu obrazu metody getFeaturedMediaItem a rád bych vrátil dynamickou proxy třídy, která typově odpovídá deklaraci. Volání:


Album.class.getDeclaredMethod("getFeaturedMediaItem").getReturnType()

mi však vrátí třídu Media. V kontextu třídy PhotoAlbum bych ale rád získal specifický typ Media a to je Photo - to je přeci z deklarace Java třídy krásně vidět. Tak jak se k této informaci dostat pomocí reflexe? Na rovinu říkám, že čím víc o generikách vím, tím víc mi připadají jako velmi komplexní rébus. Připadám si trošku jako když účetní nakoukne do učebnic kvantové fyziky.

Když odhlédnu od toho, že mě zajímá obecný princip jak se dostat k informaci o konkrétní třídě odpovídající zástupnému generickému symbolu, mohu se v tomto případě k cílovému typu dostat takto:


public void testGenerics() throws Exception {
	Class resolvedClass = null;
	//toto je naše S
	Type searchedType = Album.class.getDeclaredMethod("getFeaturedMediaItem").getGenericReturnType();
	//tako získáme kontextovou informaci, co je S v případě třídy PhotoAlbum
	ParameterizedType genericSuperClass = ((ParameterizedType)PhotoAlbum.class.getGenericSuperclass());
	//na předkovi se podíváme na deklarované parametry
	Type[] typeParameters = PhotoAlbum.class.getSuperclass().getTypeParameters();
	//na generickém obrazu nadřízené třídy se podíváme na "vyhodnocené" generické argumenty
	Type[] resolvedParameters = genericSuperClass.getActualTypeArguments();
	//projdeme všechny deklarované parametry na třídě, a porovnáme je s hledaným typem v návratové hodnotě
	for(int i = 0; i < typeParameters.length; i++) {
		Type rawParameter = typeParameters[i];
		if(searchedType.equals(rawParameter)) {
			resolvedClass = (Class)resolvedParameters[i];
		}
	}
	//a našli jsme námi hledanou třídu Photo
	assertSame(Photo.class, resolvedClass);
}

Legrační že? Mě tedy trvalo docela dlouho, než jsem do toho trošku pronikl. Teď ještě vymyslet způsob, jak tuto analýzu provést obecně na libovolné hierarchii tříd. V reflexním API se pracuje s obecným interfacem Type, který může být reprezentován několika různými podtypy:

  • Class: klasický jednoduchý typ jako třeba String nebo Integer[] - to je v podstatě to, co hledáme
  • TypeVariable: proměnný typ - např. T - to je něco co potřebujeme vyhodnotit
  • ParameterizedType: parametrizovaný typ, např. List nebo Set - to je něco, co nám dokáže poskytnout informace o přiřazení TypeVariable na Class, minimálně jsme schopni přes to získat tzv. "upper bounds"
  • GenericArrayType: generické pole - tedy List[] nebo T[]
  • WildcardType: wildcard, např. ? extends Number nebo ? super Long - tady na tomhle objektu jsme schopní se zeptat na "upper bounds" a "lower bounds"

Pouze v případě ParameterizedType se dokážeme zjistit, jestli některému proměnnému typu nebyla přiřazena konkrétní class - díky volání metody getActualTypeArguments(). Problémem zůstává jak zjistit, co bylo čemu vlastně přiřazeno. Tady musíme porovnávat původní typy získané přes getTypeParameters() s naším hledaným typem v TypeVariable. Celé je to dost zamotané, protože vyhodnocené typy a původní typy získáváme přes různá volání - tj. getSuperClass() a getGenericSuperClass(). Porovnání typů se navíc nesmí dělat porovnáním referencí (==), ale přes volání metody equals(). Zkrátka a prostě, je to taková dobrá mentální rozcvička. Po pár hodinách zkoušení jsem ve svém kódu došel k implementaci, která mi dokázala vyhodnotit konkrétní typy přes celou hierarchii nadřízených tříd a všech odkazovaných interfaců (ke stažení zde).

Nejvtipnější na tom celém je to, že když jsem do celé záležitosti jakž tak pronikl, tak jsem našel hledanou utilitu hotovou a vyladěnou ve Spring Frameworku. Pro příklad uvádím jednoduchý test, který získá informace, které jsem hledal (plus další test na extenzi generické HashMap pro ukázku resolvování generického typu v argumentu metody):


public class GenericsTest extends TestCase {
	public void testGenericsWithSpring() throws Exception {
		Class resolvedClass = GenericTypeResolver.resolveReturnType(
				Album.class.getDeclaredMethod("getFeaturedMediaItem"), PhotoAlbum.class
		);
		assertSame(Photo.class, resolvedClass);
	}
	public void testGenericsMapWithSpring() throws Exception {
		Class resolvedType = GenericTypeResolver.resolveParameterType(
				new MethodParameter(
						HashMap.class.getDeclaredMethod("put", Object.class, Object.class), 0
				), MyMap.class
		);
		assertSame(String.class, resolvedType);
	}
	private static class MyMap extends HashMap {}
}

Tím, že jsem si prošel delší a trnitější cestou, jsem se minimálně donutil vztahům v reflexi generik trošku porozumět (i když na rovinu říkám, že si pořád nejsem moc jistý v kramflecích). Na druhou stranu se čím dál víc přesvědčuji o tom, že knihovna Springu v sobě skrývá nečekané poklady - jen je umět najít. Neuplyne půl roku abych nějaký podobný poklad neobjevil.

Jen mě stále trápí otázka, proč tak jednoduché API pro dotazování generik jaké má Spring - zopakujme si:


class GenericTypeResolver {
   static Class resolveParameterType(MethodParameter methodParam, Class clazz);
   static Class resolveReturnType(Method method, Class clazz);
}

UŽ NENÍ K DISPOZICI V ZÁKLADNÍCH BALÍCÍCH JAVY !!!

Zdroje ze kterých jsem čerpal