How Java Retrieves Actual Generic Types: Insights from SimpleChannelInboundHandler
Jan 28, 2024 · 750 words
Counter-intuitive Generic Classes
Anyone who has used Netty has likely encountered SimpleChannelInboundHandler, an out-of-the-box class for processing request data. In SimpleChannelInboundHandler, we only need to override the channelRead0(ChannelHandlerContext, T) method to directly handle data objects of type T without manual casting:
public class CreateUserHandler extends SimpleChannelInboundHandler<CreateUserRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, CreateUserRequest request) throws Exception {
// handles request
}
}
It's incredibly convenient... wait a minute! Isn't Java generics subject to type erasure at compile time? How does SimpleChannelInboundHandler know the actual type of our data object?
We know that Java implements generics through type erasure. Therefore, an object of SimpleChannelInboundHandler<CreateUserRequest> will have CreateUserRequest erased to Object after compilation. Consequently, to determine the specific generic type, there are generally two ways:
// 1. The generic object is a method parameter; get the actual type via getClass()
void handle(T data) {
Class<T> clazz = data.getClass();
// ...
}
// 2. No generic object in parameters; explicitly pass the generic type as an argument
void handle(String message, Class<T> clazz) {
// ...
}
However, SimpleChannelInboundHandler clearly doesn't follow either of these patterns. What's going on?
The Magical Code
To investigate, I opened the Netty source code. The key to obtaining the generic type lies in this line of code:
matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I");
Here, I is the generic parameter declaration for SimpleChannelInboundHandler:
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
}
This single line of code achieves a functionality I thought was impossible. To find out why, let's look deeper:
public static TypeParameterMatcher find(
final Object object, final Class<?> parameterizedSuperclass, final String typeParamName) {
final Map<Class<?>, Map<String, TypeParameterMatcher>> findCache =
InternalThreadLocalMap.get().typeParameterMatcherFindCache();
final Class<?> thisClass = object.getClass();
Map<String, TypeParameterMatcher> map = findCache.get(thisClass);
if (map == null) {
map = new HashMap<String, TypeParameterMatcher>();
findCache.put(thisClass, map);
}
TypeParameterMatcher matcher = map.get(typeParamName);
if (matcher == null) {
matcher = get(find0(object, parameterizedSuperclass, typeParamName));
map.put(typeParamName, matcher);
}
return matcher;
}
private static Class<?> find0(
final Object object, Class<?> parameterizedSuperclass, String typeParamName) {
final Class<?> thisClass = object.getClass();
Class<?> currentClass = thisClass;
for (;;) {
if (currentClass.getSuperclass() == parameterizedSuperclass) {
int typeParamIndex = -1;
TypeVariable<?>[] typeParams = currentClass.getSuperclass().getTypeParameters();
for (int i = 0; i < typeParams.length; i ++) {
if (typeParamName.equals(typeParams[i].getName())) {
typeParamIndex = i;
break;
}
}
if (typeParamIndex < 0) {
throw new IllegalStateException(
"unknown type parameter '" + typeParamName + "': " + parameterizedSuperclass);
}
Type genericSuperType = currentClass.getGenericSuperclass();
if (!(genericSuperType instanceof ParameterizedType)) {
return Object.class;
}
Type[] actualTypeParams = ((ParameterizedType) genericSuperType).getActualTypeArguments();
Type actualTypeParam = actualTypeParams[typeParamIndex];
if (actualTypeParam instanceof ParameterizedType) {
actualTypeParam = ((ParameterizedType) actualTypeParam).getRawType();
}
if (actualTypeParam instanceof Class) {
return (Class<?>) actualTypeParam;
}
if (actualTypeParam instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) actualTypeParam).getGenericComponentType();
if (componentType instanceof ParameterizedType) {
componentType = ((ParameterizedType) componentType).getRawType();
}
if (componentType instanceof Class) {
return Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
if (actualTypeParam instanceof TypeVariable) {
// Resolved type parameter points to another type parameter.
TypeVariable<?> v = (TypeVariable<?>) actualTypeParam;
currentClass = thisClass;
if (!(v.getGenericDeclaration() instanceof Class)) {
return Object.class;
}
parameterizedSuperclass = (Class<?>) v.getGenericDeclaration();
typeParamName = v.getName();
if (parameterizedSuperclass.isAssignableFrom(thisClass)) {
continue;
} else {
return Object.class;
}
}
return fail(thisClass, typeParamName);
}
currentClass = currentClass.getSuperclass();
if (currentClass == null) {
return fail(thisClass, typeParamName);
}
}
}
private static Class<?> fail(Class<?> type, String typeParamName) {
throw new IllegalStateException(
"cannot determine the type of the type parameter '" + typeParamName + "': " + type);
}
I copied this magical code to my local environment and wrote a test case:
public class ExceptionHandler<E extends RuntimeException> {
public ExceptionHandler() {
Class<?> c = find(this, ExceptionHandler.class, "E");
System.out.println(c);
}
}
public class IllegalArgumentExceptionHandler extends ExceptionHandler<IllegalArgumentException> {
}
public class ExceptionHandlerTest {
public static void main(String[] args) {
ExceptionHandler<IllegalArgumentException> handler = new IllegalArgumentExceptionHandler();
}
}
By stepping through with a debugger, I discovered that the most critical part of this code is the statement currentClass.getGenericSuperclass(). It turns out that a subclass can retrieve the generic type of its parent class. This means that in inheritance scenarios, it is possible to obtain the actual generic type. The method-level generics I mentioned earlier, however, cannot be retrieved this way.
The Source of Generic Information
So, why is it that the actual generic type can only be retrieved in inheritance scenarios?
After researching, it turns out that the generic signature of the parent class is preserved in the bytecode.
Decoding the IllegalArgumentExceptionHandler.class file using javap, we can see a specific line:
Signature: #12 // Lcom/oceanbase/ocp/obsdk/exception/ExceptionHandler<Ljava/lang/IllegalArgumentException;>;
This record contains the generic type signature of the class, including the actual generic type java.lang.IllegalArgumentException. getGenericSuperclass() retrieves information from exactly this location.
It seems that when inheritance is involved, this method can be used to simplify code.