¿Sabías que las clases de Java se pueden generar dinámicamente en tiempo de ejecución?

Prefacio

El objetivo de esta publicación de blog es: ¿ qué métodos se pueden utilizar para generar dinámicamente una clase Java en tiempo de ejecución?  

Descripción general

Podemos analizar desde fuentes comunes de clases Java. El proceso de desarrollo habitual es que los desarrolladores escriban código Java, llamen a javac para compilarlo en un archivo de clase y luego lo carguen en la JVM a través del mecanismo de carga de clases, que se convierte en una clase Java que puede utilizarse cuando la aplicación se esté ejecutando.

Inspirándose en el proceso anterior, una forma directa es comenzar desde el código fuente. Puede usar un programa Java para generar un fragmento de código fuente y luego guardarlo en un archivo. A continuación, solo necesita resolver la compilación. problema.

Existe una forma estúpida de iniciar el proceso javac directamente usando ProcessBuilder o similar y especificar el archivo generado anteriormente como entrada para la compilación. Finalmente, use el cargador de clases para cargarlo en tiempo de ejecución.

El método anterior se compila esencialmente fuera del proceso del programa actual, entonces, ¿existe una forma menos lenta?

Puede considerar utilizar la API del compilador de Java, que es una API estándar proporcionada por JDK, que proporciona funciones de compilación equivalentes a javac. Para obtener más detalles, consulte los documentos relacionados con java.compiler.

Pensando más a fondo, nos hemos centrado en compilar el código fuente de Java en un código de bytes que la JVM pueda entender. En otras palabras, siempre que sea un código de bytes que cumpla con las especificaciones de la JVM, no importa cómo se genere, ¿puede ser cargado por la JVM? JVM? ¿Podemos generar directamente el código de bytes correspondiente y luego entregárselo al cargador de clases para que lo cargue? Por supuesto que es posible, pero escribir código de bytes directamente es demasiado difícil. Por lo general, podemos usar herramientas de manipulación de código de bytes de Java y bibliotecas de clases para lograrlo.  

texto

Primero, comprendamos la conversión de una clase de código de bytes a objeto de clase. En el proceso de carga de clases, este paso se realiza a través de las funciones proporcionadas por el siguiente método u otras implementaciones locales equivalentes de defineClass.

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                   ProtectionDomain protectionDomain)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                   ProtectionDomain protectionDomain)
复制代码

Aquí solo se seleccionan las dos implementaciones típicas de defineClass más básicas. Java sobrecarga varios métodos diferentes.

Se puede ver que siempre que se pueda generar un código de bytes estandarizado, ya sea en forma de una matriz de bytes o colocado en un ByteBuffer, el proceso de conversión de un código de bytes a un objeto Java se puede completar sin problemas.

El método defineClass proporcionado por JDK finalmente se implementa en código local.

static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                  ProtectionDomain pd, String source);

static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b,
                                  int off, int len, ProtectionDomain pd, String source);
复制代码

Yendo más allá, echemos un vistazo al código de implementación del proxy dinámico JDK . Encontrará que la lógica correspondiente se implementa en la clase interna estática ProxyBuilder. ProxyGenerator genera un código de bytes y lo guarda en forma de matriz de bytes, y luego llama a la entrada defineClass proporcionada por Unsafe.

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
      proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
try {
  Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
                                   0, proxyClassFile.length,
                                   loader, null);
  reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
  return pc;
} catch (ClassFormatError e) {.
// 如果出现ClassFormatError,很可能是输入参数有问题,比如,ProxyGenerator 有 bug
}
复制代码

Hemos aclarado el proceso de conversión de información de código de bytes binario a objetos de clase. Parece que no hemos analizado cómo generar el código de bytes que necesitamos. A continuación, echemos un vistazo a la lógica de manipulación del código de bytes relacionado.

Para conocer la lógica del proxy dinámico dentro de JDK, consulte la implementación interna de java.lang.reflect.ProxyGenerator . Creo que esto puede considerarse una tecnología alternativa de manipulación de códigos de bytes, que utiliza las capacidades proporcionadas por DataOutputStrem y coopera con varios métodos de implementación de instrucciones JVM codificadas para generar la matriz de códigos de bytes requerida.

private void codeLocalLoadStore(int lvar, int opcode, int opcode_0,
                              DataOutputStream out)
  throws IOException
{
  assert lvar >= 0 && lvar <= 0xFFFF;
  // 根据变量数值,以不同格式,dump操作码
    if (lvar <= 3) {
      out.writeByte(opcode_0 + lvar);
  } else if (lvar <= 0xFF) {
      out.writeByte(opcode);
      out.writeByte(lvar & 0xFF);
  } else {
      // 使用宽指令修饰符,如果变量索引不能用无符号byte
      out.writeByte(opc_wide);
      out.writeByte(opcode);
      out.writeShort(lvar & 0xFFFF);
  }
}
复制代码

La ventaja de esta implementación es que no tiene muchas dependencias y es simple y práctica. Sin embargo, la premisa es que es necesario comprender varias instrucciones de JVM y saber cómo manejar esas direcciones de desplazamiento. El umbral real es muy alto, por lo que No es adecuado para la mayoría de escenas de desarrollo ordinarias.

