2011-09-15 17 views
7

Digamos que tengo una clase abstracta:¿Implementando métodos abstractos en tiempo de ejecución?

abstract class Foo extends Bar { 

    public abstract int foo(); 

} 

que quiero extender en tiempo de ejecución para crear un objeto de clase. La esperanza sería que pudiera tener una clase generada dinámicamente:

class FooImpl extends Foo { 

    @Override 
    public int foo() { 
     return 5; 
    } 

} 

que sería representado por un objeto de clase y que luego podría utilizar la reflexión para crear nuevas instancias de. La clave es que me gustaría decidir el valor de retorno del método foo() en tiempo de ejecución. Mi idea es usar ASM para crear el bytecode para la clase y luego usar la reflexión en un objeto ClassLoader para definir la clase.

Está utilizando ASM y luego refleja el método ClassLoader # defineClass en los bytes generados la mejor manera de implementar métodos abstractos en tiempo de ejecución con valores no codificados?

En caso afirmativo, ¿cómo podría hacerlo? Mi instinto es utilizar ASMifierClassVisitor, pero no estoy seguro del método exacto para hacerlo. Sé que si todo lo demás falla, puedo consultar manualmente las instrucciones de JVM necesarias para definir una clase específica, pero creo que debe haber una manera más fácil.

Si no, ¿cuál es la mejor manera y cómo haré para usar la mejor manera?

EDIT: Revisé todas las respuestas y decidí que ninguna de ellas era exactamente lo que estaba buscando. Terminé creando una pequeña implementación de lo que estaba hablando con ASM. Pensé que debería publicarlo aquí:

import org.objectweb.asm.*; 

import java.io.File; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.PrintWriter; 
import java.lang.reflect.Field; 
import java.lang.reflect.Method; 
import java.util.HashMap; 

/** 
* Created by IntelliJ IDEA. 
* User: Matt 
* Date: 9/17/11 
* Time: 12:42 PM 
*/ 
public class OverrideClassAdapter extends ClassAdapter { 

    private final HashMap<String, Object> code; 
    private final String className; 

    private final ClassWriter writer; 

    private String superName; 

    public OverrideClassAdapter(ClassWriter writer, String className, Queue<int[]> constructorCode, HashMap<String, Object> code) { 
     super(writer); 
     this.writer = writer; 
     this.className = className; 
     this.code = code; 
    } 

    @Override 
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 
     this.superName = name; 
     if((access & Opcodes.ACC_ABSTRACT) != 0) 
      access &= ~Opcodes.ACC_ABSTRACT; 
     if((access & Opcodes.ACC_INTERFACE) != 0) 
      access &= ~Opcodes.ACC_INTERFACE; 
     cv.visit(version, access, className, signature, name, null); 
    } 

    @Override 
    public void visitSource(String source, String debug) { 
    } 

    @Override 
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 
     boolean isAbstract = (access & Opcodes.ACC_ABSTRACT) != 0; 
     if(isAbstract) 
      access &= ~Opcodes.ACC_ABSTRACT; 
     MethodWriter mw = (MethodWriter) cv.visitMethod(access, name, desc, signature, exceptions); 
     Object value = code.get(name); 
     if(isAbstract || value != null) { 
      if(value instanceof BytecodeValue) { 
       BytecodeValue returnableValue = (BytecodeValue) value; 
       int[] byteCode = new int[returnableValue.getValueCode().length + 1]; 
       System.arraycopy(returnableValue.getValueCode(), 0, byteCode, 0, returnableValue.getValueCode().length); 
       if(returnableValue.getValueCode().length > 1 && returnableValue.getValueCode()[1] == 0) { 
        byteCode[1] = writer.newConst(returnableValue.getValue()); 
       } 
       byteCode[byteCode.length - 1] = returnableValue.getReturnCode(); 
       value = byteCode; 
      } 
      return new OverrideMethodAdapter(mw, (int[]) value); 
     } 
     return mw; 
    } 

    private class OverrideMethodAdapter extends MethodAdapter { 

     private final int[] code; 

     private final MethodWriter writer; 

     public OverrideMethodAdapter(MethodWriter writer, int[] code) { 
      super(writer); 
      this.writer = writer; 
      this.code = code; 
     } 

     @Override 
     public void visitEnd() { 
      try { 
       Field code = MethodWriter.class.getDeclaredField("code"); 
       code.setAccessible(true); 
       ByteVector bytes = new ByteVector(); 
       for(int b : this.code) 
        bytes.putByte(b); 
       code.set(writer, bytes); 
      } catch (Exception e) { 
       e.printStackTrace(); 
      } 
     } 
    } 

    public static byte[] extendClassBytes(Class clazz, String className, HashMap<String, Object> methodImpls) throws IOException { 
     ClassReader cr = new ClassReader(clazz.getName()); 
     ClassWriter cw = new ClassWriter(0); 
     cr.accept(new OverrideClassAdapter(cw, className, methodImpls), ClassReader.SKIP_DEBUG); 
     cr = new ClassReader(cw.toByteArray()); 
     cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); 
     cr.accept(cw, ClassReader.SKIP_DEBUG); 
     //CheckClassAdapter.verify(new org.objectweb.asm.ClassReader(cw.toByteArray()), true, new PrintWriter(System.out)); 
     /*File file = new File(className + ".class"); 
     new FileOutputStream(file).write(cw.toByteArray());*/ 
     return cw.toByteArray(); 
    } 


    public static Class extendClass(Class clazz, String className, HashMap<String, Object> methodImpls) throws IOException { 
     return defineClass(extendClassBytes(clazz, className, methodImpls), className); 
    } 

    public static Class defineClass(byte[] code, String name) { 
     try { 
      Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); 
      method.setAccessible(true); 
      return (Class) method.invoke(Thread.currentThread().getContextClassLoader(), name, code, 0, code.length); 
     } catch (Exception e) { 
      e.printStackTrace(); 
      return null; 
     } 
    } 
} 
+0

