JacoDB es una biblioteca Java pura que le permite obtener información sobre Java Bytecode fuera del proceso JVM y almacenarla en una base de datos. Si bien Java Reflection hace posible inspeccionar el código en tiempo de ejecución, JacoDB hace lo mismo para el bytecode almacenado en un sistema de archivos.
JacoDB utiliza ASM Framework para leer y analizar Java Bytecode.
La información sobre clases, jerarquías, anotaciones, métodos, campos y sus usos se almacena en la base de datos SQLite, ya sea en memoria o persistente. Los datos persistidos se pueden reutilizar entre los reinicios. No se admite acceder al almacenamiento persistente desde múltiples procesos simultáneamente.
Gradle:
implementation(group = " org.jacodb " , name = " jacodb-api " , version = " 1.2.0 " )
implementation(group = " org.jacodb " , name = " jacodb-core " , version = " 1.2.0 " )
implementation(group = " org.jacodb " , name = " jacodb-analysis " , version = " 1.2.0 " )o
Maven:
< dependencies >
< dependency >
< groupId >org.jacodb</ groupId >
< artifactId >jacodb-core</ artifactId >
< verison >1.2.0</ verison >
</ dependency >
< dependency >
< groupId >org.jacodb</ groupId >
< artifactId >jacodb-api</ artifactId >
< verison >1.2.0</ verison >
</ dependency >
< dependency >
< groupId >org.jacodb</ groupId >
< artifactId >jacodb-analysis</ artifactId >
< verison >1.2.0</ verison >
</ dependency >
</ dependencies >La API tiene dos niveles: el que representa en el sistema de archivos ( bytecode y classes ) y el que aparece en tiempo de ejecución ( tipos ).
class : clase con métodos, campos, etc. Ambos niveles están conectados a JcClasspath . No puede modificar las clases recuperadas de Pure ByTecode. Los tipos pueden construirse manualmente por sustitución de genéricos.
public static void getStringFields () throws Exception {
JcDatabase database = JacoDB . async ( new JcSettings (). useProcessJavaRuntime ()). get ();
JcClassOrInterface clazz = database . asyncClasspath ( emptyList ()). get (). findClassOrNull ( "java.lang.String" );
System . out . println ( clazz . getDeclaredFields ());
}Kotlín
val database = jacodb {
useProcessJavaRuntime()
}
val clazz = database.classpath().findClassOrNull( " java.lang.String " )Ejemplos más complejos:
Java
class Example {
public static MethodNode findNormalDistribution () throws Exception {
File commonsMath32 = new File ( "commons-math3-3.2.jar" );
File commonsMath36 = new File ( "commons-math3-3.6.1.jar" );
File buildDir = new File ( "my-project/build/classes/java/main" );
JcDatabase database = JacoDB . async (
new JcSettings ()
. useProcessJavaRuntime ()
. persistent ( "/tmp/compilation-db/" + System . currentTimeMillis ()) // persist data
). get ();
// Let's load these three bytecode locations
database . asyncLoad ( Arrays . asList ( commonsMath32 , commonsMath36 , buildDir ));
// This method just refreshes the libraries inside the database. If there are any changes in libs then
// the database updates data with the new results.
database . asyncLoad ( Collections . singletonList ( buildDir ));
// Let's assume that we want to get bytecode info only for `commons-math3` version 3.2.
JcClassOrInterface jcClass = database . asyncClasspath ( Arrays . asList ( commonsMath32 , buildDir ))
. get (). findClassOrNull ( "org.apache.commons.math3.distribution.NormalDistribution" );
System . out . println ( jcClass . getDeclaredMethods (). size ());
System . out . println ( jcClass . getAnnotations (). size ());
System . out . println ( JcClasses . getConstructors ( jcClass ). size ());
// At this point the database read the method bytecode and return the result.
return jcClass . getDeclaredMethods (). get ( 0 ). asmNode ();
}
}Kotlín
suspend fun findNormalDistribution (): MethodNode {
val commonsMath32 = File ( " commons-math3-3.2.jar " )
val commonsMath36 = File ( " commons-math3-3.6.1.jar " )
val buildDir = File ( " my-project/build/classes/java/main " )
val database = jacodb {
useProcessJavaRuntime()
persistent( " /tmp/compilation-db/ ${ System .currentTimeMillis()} " ) // persist data
}
// Let's load these three bytecode locations
database.load( listOf (commonsMath32, commonsMath36, buildDir))
// This method just refreshes the libraries inside the database. If there are any changes in libs then
// the database updates data with the new results.
database.load( listOf (buildDir))
// Let's assume that we want to get bytecode info only for `commons-math3` version 3.2.
val jcClass = database.classpath( listOf (commonsMath32, buildDir))
.findClass( " org.apache.commons.math3.distribution.NormalDistribution " )
println (jcClass.declaredMethods.size)
println (jcClass.constructors.size)
println (jcClass.annotations.size)
// At this point the database read the method bytecode and return the result.
return jcClass.methods[ 0 ].asmNode()
} La clase podría estar en un entorno incompleto (es decir, la súper clase, la interfaz, el tipo de retorno o el parámetro de método no se encuentran en classpath), la API arrojará NoClassInClasspathException en tiempo de ejecución. Para corregir esta función de instalación UnknownClasses en classpath durante la creación.
La base de datos puede observar los cambios del sistema de archivos en segundo plano y actualizar los archivos jar explícitamente:
Java
public static void watchFileSystem () throws Exception {
JcDatabase database = JacoDB . async ( new JcSettings ()
. watchFileSystem ()
. useProcessJavaRuntime ()
. loadByteCode ( Arrays . asList ( lib1 , buildDir ))
. persistent ( "" , false )). get ();
}
// A user rebuilds the buildDir folder.
// The database re-reads the rebuilt directory in the background.Kotlín
val database = jacodb {
watchFileSystem()
useProcessJavaRuntime()
loadByteCode( listOf (lib1, buildDir))
}
// A user rebuilds the buildDir folder.
// The database re-reads the rebuilt directory in the background.El tipo puede representarse como uno de
Representa el comportamiento de tiempo de ejecución de acuerdo con la sustitución de los parámetros en el tipo genérico dado:
Java
public static class A < T > {
T x = null ;
}
public static class B extends A < String > {
}
public static void typesSubstitution () {
JcClassType b = ( JcClassType ) classpath . findTypeOrNull ( "org.jacodb.examples.JavaReadMeExamples.B" );
JcType xType = b . getFields ()
. stream ()
. filter ( it -> "x" . equals ( it . getName ()))
. findFirst (). get (). getFieldType ();
JcClassType stringType = ( JcClassType ) classpath . findTypeOrNull ( "java.lang.String" );
System . out . println ( xType . equals ( stringType )); // will print `true`
}Kotlín
open class A < T >( val x : T )
class B ( x : String ): A<String>(x)
suspend fun typesSubstitution () {
val db = jacodb {
loadByteCode( listOf ( File ( " all-classpath " )))
}
val classpath = db.classpath( listOf ( File ( " all-classpath " )))
val b = classpath.findClass< B >().toType()
println (b.fields.first { it.name == " x " }.fieldType == classpath.findClass< String >().toType()) // will print `true`
} Las características podrían usarse para JcDatabase integral o JcClasspath particular
Característica de la base de datos para buscar usos de campos y métodos.
val database = jacodb {
install( Usages )
}ver más
Se burla de clases desconocidas. Es una forma de gracia de manejar classpats incompletos
val database = jacodb {}
val classpath = database.classpath( listOf ( UnknownClasses ))
val clazz = classpath.findClass( " UnknownClasses " ) // will return `JcClassOrInterface` instancever más
Jerarquía un poco más rápida pero consumo más memoria RAM:
val database = jacodb {
install( InMemoryHierarchy )
}ver más
Las instancias de JcClassOrInterface , JcMethod y JcClasspath son seguras e inmutables.
JcClasspath representa una instantánea independiente de clases, que no se puede modificar desde que se crea. Eliminar o modificar los archivos de la biblioteca no afecta la estructura de instancia JcClasspath . El método JcClasspath#close libera todas las instantáneas y limpia los datos persistidos si algunas bibliotecas están desactualizadas.
Java
public static void refresh () throws Exception {
JcDatabase database = JacoDB . async (
new JcSettings ()
. watchFileSystem ()
. useProcessJavaRuntime ()
. loadByteCode ( Arrays . asList ( lib1 , buildDir ))
. persistent ( "..." )
). get ();
JcClasspath cp = database . asyncClasspath ( Collections . singletonList ( buildDir )). get ();
database . asyncRefresh (). get (); // does not affect cp classes
JcClasspath cp1 = database . asyncClasspath ( Collections . singletonList ( buildDir )). get (); // will use new version of compiled results in buildDir
}Kotlín
val database = jacodb {
watchFileSystem()
useProcessJavaRuntime()
loadByteCode( listOf (lib1, buildDir))
persistent( " " )
}
val cp = database.classpath( listOf (buildDir))
database.refresh() // does not affect cp classes
val cp1 = database.classpath( listOf (buildDir)) // will use new version of compiled results in buildDir Si hay una solicitud de una instancia de JcClasspath que contiene las bibliotecas, que aún no se han indexado, el proceso de indexación se activa y se devuelve la nueva instancia del conjunto JcClasspath .
Java
public static void autoProcessing () throws Exception {
JcDatabase database = JacoDB . async (
new JcSettings ()
. loadByteCode ( Arrays . asList ( lib1 ))
. persistent ( "..." )
). get ();
JcClasspath cp = database . asyncClasspath ( Collections . singletonList ( buildDir )). get (); // database will automatically process buildDir
}Kotlín
val database = jacodb {
loadByteCode( listOf (lib1))
persistent( " " )
}
val cp = database.classpath( listOf (buildDir)) // database will automatically process buildDir JacoDB es seguro de hilo. Si se solicita una instancia JcClasspath mientras carga los archivos jar de otro hilo, JcClasspath puede representar solo un estado consistente de los archivos jares que se están cargando. Es el archivo de jarro completamente cargado que aparece en JcClasspath . Tenga en cuenta: no hay garantía de que todos los archivos de jar, enviados para la carga, realmente se cargarán.
Java
class Example {
public static void main ( String [] args ) {
val db = JacoDB . async ( new JcSettings ()). get ();
new Thread (() -> db . asyncLoad ( Arrays . asList ( lib1 , lib2 )). get ()). start ();
new Thread (() -> {
// maybe created when lib2 or both are not loaded into database
// but buildDir will be loaded anyway
var cp = db . asyncClasspath ( buildDir ). get ();
}). start ();
}
}Kotlín
val db = jacodb {
persistent( " " )
}
thread(start = true ) {
runBlocking {
db.load( listOf (lib1, lib2))
}
}
thread(start = true ) {
runBlocking {
// maybe created when lib2 or both are not loaded into database
// but buildDir will be loaded anyway
val cp = db.classpath( listOf (buildDir))
}
}La carga de código de bytecodo consta de dos pasos:
JcFeature , etc.) Las instancias JacoDB o JcClasspath se devuelven justo después de que se realiza el primer paso. Recuperas la representación final de las clases durante el segundo paso. Es posible que los archivos .class experimenten cambios en algún momento entre el primer paso y el segundo, y la representación de clases se ve afectada en consecuencia.
Aquí hay resultados de referencia y migración de hollín.