Table of contents
- source code;
- rationale;
- client type dispatching interface;
- type dispatcher implementation;
- type dispatcher unit test;
- type arguments resolver contract;
- type arguments resolver implementation
- type arguments resolver unit test
All source code packaged as a Maven project may be downloaded here
I wrote about utility class for generic type arguments processing earlier. However, it became obvious that its contract may be extended and implementation can be greatly improved as I started to use the class. General considerations:
- define generic interface that works in terms of Type (Class was used earlier). E.g. it shoud be legal to provide Comparable as an object of ParameterizedType class as a 'base' type and get ParameterizedType from processing method back;
- avoid type casts as much as possible;
Let's start from the second concern. Standard java reflection api defines generics-related interfaces hierarchy with root java.lang.reflect.Type (ParameterizedType, TypeVariable etc). However, the api doesn't provide convenient way to work with it. I mean the following situation - suppose you have a reference variable which static type is 'Type'. There is no way to check if it, say, 'ParameterizedType' over than direct examination ('instanceof', Class.isAssignableFrom() etc).
Conclusion - we need to perform such manual checks but they should be localized as much as possible in order to have most of the code work without them. Also that functionality should be flexible enough to be reusable at another generics-related tasks.
I solved it via classic GoF Visitor pattern. Here is a base interface to be used at the client side:
TypeVisitor.java
package org.springcontrib.util.generic;
import java.lang.reflect.*;
/**
* Stands for the visitor from <code>GoF Visitor</code> pattern for {@link Type} dispatching.
*
* @author Denis Zhdanov
* @since Oct 28, 2009
*/
public interface TypeVisitor {
void visitParameterizedType(ParameterizedType type);
void visitWildcardType(WildcardType type);
void visitGenericArrayType(GenericArrayType type);
void visitTypeVariable(TypeVariable<? extends GenericDeclaration> type);
void visitClass(Class<?> clazz);
void visitType(Type type);
}
Convenient adapter implementation:
TypeVisitorAdapter.java
package org.springcontrib.util.generic;
import java.lang.reflect.*;
/**
* Implements {@link TypeVisitor} with empty mehod bodies.
*
* @author Denis Zhdanov
* @since Nov 27, 2009
*/
public class TypeVisitorAdapter implements TypeVisitor {
@Override
public void visitParameterizedType(ParameterizedType type) {
}
@Override
public void visitWildcardType(WildcardType type) {
}
@Override
public void visitGenericArrayType(GenericArrayType type) {
}
@Override
public void visitTypeVariable(TypeVariable<? extends GenericDeclaration> type) {
}
@Override
public void visitClass(Class<?> clazz) {
}
@Override
public void visitType(Type type) {
}
}
And eventually type dispatcher, the only place that encapsulates all explicit type casts:
package org.springcontrib.util.generic;
import java.lang.reflect.*;
/**
* Allows to dispatch object reference of static type {@link Type} to particular subtype if any.
* <p/>
* This class is not singleton but offers single=point-of-usage field {@link #INSTANCE}.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since Oct 28, 2009
*/
public class TypeDispatcher {
/** 'Single-point-of-usage' field. */
public static final TypeDispatcher INSTANCE = new TypeDispatcher();
/**
* Allows to dispatch given <code>'type'</code> to the actual type to the given visitor.
* <p/>
* <b>Note:<b> there is a theoretical possibility that given <code>'type'</code> reference corresponds
* to more than one target type defined at {@link TypeVisitor} (e.g. it may implement {@link ParameterizedType}
* and {@link WildcardType} interfaces). All corresponding <code>'visitParameterizedType()'</code> methods are called then
* (their order is undefined).
* <p/>
* The only exception to the rules described above is a {@link TypeVisitor#visitType(Type)} - it's called
* <b>only</b> when given <code>'type'</code> object doesn't implement any interested {@link Type}
* sub-interface and it's not IS-A {@link Class}.
* <p/>
* Thread-safe.
*
* @param type target {@link Type} object to dispatch
* @param visitor visitor to use for type dispatching
* @throws IllegalArgumentException if any of the given arguments is <code>null</code>
*/
public void dispatch(Type type, TypeVisitor visitor) throws IllegalArgumentException {
if (type == null) {
throw new IllegalArgumentException("Can't process TypeDispatcher.dispatch(). Reason: given 'type' "
+ "argument is null");
}
if (visitor == null) {
throw new IllegalArgumentException("Can't process TypeDispatcher.dispatch(). Reason: given 'visitor' "
+ "argument is null");
}
boolean matched = false;
if (type instanceof ParameterizedType) {
visitor.visitParameterizedType((ParameterizedType)type);
matched = true;
}
if (type instanceof WildcardType) {
visitor.visitWildcardType((WildcardType)type);
matched = true;
}
if (type instanceof GenericArrayType) {
visitor.visitGenericArrayType((GenericArrayType)type);
matched = true;
}
if (type instanceof TypeVariable) {
visitor.visitTypeVariable((TypeVariable<?>)type);
matched = true;
}
if (type instanceof Class) {
visitor.visitClass((Class<?>)type);
matched = true;
}
if (!matched) {
visitor.visitType(type);
}
}
}
Simple mock unit test for the dispatcher at jmock2 format:
package org.springcontrib.util.generic;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.*;
/**
* @author Denis Zhdanov
* @since 10/28/2009
*/
public class TypeDispatcherTest {
private TypeDispatcher dispatcher;
private Mockery mockery;
private TypeVisitor visitor;
@Before
@SuppressWarnings("unchecked")
public void setUp() throws Exception {
dispatcher = new TypeDispatcher();
mockery = new Mockery() {{
setImposteriser(ClassImposteriser.INSTANCE);
}};
visitor = mockery.mock(TypeVisitor.class);
}
@After
public void tearDown() {
mockery.assertIsSatisfied();
}
@Test(expected = IllegalArgumentException.class)
public void nullType() {
dispatcher.dispatch(null, visitor);
}
@Test(expected = IllegalArgumentException.class)
public void nullVisitor() {
dispatcher.dispatch(String.class, null);
}
@Test
public void pureType() {
mockery.checking(new Expectations() {{
one(visitor).visitType(with(any(Type.class)));
}});
dispatcher.dispatch(mockery.mock(Type.class), visitor);
}
@Test
public void pureParameterizedType() {
mockery.checking(new Expectations() {{
one(visitor).visitParameterizedType(with(any(ParameterizedType.class)));
}});
dispatcher.dispatch(mockery.mock(ParameterizedType.class), visitor);
}
@Test
public void pureWildcardType() {
mockery.checking(new Expectations() {{
one(visitor).visitWildcardType(with(any(WildcardType.class)));
}});
dispatcher.dispatch(mockery.mock(WildcardType.class), visitor);
}
@Test
public void pureGenericArrayType() {
mockery.checking(new Expectations() {{
one(visitor).visitGenericArrayType(with(any(GenericArrayType.class)));
}});
dispatcher.dispatch(mockery.mock(GenericArrayType.class), visitor);
}
@Test
public void pureTypeVariable() {
mockery.checking(new Expectations() {{
one(visitor).visitTypeVariable(with(any(TypeVariable.class)));
}});
dispatcher.dispatch(mockery.mock(TypeVariable.class), visitor);
}
@Test
public void pureClass() {
mockery.checking(new Expectations() {{
one(visitor).visitClass(with(any(Class.class)));
}});
dispatcher.dispatch(Class.class, visitor);
}
@Test
public void multipleMatches() {
class TestClass implements TypeVariable, GenericArrayType {
@Override
public Type getGenericComponentType() {
return null;
}
@Override
public Type[] getBounds() {
return new Type[0];
}
@Override
public GenericDeclaration getGenericDeclaration() {
return null;
}
@Override
public String getName() {
return null;
}
}
mockery.checking(new Expectations() {{
one(visitor).visitTypeVariable(with(any(TypeVariable.class)));
one(visitor).visitGenericArrayType(with(any(GenericArrayType.class)));
}});
dispatcher.dispatch(new TestClass(), visitor);
}
}
Finally, here is an interface that defines basic contract for type arguments resolving:
TypeArgumentResolver.java
package org.springcontrib.util.generic.resolver;
import java.lang.reflect.Type;
/**
* Defines general contract for resolving type arguments.
* <p/>
* Implementations of this interface are assumed to be thread-safe.
*
* @author Denis Zhdanov
* @since Nov 7, 2009
*/
public interface TypeArgumentResolver {
/** Stands for a marker of a raw type */
public static final Type RAW_TYPE = new Type() {
@Override
public String toString() {
return "TypeArgumentResolver.RAW_TYPE (raw type indicator)";
}
};
/**
* Assumes that given <code>'base'</code> type is a parameterized type and given
* <code>'target' IS-A 'base'</code> and tries to resolve type argument of the <code>'base'</code>
* type for the given index.
* <p/>
* E.g. consider the following example:
* <pre>
* public class Base<T> {}
*
* public class Sub1<F, S> extends Base<S> {}
*
* public class TestClass extends Sub<Integer, String> {}
* </pre>
* <ul>
* <li>resolve(Base.class, TestClass.class, 0) is String.class;</li>
* <li>resolve(Sub1.class, TestClass.class, 0) is String.class;</li>
* <li>resolve(Sub1.class, TestClass.class, 1) is Integer.class;</li>
* </ul>
* <p/>
* There is a possible case that 'raw' implementation class is used instead of generic one
* (e.g. consider the following hierarchy:
* <pre>
* class Parent<T> implements Comparable<T> { // ... implementation}
*
* class Child extends Parent {}
* </pre>
* Here <code>'Child'</code> class uses <code>'raw'</code> version of <code>'Parent'</code> class).
* {@link #RAW_TYPE} is returned then.
*
* @param base target base type that has type arguments
* @param target type that <code>IS-A 'base'</code> type
* @param index target index of the <code>'base'</code> type type parameter to resolve
* @return <code>'base'</code> type argument resolved against the given <code>'target'</code> type
* @throws IllegalArgumentException if any of the given arguments is <code>null</code> or if given
* <code>'base'</code> type doesn't have type argument for the given
* index (or doesn't have type arguments at all) or if given
* <code>'target'</code> type is not <code>IS-A 'base'</code> type
*/
Type resolve(Type base, Type target, int index) throws IllegalArgumentException;
}
DefaultTypeArgumentResolver
Type arguments resolver implementation:
package org.springcontrib.util.generic.resolver;
import org.springcontrib.util.generic.TypeDispatcher;
import org.springcontrib.util.generic.TypeVisitor;
import org.springcontrib.util.generic.TypeVisitorAdapter;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* Default {@link TypeArgumentResolver} implementation.
* <p/>
* This class is not singleton but offers single-point-of-usage field ({@link #INSTANCE}).
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since Nov 7, 2009
*/
public class DefaultTypeArgumentResolver implements TypeArgumentResolver {
/** Single-point-of-usage field. */
public static final DefaultTypeArgumentResolver INSTANCE = new DefaultTypeArgumentResolver();
/** Builds actual type arguments mappings. */
private final TypeVisitor typeArgumentsMappingBuilder = new TypeVisitorAdapter() {
@Override
public void visitParameterizedType(ParameterizedType type) {
rememberMappings(type);
if (type.getRawType() == baseClass.get()) {
matched.set(true);
return;
}
typeDispatcher.get().dispatch(type.getRawType(), this);
}
@Override
public void visitClass(Class<?> clazz) {
if (clazz == baseClass.get()) {
matched.set(true);
return;
}
interfaceFlag.set(true);
Type[] genericInterfaces = clazz.getGenericInterfaces();
Class<?>[] rawInterfaces = clazz.getInterfaces();
for (int i = 0; i < genericInterfaces.length; ++i) {
if (!clazz.isInterface()) {
Map<Type, Type> map = interfaceArgumentsMap.get();
map.clear();
map.putAll(classArgumentsMap.get());
}
if (genericInterfaces[i] == rawInterfaces[i]) {
rememberRawMappings(rawInterfaces[i]);
}
typeDispatcher.get().dispatch(genericInterfaces[i], this);
if (matched.get()) {
return;
}
}
if (clazz.isInterface()) {
// There is no point in asking interface for superclass
return;
}
interfaceFlag.set(false);
Type genericSuperclass = clazz.getGenericSuperclass();
if (genericSuperclass == Object.class) {
return;
}
Class<?> rawSuperclass = clazz.getSuperclass();
if (genericSuperclass == rawSuperclass) {
rememberRawMappings(rawSuperclass);
}
if (genericSuperclass != null) {
typeDispatcher.get().dispatch(genericSuperclass, this);
}
}
};
/** Identifies base class to check. */
private final TypeVisitor baseClassInitializer = new TypeVisitor() {
@Override
public void visitParameterizedType(ParameterizedType type) {
// We remember the mappings assuming that given type is not interface but class. They are moved to the
// corresponding collection from visitClass() otherwise.
rememberMappings(type);
typeDispatcher.get().dispatch(type.getRawType(), this);
}
@Override
public void visitWildcardType(WildcardType type) throws IllegalArgumentException {
throw new IllegalArgumentException(getErrorMessage(WildcardType.class));
}
@Override
public void visitGenericArrayType(GenericArrayType type) throws IllegalArgumentException {
throw new IllegalArgumentException(getErrorMessage(GenericArrayType.class));
}
@Override
public void visitTypeVariable(TypeVariable<? extends GenericDeclaration> type) throws IllegalArgumentException {
throw new IllegalArgumentException(getErrorMessage(TypeVariable.class));
}
@Override
public void visitClass(Class<?> clazz) {
// We know that parameterized type mappings are remembered at 'class' map by default, so,
// if target raw type is interface just move the mappings to 'interface' map.
if (clazz.isInterface()) {
interfaceArgumentsMap.get().putAll(classArgumentsMap.get());
classArgumentsMap.get().clear();
}
baseClass.set(clazz);
}
@Override
public void visitType(Type type) throws IllegalArgumentException {
throw new IllegalArgumentException(getErrorMessage(Type.class));
}
private String getErrorMessage(Class<?> targetClass) {
return String.format("Type argument resolving rule from '%s' type undefined", targetClass);
}
};
/** This visitor is served for correct {@link #interfaceFlag} initialization for the 'target' type */
private final TypeVisitor interfaceFlagInitializer = new TypeVisitorAdapter() {
@Override
public void visitParameterizedType(ParameterizedType type) {
typeDispatcher.get().dispatch(type.getRawType(), this);
}
@Override
public void visitClass(Class<?> clazz) {
interfaceFlag.set(clazz.isInterface());
}
};
private final ThreadLocal<Map<Type, Type>> classArgumentsMap = new ThreadLocal<Map<Type, Type>>() {
@Override
protected Map<Type, Type> initialValue() {
return new LinkedHashMap<Type, Type>();
}
};
private final ThreadLocal<Map<Type, Type>> interfaceArgumentsMap = new ThreadLocal<Map<Type, Type>>() {
@Override
protected Map<Type, Type> initialValue() {
return new LinkedHashMap<Type, Type>();
}
};
private final ThreadLocal<Map<Type, Type>> tempMap = new ThreadLocal<Map<Type, Type>>() {
@Override
protected Map<Type, Type> initialValue() {
return new LinkedHashMap<Type, Type>();
}
};
private final ThreadLocal<Boolean> interfaceFlag = new ThreadLocal<Boolean>();
private final ThreadLocal<Boolean> matched = new ThreadLocal<Boolean>();
private final ThreadLocal<Class<?>> baseClass = new ThreadLocal<Class<?>>();
private final AtomicReference<TypeDispatcher> typeDispatcher
= new AtomicReference<TypeDispatcher>(TypeDispatcher.INSTANCE);
/** {@inheritDoc} */
@Override
public Type resolve(Type base, Type target, int index) throws IllegalArgumentException {
// We don't explicitly check arguments for null here because TypeDispatcher claims that it already does that.
if (index < 0) {
throw new IllegalArgumentException(String.format("Can't resolve type parameter of the type '%s' "
+ "against type '%s'. Reason: given index is negative (%d)", base, target, index));
}
interfaceFlag.set(false);
typeDispatcher.get().dispatch(base, baseClassInitializer);
if (baseClass.get().getTypeParameters().length <= index) {
throw new IllegalArgumentException(String.format("Can't resolve type parameter of the type '%s' "
+ "against type '%s'. Reason: given index is too big (%d). Available type arguments number is %d",
base, target, index, baseClass.get().getTypeParameters().length));
}
typeDispatcher.get().dispatch(target, interfaceFlagInitializer);
matched.set(false);
typeDispatcher.get().dispatch(target, typeArgumentsMappingBuilder);
if (!matched.get()) {
throw new IllegalArgumentException(String.format("Can't resolve type parameter #%d of the type '%s' "
+ "against type '%s'. Reason: there is no IS-A relation between them", index, base, target));
}
Map<Type, Type> argumentTypes = baseClass.get().isInterface()
? interfaceArgumentsMap.get() : classArgumentsMap.get();
for (Map.Entry<Type, Type> entry : argumentTypes.entrySet()) {
if (--index < 0) {
return entry.getValue();
}
}
return RAW_TYPE;
}
/**
* Allows to define custom type dispatcher to use.
* <p/>
* {@link TypeDispatcher#INSTANCE} is used by default.
*
* @param typeDispatcher custom type dispatcher to use
*/
public void setTypeDispatcher(TypeDispatcher typeDispatcher) {
this.typeDispatcher.set(typeDispatcher);
}
private void rememberMappings(ParameterizedType type) {
Type[] actualArguments = type.getActualTypeArguments();
Class<?> clazz = (Class<?>) type.getRawType();
TypeVariable<? extends Class<?>>[] typeVariables = clazz.getTypeParameters();
rememberMappings(typeVariables, actualArguments);
}
private void rememberRawMappings(Class<?> rawClass) {
Type[] typeVariables = rawClass.getTypeParameters();
Type[] rawVariables = new Type[typeVariables.length];
Arrays.fill(rawVariables, RAW_TYPE);
rememberMappings(typeVariables, rawVariables);
}
private void rememberMappings(Type[] from, Type[] to) {
Map<Type, Type> previousMappings = interfaceFlag.get() ? interfaceArgumentsMap.get() : classArgumentsMap.get();
Map<Type, Type> currentMappings = tempMap.get();
for (int i = 0; i < from.length; ++i) {
Type resolved = to[i];
if (previousMappings.containsKey(resolved)) {
resolved = previousMappings.get(resolved);
}
currentMappings.put(from[i], resolved);
}
previousMappings.clear();
previousMappings.putAll(currentMappings);
currentMappings.clear();
}
}
And finally unit test for the type arguments resolver implementation:
package org.springcontrib.util.generic.resolver;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.After;
import org.junit.Test;
import org.jmock.Mockery;
import org.jmock.lib.legacy.ClassImposteriser;
import java.io.*;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.List;
/**
* @author Denis Zhdanov
* @since 11/07/2009
*/
@SuppressWarnings({"RawUseOfParameterizedType", "unchecked", "UnusedDeclaration", "serial"})
public class DefaultTypeArgumentResolverTest {
private final ParameterizedType testInterfaceType
= (ParameterizedType) TestInterfaceImpl.class.getGenericInterfaces()[0];
private final ParameterizedType testInterfaceImplType
= (ParameterizedType) ParameterizedChild.class.getGenericSuperclass();
private DefaultTypeArgumentResolver resolver;
private Mockery mockery;
@Before
public void setUp() throws Exception {
resolver = new DefaultTypeArgumentResolver();
mockery = new Mockery() {{
setImposteriser(ClassImposteriser.INSTANCE);
}};
}
@After
public void tearDown() {
mockery.assertIsSatisfied();
}
@Test(expected = IllegalArgumentException.class)
public void toWildCardType() throws NoSuchFieldException {
class Test {
public Collection<? extends Comparable<Number>> field;
}
Type type = ((ParameterizedType)Test.class.getField("field").getGenericType()).getActualTypeArguments()[0];
resolver.resolve(type, Integer.class, 0);
}
@Test(expected = IllegalArgumentException.class)
public void toGenericArrayType() throws NoSuchFieldException {
class Test<T> implements TestInterface<T[], Integer, String>{}
Type type = ((ParameterizedType)Test.class.getGenericInterfaces()[0]).getActualTypeArguments()[0];
resolver.resolve(type, Integer.class, 0);
}
@Test(expected = IllegalArgumentException.class)
public void toTypeVariable() throws NoSuchFieldException {
class Test<T>{}
Type type = Test.class.getTypeParameters()[0];
resolver.resolve(type, Integer.class, 0);
}
@Test(expected = IllegalArgumentException.class)
public void toVariable() throws NoSuchFieldException {
Type type = new Type() {};
resolver.resolve(type, Integer.class, 0);
}
@Test(expected = IllegalArgumentException.class)
public void toIncompatibleTypes() {
resolver.resolve(TestInterface.class, Integer.class, 0);
}
@Test(expected = IllegalArgumentException.class)
public void negativeIndex() {
resolver.resolve(Comparable.class, Integer.class, -1);
}
@Test(expected = IllegalArgumentException.class)
public void indexOutOfBounds() {
resolver.resolve(Comparable.class, Integer.class, 1);
}
@Test
public void toParameterizedDirectInterface() {
class Sub1<X, Y, Z> implements TestInterface<Y, Z, X> {}
class Sub2<D, E> extends Sub1<String, E, D> {}
class Sub3 extends Sub2<Integer, Long> {}
assertSame(Long.class, resolver.resolve(testInterfaceType, Sub3.class, 0));
assertSame(Long.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 0));
assertSame(Integer.class, resolver.resolve(testInterfaceType, Sub3.class, 1));
assertSame(Integer.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 1));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub3.class, 2));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 2));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub2.class, 2));
Type resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 0);
assertTrue(resolvedType instanceof TypeVariable);
TypeVariable<?> typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("E", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 1);
assertTrue(resolvedType instanceof TypeVariable);
typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("D", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
}
@Test
public void toParameterizedIndirectInterface() {
class Sub1<A, B, C> implements SubInterface2<A, C, B> {}
class Sub2<A, C> extends Sub1<String, C, A> implements Comparable<A>{
@Override
public int compareTo(A o) {
return 0;
}
}
class Sub3 extends Sub2<Integer, Long> implements Serializable {}
assertSame(Long.class, resolver.resolve(testInterfaceType, Sub3.class, 0));
assertSame(Long.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 0));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub3.class, 1));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 1));
assertSame(Integer.class, resolver.resolve(testInterfaceType, Sub3.class, 2));
assertSame(Integer.class, resolver.resolve(testInterfaceType, Sub3.class.getGenericSuperclass(), 2));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub2.class, 1));
Type resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 0);
assertTrue(resolvedType instanceof TypeVariable);
TypeVariable<?> typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("C", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 2);
assertTrue(resolvedType instanceof TypeVariable);
typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("A", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
}
@Test
public void toValidParameterizedClass() {
class Sub1<X, Y, Z> extends TestInterfaceImpl<Y, Z, X> {}
class Sub2<D, E> extends Sub1<String, E, D> {}
class Sub3 extends Sub2<Integer, Long> {}
assertSame(Long.class, resolver.resolve(testInterfaceImplType, Sub3.class, 0));
assertSame(Long.class, resolver.resolve(testInterfaceImplType, Sub3.class.getGenericSuperclass(), 0));
assertSame(Integer.class, resolver.resolve(testInterfaceImplType, Sub3.class, 1));
assertSame(Integer.class, resolver.resolve(testInterfaceImplType, Sub3.class.getGenericSuperclass(), 1));
assertSame(String.class, resolver.resolve(testInterfaceImplType, Sub3.class, 2));
assertSame(String.class, resolver.resolve(testInterfaceImplType, Sub3.class.getGenericSuperclass(), 2));
assertSame(String.class, resolver.resolve(testInterfaceType, Sub2.class, 2));
Type resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 0);
assertTrue(resolvedType instanceof TypeVariable);
TypeVariable<?> typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("E", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
resolvedType = resolver.resolve(testInterfaceType, Sub2.class, 1);
assertTrue(resolvedType instanceof TypeVariable);
typeVariable = (TypeVariable<?>) resolvedType;
assertEquals("D", typeVariable.getName());
assertSame(Sub2.class, typeVariable.getGenericDeclaration());
}
@Test
public void toRawInterface() {
class TestClass implements SubInterface2 {}
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceType, TestClass.class, 0));
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceType, TestClass.class, 1));
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceType, TestClass.class, 2));
}
@Test
public void toRawClass() {
class TestClass extends ParameterizedChild {}
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceImplType, TestClass.class, 0));
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceImplType, TestClass.class, 1));
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(testInterfaceImplType, TestClass.class, 2));
assertSame(TypeArgumentResolver.RAW_TYPE, resolver.resolve(Comparable.class, Comparable.class, 0));
}
@Test
public void classToInterface() throws NoSuchFieldException {
class TestClass {
public List<Integer> field;
}
assertSame(Integer.class, resolver.resolve(Collection.class, TestClass.class.getField("field").getGenericType(), 0));
}
@Test
public void parameterizedClassToParameterizedInterface() throws NoSuchFieldException {
class TestClass {
public Collection<Long> field1;
public List<Integer> field2;
}
assertSame(
Integer.class,
resolver.resolve(
TestClass.class.getField("field1").getGenericType(),
TestClass.class.getField("field2").getGenericType(),
0
)
);
}
private interface TestInterface<A, B, C> {}
private interface SubInterface1<A, B, C> extends TestInterface<B, C, A> {}
private interface SubInterface2<A, B, C> extends SubInterface1<B, C, A> {}
private class TestInterfaceImpl<A, B, C> implements TestInterface<A, B, C> {}
private class ParameterizedChild<A, B, C> extends TestInterfaceImpl<A, B, C> {}
}
You can take a brief look at the unit-test in order to check that even complex situations with big class hierarchies and replaced type parameters are correctly resolved.