Afortunadamente, los expertos de la comunidad Java proporcionan una variedad de bibliotecas de manipulación de códigos de bytes desde niveles de abstracción de bajo nivel hasta niveles más altos, por lo que no necesitamos hacer todo desde cero. La biblioteca de clases ASM está integrada dentro del JDK y, aunque no se expone como una API pública, se usa ampliamente, por ejemplo, en la implementación subyacente de la API java.lang.instrumentation o en la lógica interna generada por la llamada Lambda. Sitio. Implementaré estos códigos. No los ampliaré aquí. Si está realmente interesado o lo necesita, puede consultar la lógica de generación de código de bytes similar a LamdaForm: java.lang.invoke.InvokerBytecodeGenerator.

Piénselo desde una perspectiva relativamente práctica: ¿qué debe hacer para implementar un proxy dinámico simple? ¿Cómo utilizar la tecnología de manipulación de códigos de bytes para realizar este proceso?

Para un proxy dinámico Java ordinario, su proceso de implementación se puede simplificar a:

  • Proporcione una interfaz básica como punto de entrada unificado entre el tipo llamado (com.mycorp.HelloImpl) y la clase de proxy, como com.mycorp.Hello.
  • Implemente InvocationHandler y la llamada al método del objeto proxy se enviará a su método de invocación para implementar realmente la acción.
  • A través de la clase Proxy, llame a su método newProxyInstance para generar una instancia de clase proxy que implemente la interfaz básica correspondiente. Puede ver la firma del método a continuación.
public static Object newProxyInstance(ClassLoader loader,
                                    Class<?>[] interfaces,
                                    InvocationHandler h)
复制代码

Analicemos, ¿en qué etapa ocurre específicamente la generación de código dinámico?

Sí, ahí es cuando newProxyInstance genera una instancia de clase de proxy. Elegí ASM utilizado por el propio JDK como ejemplo. Echemos un vistazo al breve proceso de uso de ASM. Consulte el fragmento de código de muestra a continuación.

El primer paso es generar la clase correspondiente, que en realidad es muy similar a cómo escribimos código Java, excepto que usamos métodos ASM y parámetros específicos en lugar del código fuente que escribimos.

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

cw.visit(V1_8,                      // 指定Java版本
      ACC_PUBLIC,                   // 说明是public类型
      "com/mycorp/HelloProxy",      // 指定包和类的名称
      null,                         // 签名,null表示不是泛型
      "java/lang/Object",                    // 指定父类
      new String[]{ "com/mycorp/Hello" });   // 指定需要实现的接口
复制代码

Además, podemos generar los métodos y la lógica necesarios para la instancia del objeto proxy según sea necesario.

MethodVisitor mv = cw.visitMethod(
      ACC_PUBLIC,                 // 声明公共方法
      "sayHello",                 // 方法名称
      "()Ljava/lang/Object;",     // 描述符
      null,                       // 签名,null表示不是泛型
      null);                      // 可能抛出的异常,如果有,则指定字符串数组

mv.visitCode();
// 省略代码逻辑实现细节
cw.visitEnd();                      // 结束类字节码生成
复制代码

Aunque el código anterior es un poco oscuro, aún puede comprender su propósito hasta cierto punto. Los diferentes métodos visitX proporcionan lógica para crear tipos, crear varios métodos, etc. ASM API utiliza ampliamente el modo Visitante. Si está familiarizado con este modo, sabrá que el escenario al que se dirige es desacoplar algoritmos y estructuras de objetos. Es muy adecuado para situaciones de manipulación de código de bytes, porque la mayoría de nosotros confiamos en Modificar o agregar nuevos métodos, variables o tipos a una estructura específica.

Según el análisis anterior, la mayoría de las operaciones de código de bytes deberían generar en última instancia matrices de bytes, y ClassWriter proporciona un método conveniente.

cw.toByteArray();
复制代码

Luego, podemos ingresar al proceso de carga de clases con el que estamos familiarizados;

La última pregunta es, además del proxy dinámico, ¿dónde más se puede aplicar la tecnología de manipulación de códigos de bytes?

Esta tecnología parece estar muy lejos de nuestro desarrollo diario, pero de hecho ha penetrado en todos los aspectos. Tal vez muchos de los marcos y herramientas que estás utilizando ahora apliquen esta tecnología. Aquí hay algunas áreas comunes que se me ocurren.

  • Varios marcos simulados
  • marco ORM
  • contenedor del COI
  • Algunas herramientas Profiler o herramientas de diagnóstico en tiempo de ejecución, etc.
  • Herramientas para generar código formal

Incluso se puede considerar que la tecnología de manipulación de códigos de bytes es una parte esencial de las herramientas y marcos básicos, lo que reduce en gran medida la carga para los desarrolladores.

posdata

Lo anterior es  todo.¿Sabías que las clases de Java se pueden generar dinámicamente en tiempo de ejecución?  todo el contenido;

Explora técnicas más profundas en carga de clases y manipulación de códigos de bytes. Para comprender los principios subyacentes, el ejemplo seleccionado es una biblioteca de clases de nivel relativamente bajo con capacidades integrales. Si se requieren operaciones básicas de código de bytes en proyectos reales, puede considerar usar una biblioteca de clases con una perspectiva de nivel superior.

Supongo que te gusta

Origin blog.csdn.net/2301_76607156/article/details/130525690
Recomendado
Clasificación