Your problem is not about abstract
vs concrete methods. You have annotated methods whose erasure differs from the erasure of the overriding methods. If it was just for invoking the methods, you could simply invoke the Method
describing the abstract method and be sure that a concrete class will have a concrete overriding implementation for it, though it might be a bridge method delegating to an actual implementation method with a more specific erasure.
In other words, your actual question is about getting actual types for methods in the context of generic declarations.
This is not an easy task. Basically, if a method has references to type parameters, you have to substitute them with the actual types of the actual concrete class. There is no guaranty that this will work, i.e. the actual class doesn’t have to be a reifiable type, just like ArrayList
is a concrete class implementing List
, but not a reifiable class, so it’s impossible to get the actual element type of an ArrayList
via Reflection at runtime. But you can still get the most specific implementation method of such a class.
public static Set<Method> getAnnotatedMethods(
Class<?> actualClass, Class<? extends Annotation> a) {
Set<Method> raw = getRawAnnotatedMethods(actualClass, a);
if(raw.isEmpty()) return raw;
Set<Method> resolved = new HashSet<>();
for(Method m: raw) {
if(m.getDeclaringClass()==actualClass) resolved.add(m);
else {
Method x = getMoreSpecific(actualClass, m);
resolved.add(!x.isBridge()? x: resolveGeneric(x, m));
}
}
return resolved;
}
private static Method resolveGeneric(Method x, Method m) {
final Class<?> decl = m.getDeclaringClass();
Map<Type,Type> translate = new HashMap<>();
up: for(Class<?> c=x.getDeclaringClass(); c!=decl; ) {
if(decl.isInterface()) {
for(Type t: c.getGenericInterfaces()) {
Class<?> e = erased(t);
if(updateMap(decl, e, t, translate)) continue;
c = e;
continue up;
}
}
Type t = c.getGenericSuperclass();
c = erased(t);
updateMap(decl, c, t, translate);
}
Class<?>[] raw = m.getParameterTypes();
Type[] generic = m.getGenericParameterTypes();
for(int ix = 0; ix<raw.length; ix++)
raw[ix] = erased(translate.getOrDefault(generic[ix], raw[ix]));
return getMoreSpecific(x.getDeclaringClass(), x, raw);
}
private static Method getMoreSpecific(Class<?> actual, Method inherited) {
return getMoreSpecific(actual, inherited, inherited.getParameterTypes());
}
private static Method getMoreSpecific(
Class<?> actual, Method inherited, Class<?>[] pTypes) {
try {
final String name = inherited.getName();
if(inherited.getDeclaringClass().isInterface()
|| Modifier.isPublic(inherited.getModifiers())) {
return actual.getMethod(name, pTypes);
}
for(;;) try {
return actual.getDeclaredMethod(name, pTypes);
}
catch(NoSuchMethodException ex) {
actual = actual.getSuperclass();
if(actual == null) throw ex;
}
}
catch(NoSuchMethodException ex) {
throw new IllegalStateException(ex);
}
}
private static boolean updateMap(Class<?> decl, Class<?> e, Type t, Map<Type, Type> m) {
if (!decl.isAssignableFrom(e)) {
return true;
}
if(t!=e) {
TypeVariable<?>[] tp = e.getTypeParameters();
if(tp.length>0) {
Type[] arg = ((ParameterizedType)t).getActualTypeArguments();
for(int ix=0; ix<arg.length; ix++)
m.put(tp[ix], erased(m.getOrDefault(arg[ix], arg[ix])));
}
}
return false;
}
private static Class<?> erased(Type t) {
if(t instanceof Class<?>) return (Class<?>)t;
if(t instanceof ParameterizedType)
return (Class<?>)((ParameterizedType)t).getRawType();
if(t instanceof TypeVariable<?>) return erased(((TypeVariable<?>)t).getBounds()[0]);
if(t instanceof GenericArrayType) return Array.newInstance(
erased(((GenericArrayType)t).getGenericComponentType()), 0).getClass();
return erased(((WildcardType)t).getUpperBounds()[0]);
}
/**
* You may replace this with the MethodUtils.getMethodsListWithAnnotation()
* you were already using.
*/
private static Set<Method> getRawAnnotatedMethods(
Class<?> c, Class<? extends Annotation> a) {
Set<Method> s = new HashSet<>();
for(; c!=null; c=c.getSuperclass()) {
for(Method m: c.getDeclaredMethods()) {
if(m.isAnnotationPresent(a)) s.add(m);
}
for(Class<?> ifC: c.getInterfaces()) {
for(Method m: ifC.getMethods()) {
if(m.isAnnotationPresent(a)) s.add(m);
}
}
}
return s;
}
The first thing, this method does, is to look up the method with identical raw parameter types with the most specific declaring class. If the method is declared by an interface or has been declared public
, the implementing/overriding method(s) must be public
as well, so a simple getMethod
is sufficient. Otherwise, traversing the hierarchy using getDeclaredMethod
is unavoidable.
Then, a simple check tells us whether we have to do more. If a method with more specific parameter and/or return types exists, the method we’ve just found must be a bridge method. If so, we have to follow the chain of possibly generic extends
or implements
relationships to get the actual types for the type parameters. Then, we have to substitute the method’s uses of those type parameters with the actual types. If not possible, e.g. if the method itself is generic or the actual type is not reifiable, all remaining type parameters are replaced with the erasure of their bounds.
Finally, the method with that erasure is looked up. As you can see, it’s quiet a complex process, so I can’t guaranty that this code handles all corner cases. But it’s capable of handling your use case.