Hi Denis,
ReplyDeleteI've found your mini-framework from this post:
http://forum.springsource.org/showpost.php?p=274096&postcount=9
that you made about a problem with CGLIB and the usage of ParameterizedType. Thank you for this, many people ask the same question.
I think that I start understanding the problem. Here is my hand written code ugly patching the problem.
My question is: how to use your mini-framework? Could you change the code below to use your framework?
Many thanks.
John - JavaBlackBelt.com
public abstract class GenericDao, K extends Serializable> {
public Class entityClass;
@PersistenceContext protected EntityManager em;
public GenericDao() {
Class bankDaoClass; // Your dao class, i.e. BankDao (extending GenericDao)
if (this.getClass().getSuperclass() == GenericDao.class) { // We are instantiated without CGLIB: new BankDao()
bankDaoClass = this.getClass();
} else { // Spring instantiates as CGLIB class extending our concrete DAO class (as BankDao): new BankDaoCGLIB()
Class cglibConcreteDaoClass = this.getClass();
bankDaoClass = cglibConcreteDaoClass.getSuperclass();
}
ParameterizedType genericDaoType = (ParameterizedType) bankDaoClass.getGenericSuperclass();
Type entityType = (genericDaoType).getActualTypeArguments()[0];
entityClass = (Class) entityType;
}
...
}
Hello John,
ReplyDeleteThanks for the interest to the framework. Your solution may be rewritten as following using it:
public abstract class GenericDao, K extends Serializable> {
public Class entityClass;
@PersistenceContext
protected EntityManager em;
public GenericDao() {
Type resolvedType = DefaultTypeArgumentResolver.INSTANCE.resolve(GenericDao.class, getClass(), 0);
TypeDispatcher.INSTANCE.dispatch(resolvedType, new TypeVisitorAdapter() {
@SuppressWarnings({"unchecked"})
@Override
public void visitClass(Class clazz) {
entityClass = (Class)clazz;
}
});
}
}
The most important thing here is that we don't obey classes hierarchy to have one of two forms:
*) BankDao extends GenericDao
*) BankDao extends AnotherClass extends GenericDao
We just assume that current class has IS-A relation to GenericDao class and the framework resolve the actual hierarchy. It's benefit is that it works with interfaces and provides convenient api to work with all java type representations (dispatch target Type object and provide necessary processing for Class, ParameterizedType, TypeVariable etc via TypeVisitor).
Regards, Denis