reflect - obtener atributos de una clase java
Inferencia de tipo de reflexión en Java 8 Lambdas (5)
Estaba experimentando con los nuevos Lambdas en Java 8, y estoy buscando una forma de utilizar la reflexión en las clases lambda para obtener el tipo de retorno de una función lambda. Estoy especialmente interesado en casos donde la lambda implementa una superinterfaz genérica. En el siguiente ejemplo de código, MapFunction<F, T>
es la superinterfaz genérica, y estoy buscando una forma de averiguar qué tipo se une al parámetro genérico T
Si bien Java descarta una gran cantidad de información de tipo genérico después del compilador, las subclases (y las subclases anónimas) de superclases genéricas y superinterfaces genéricas conservaron esa información de tipo. A través de la reflexión, estos tipos fueron accesibles. En el ejemplo siguiente (caso 1) , la reflexión me dice que la implementación de MapFunction
de MapFunction
vincula java.lang.Integer
al parámetro de tipo genérico T
Incluso para las subclases que son genéricas, existen ciertos medios para descubrir qué se une a un parámetro genérico, si se conocen otros. Considere el caso 2 en el ejemplo a continuación, IdentityMapper
donde tanto F
como T
unen al mismo tipo. Cuando lo sabemos, conocemos el tipo F
si conocemos el tipo de parámetro T
(que en mi caso lo hacemos).
La pregunta es ahora, ¿cómo puedo darme cuenta de algo similar para Java 8 lambdas? Como en realidad no son subclases regulares de la superinterfaz genérica, el método descrito anteriormente no funciona. Específicamente, ¿puedo deducir que parseLambda
vincula java.lang.Integer
a T
, y la identityLambda
vincula lo mismo a F
y T
?
PD: En teoría, debería ser posible descompilar el código lambda y luego usar un compilador integrado (como el JDT) y tocar su tipo de inferencia. Espero que haya una manera más simple de hacer esto ;-)
/**
* The superinterface.
*/
public interface MapFunction<F, T> {
T map(F value);
}
/**
* Case 1: A non-generic subclass.
*/
public class MyMapper implements MapFunction<String, Integer> {
public Integer map(String value) {
return Integer.valueOf(value);
}
}
/**
* A generic subclass
*/
public class IdentityMapper<E> implements MapFunction<E, E> {
public E map(E value) {
return value;
}
}
/**
* Instantiation through lambda
*/
public MapFunction<String, Integer> parseLambda = (String str) -> { return Integer.valueOf(str); }
public MapFunction<E, E> identityLambda = (value) -> { return value; }
public static void main(String[] args)
{
// case 1
getReturnType(MyMapper.class); // -> returns java.lang.Integer
// case 2
getReturnTypeRelativeToParameter(IdentityMapper.class, String.class); // -> returns java.lang.String
}
private static Class<?> getReturnType(Class<?> implementingClass)
{
Type superType = implementingClass.getGenericInterfaces()[0];
if (superType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) superType;
return (Class<?>) parameterizedType.getActualTypeArguments()[1];
}
else return null;
}
private static Class<?> getReturnTypeRelativeToParameter(Class<?> implementingClass, Class<?> parameterType)
{
Type superType = implementingClass.getGenericInterfaces()[0];
if (superType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) superType;
TypeVariable<?> inputType = (TypeVariable<?>) parameterizedType.getActualTypeArguments()[0];
TypeVariable<?> returnType = (TypeVariable<?>) parameterizedType.getActualTypeArguments()[1];
if (inputType.getName().equals(returnType.getName())) {
return parameterType;
}
else {
// some logic that figures out composed return types
}
}
return null;
}
Esto es posible de resolver actualmente, pero solo de una manera bastante hackie, pero permítanme primero explicar algunas cosas:
Cuando escribe un lambda, el compilador inserta una instrucción de invocación dinámica que apunta a la LambdaMetafactory y un método privado de síntesis estática con el cuerpo de la lambda. El método sintético y el método manejado en el conjunto constante contienen el tipo genérico (si el lambda usa el tipo o es explícito como en los ejemplos).
Ahora en el tiempo de ejecución se llama al LambdaMetaFactory
y se genera una clase utilizando ASM que implementa la interfaz funcional y el cuerpo del método llama al método privado estático con cualquier argumento pasado. Luego se inyecta en la clase original utilizando Unsafe.defineAnonymousClass
(consulte la publicación de John Rose ) para que pueda acceder a los miembros privados, etc.
Lamentablemente, la clase generada no almacena las firmas genéricas (podría) por lo que no puede usar los métodos de reflexión habituales que le permiten borrar el borrado
Para una clase normal puede inspeccionar el bytecode usando Class.getResource(ClassName + ".class")
pero para clases anónimas definidas usando Unsafe
no tiene suerte. Sin embargo, puede hacer que LambdaMetaFactory
los LambdaMetaFactory
con el argumento de JVM:
java -Djdk.internal.lambda.dumpProxyClasses=/some/folder
Al mirar el archivo de clase volcado (usando javap -p -s -v
), uno puede ver que efectivamente llama al método estático. Pero el problema sigue siendo cómo obtener el bytecode desde el propio Java.
Desafortunadamente, este es el lugar donde se piratea:
Usando la reflexión podemos llamar a Class.getConstantPool
y luego acceder a MethodRefInfo para obtener los descriptores de tipo. Entonces podemos usar ASM para analizar esto y devolver los tipos de argumentos. Poniendolo todo junto:
Method getConstantPool = Class.class.getDeclaredMethod("getConstantPool");
getConstantPool.setAccessible(true);
ConstantPool constantPool = (ConstantPool) getConstantPool.invoke(lambda.getClass());
String[] methodRefInfo = constantPool.getMemberRefInfoAt(constantPool.size() - 2);
int argumentIndex = 0;
String argumentType = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(methodRef[2])[argumentIndex].getClassName();
Class<?> type = (Class<?>) Class.forName(argumentType);
Actualizado con la sugerencia de jonathan
Ahora, idealmente, las clases generadas por LambdaMetaFactory
deberían almacenar las firmas genéricas (podría ver si puedo enviar un parche al OpenJDK), pero actualmente esto es lo mejor que podemos hacer. El código anterior tiene los siguientes problemas:
- Utiliza métodos y clases no documentados
- Es extremadamente vulnerable a los cambios de código en el JDK
- No conserva los tipos genéricos, por lo que si pasa List <String> a una lambda aparecerá como List
He encontrado una forma de hacerlo para lambdas serializables. Todas mis lambdas son serializables, para eso funciona.
Gracias, Holger, por indicarme la SerializedLambda
.
Los parámetros genéricos se capturan en el método estático sintético de lambda y se pueden recuperar a partir de allí. Encontrar el método estático que implementa el lambda es posible con la información de SerializedLambda
Los pasos son los siguientes:
- Obtenga SerializedLambda a través del método de reemplazo de escritura que se genera automáticamente para todas las lambdas serializables
- Encuentre la clase que contiene la implementación lambda (como método estático sintético)
- Obtenga el método
java.lang.reflect.Method
para el método sintético estático - Obtener tipos genéricos de ese
Method
ACTUALIZACIÓN: Aparentemente, esto no funciona con todos los compiladores. Lo he intentado con el compilador de Eclipse Luna (funciona) y el Oracle javac (no funciona).
// sample how to use
public static interface SomeFunction<I, O> extends java.io.Serializable {
List<O> applyTheFunction(Set<I> value);
}
public static void main(String[] args) throws Exception {
SomeFunction<Double, Long> lambda = (set) -> Collections.singletonList(set.iterator().next().longValue());
SerializedLambda sl = getSerializedLambda(lambda);
Method m = getLambdaMethod(sl);
System.out.println(m);
System.out.println(m.getGenericReturnType());
for (Type t : m.getGenericParameterTypes()) {
System.out.println(t);
}
// prints the following
// (the method) private static java.util.List test.ClassWithLambdas.lambda$0(java.util.Set)
// (the return type, including *Long* as the generic list type) java.util.List<java.lang.Long>
// (the parameter, including *Double* as the generic set type) java.util.Set<java.lang.Double>
// getting the SerializedLambda
public static SerializedLambda getSerializedLambda(Object function) {
if (function == null || !(function instanceof java.io.Serializable)) {
throw new IllegalArgumentException();
}
for (Class<?> clazz = function.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Method replaceMethod = clazz.getDeclaredMethod("writeReplace");
replaceMethod.setAccessible(true);
Object serializedForm = replaceMethod.invoke(function);
if (serializedForm instanceof SerializedLambda) {
return (SerializedLambda) serializedForm;
}
}
catch (NoSuchMethodError e) {
// fall through the loop and try the next class
}
catch (Throwable t) {
throw new RuntimeException("Error while extracting serialized lambda", t);
}
}
throw new Exception("writeReplace method not found");
}
// getting the synthetic static lambda method
public static Method getLambdaMethod(SerializedLambda lambda) throws Exception {
String implClassName = lambda.getImplClass().replace(''/'', ''.'');
Class<?> implClass = Class.forName(implClassName);
String lambdaName = lambda.getImplMethodName();
for (Method m : implClass.getDeclaredMethods()) {
if (m.getName().equals(lambdaName)) {
return m;
}
}
throw new Exception("Lambda Method not found");
}
La decisión exacta de cómo asignar el código lambda a las implementaciones de interfaz se deja en el entorno de tiempo de ejecución real. En principio, todas las lambdas que implementan la misma interfaz sin MethodHandleProxies
podrían compartir una única clase de tiempo de ejecución como MethodHandleProxies
hace MethodHandleProxies
. El uso de diferentes clases para lambdas específicos es una optimización realizada por la implementación real de LambdaMetafactory
, pero no como una función destinada a ayudar a la depuración o Reflection.
Entonces, incluso si encuentra información más detallada en la clase de tiempo de ejecución real de una implementación de interfaz lambda, será un artefacto del entorno de tiempo de ejecución actualmente utilizado que podría no estar disponible en una implementación diferente o incluso en otras versiones de su entorno actual.
Si el lambda es Serializable
, puede usar el hecho de que el formulario serializado contiene la firma del método del tipo de interfaz instanciada para confundir los valores reales de las variables de tipo.
La información de tipo parametrizada solo está disponible en tiempo de ejecución para los elementos de código que están vinculados, es decir, compilados específicamente en un tipo. Los lambdas hacen lo mismo, pero como su Lambda se elimina con azucar a un método en lugar de a un tipo, no hay ningún tipo para capturar esa información.
Considera lo siguiente:
import java.util.Arrays;
import java.util.function.Function;
public class Erasure {
static class RetainedFunction implements Function<Integer,String> {
public String apply(Integer t) {
return String.valueOf(t);
}
}
public static void main(String[] args) throws Exception {
Function<Integer,String> f0 = new RetainedFunction();
Function<Integer,String> f1 = new Function<Integer,String>() {
public String apply(Integer t) {
return String.valueOf(t);
}
};
Function<Integer,String> f2 = String::valueOf;
Function<Integer,String> f3 = i -> String.valueOf(i);
for (Function<Integer,String> f : Arrays.asList(f0, f1, f2, f3)) {
try {
System.out.println(f.getClass().getMethod("apply", Integer.class).toString());
} catch (NoSuchMethodException e) {
System.out.println(f.getClass().getMethod("apply", Object.class).toString());
}
System.out.println(Arrays.toString(f.getClass().getGenericInterfaces()));
}
}
}
f0
y f1
conservan su información de tipo genérico, como era de esperar. Pero como son métodos independientes que se han borrado a la Function<Object,Object>
, f2
y f3
no.
Recientemente agregué soporte para resolver argumentos tipo lambda para TypeTools . Ex:
MapFunction<String, Integer> fn = str -> Integer.valueOf(str);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass());
Los argumentos resueltos del tipo son los esperados:
assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;
Para manejar una lambda pasada:
public void call(Callable<?> c) {
// Assumes c is a lambda
Class<?> callableType = TypeResolver.resolveRawArguments(Callable.class, c.getClass());
}
Nota: La implementación subyacente utiliza el enfoque ConstantPool delineado por @danielbodart que se sabe que funciona en Oracle JDK y OpenJDK (y posiblemente otros).