Creo que es posible que tenga que mirar los métodos sintéticos. – Shahzeb

+0

¿Decida el valor de retorno de 'foo' en el tiempo de ejecución? ¿Cómo llamaría una clase a eso si devuelve un tipo arbitrario? –

+0

En tiempo de ejecución en el sentido de que el valor de foo se determina cuando se genera la clase, y no necesita cambiar después. – mburke13

Respuesta

5

Es posible que desee consultar el uso de CGLib. Puede hacer lo que pueden hacer los proxies dinámicos de Java, pero para las clases abstractas, así como las interfaces, y tiene una API similar a java.lang.reflect.Proxy para hacer esto también. CGLib usa ASM detrás de escena de todos modos, pero al usar CGLib no tendrá que crear bytecode directamente.

Aquí hay un ejemplo de cómo utilizar CGLIB para hacer esto:

package cglibtest; 

import java.lang.reflect.Method; 

import net.sf.cglib.proxy.Enhancer; 
import net.sf.cglib.proxy.MethodInterceptor; 
import net.sf.cglib.proxy.MethodProxy; 

public class CGLibTest 
{ 
    public static void main(String... args) 
    { 
     MyAbstract instance = (MyAbstract)Enhancer.create(MyAbstract.class, new MyInterceptor(42)); 
     System.out.println("Value from instance: " + instance.valueMethod()); 
    } 

    public static class MyInterceptor implements MethodInterceptor 
    { 
     private final Object constantValue; 

     public MyInterceptor(Object constantValue) 
     { 
      this.constantValue = constantValue; 
     } 

     @Override 
     public Object intercept(Object obj, Method method, Object[] args, 
       MethodProxy proxy) throws Throwable 
     { 
      if ("valueMethod".equals(method.getName())) 
       return(constantValue); 
      else 
       return(null); 
     } 
    } 

    public static abstract class MyAbstract 
    { 
     public abstract int valueMethod(); 
    } 
} 
+0

Comprobé cglib y estoy un poco perdido sobre cómo implementar métodos abstractos en una clase generada. ¿Tiene alguna documentación o lectura que pueda ver para ver qué se supone que debe hacerse? – mburke13

+0

@Matt agregó un ejemplo, para obtener más información, consulte el sitio web (como [examples] (http://cglib.sourceforge.net/howto.html) o [javadocs] (http://cglib.sourceforge.net /apidocs/index.html). – prunge

+0

Gracias por el ejemplo y los enlaces. He elegido su respuesta como la mejor para mi pregunta. Por curiosidad, ¿está utilizando el método Interceptor como la mejor/única forma de implementar métodos abstractos? hace el trabajo. Me pregunto qué sucede con las clases que tienen, por ejemplo, 4 o 5 métodos abstractos cuando se debe verificar el nombre de cada método para ejecutar el código del método. Parece algo torpe (es decir, si (abstractMethod1.equals) method.getName()) doThis(); else if (abstractMethod2.equals (method.getName()) doThis1(); etc – mburke13

0

Sí, ese enfoque debería funcionar. Pero será costoso si haces mucha generación de clases. (Probablemente estamos hablando de cientos de miles de instrucciones para generar el archivo de código de bytes y luego cargarlo. Y luego está la memoria necesita representar la clase cuando se carga.)

Otro enfoque (también costoso) es generar código fuente y compilar y cargarlo en tiempo de ejecución.

Finalmente, debe considerar el enfoque de hacer que la lógica de los objetos sea conducida por tablas o implementarla usando algún tipo de intérprete. Si realmente necesita para tener diferentes clases, puede resumir esto utilizando el mecanismo de clase de proxy dinámico de Java; p.ej. ver java.lang.reflect.Proxy

+0

Estaba buscando la opción proxy, pero estoy limitado por el hecho de que los métodos abstractos deben declararse en una clase abstracta y no en una interfaz (necesito la clase abstracta para extender una clase). La generación de las clases se haría al inicio y no sería más de 10-100 clases. – mburke13

+0

@Matt: podría refactorizar su clase abstracta para que sea una interfaz y una clase abstracta que la implemente, luego ajustar la instancia de la clase abstracta dentro del proxy. Para ese número de clases, el enfoque de generación de código no debería ser demasiado caro, pero es una forma complicada y (potencialmente) frágil de abordar este problema ... si los métodos generados deben ser complicados. (Y si no lo hacen, un enfoque basado en tablas sería más simple.) –

1

Qué te impide leer el valor de decir 5 propiedades y regresar de nuevo? Eso es demasiado simple, así que supongo que debe tener algo más complejo que devolver un int que quiera lograr aquí. Estoy de acuerdo con las publicaciones anteriores que generar clases en tiempo de ejecución sería muy costoso. Si conoce su lógica comercial por adelantado, puede aplicar el patrón Factory para cargar la implementación deseada de las interfaces definidas en tiempo de ejecución. Así es como funcionan las bibliotecas JDBC.

Si no conoce la lógica comercial de antemano y tiene mucho de ella, entonces puede beneficiarse de usar un Rule Engine estándar para procesar la lógica y devolver los resultados a su programa Java. Es mucho más fácil mantener esta lógica en un Motor de reglas especialmente si está cambiando con frecuencia.

Cuestiones relacionadas