JacoDB est une bibliothèque Java pure qui vous permet d'obtenir des informations sur Java Bytecode en dehors du processus JVM et de les stocker dans une base de données. Alors que Java Reflection permet d'inspecter le code à l'exécution, JacoDB fait de même pour ByteCode stocké dans un système de fichiers.
JacoDB utilise le framework ASM pour la lecture et l'analyse des bytecode Java.
Des informations sur les classes, les hiérarchies, les annotations, les méthodes, les champs et leurs usages sont stockées dans la base de données SQLite - en mémoire ou persistante. Les données persistantes peuvent être réutilisées entre les redémarrages. L'accès au stockage persistant à partir de plusieurs processus n'est pas pris en charge simultanément.
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 " )ou
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 >L'API a deux niveaux: celui représentant dans le système de fichiers ( bytecode et classes ) et celui apparaissant à l'exécution ( types ).
class : classe avec méthodes, champs, etc. Les deux niveaux sont connectés à JcClasspath . Vous ne pouvez pas modifier les classes récupérées à partir de bytecode pur. Les types peuvent être construits manuellement par substitution générique.
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 ());
}Kotlin
val database = jacodb {
useProcessJavaRuntime()
}
val clazz = database.classpath().findClassOrNull( " java.lang.String " )Exemples plus complexes:
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 ();
}
}Kotlin
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 classe pourrait être dans un environnement incomplet (c'est-à-dire une super classe, une interface, un type de retour ou un paramètre de méthode ne se trouve pas dans ClassPath), alors l'API lancera NoClassInClasspathException au moment de l'exécution. Pour fixer cette fonction Installer UnknownClasses dans ClassPath pendant la création.
La base de données peut surveiller les modifications du système de fichiers en arrière-plan et actualiser explicitement les fichiers de jar:
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.Kotlin
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.le type peut être représenté comme l'un des
Il représente le comportement d'exécution en fonction de la substitution des paramètres dans le type générique donné:
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`
}Kotlin
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`
} Les fonctionnalités peuvent être utilisées pour JcDatabase entière ou JcClasspath en particulier
Caractéristique de la base de données pour rechercher des usages de champs et de méthodes.
val database = jacodb {
install( Usages )
}Voir plus
Se moque des classes inconnues. C'est une façon de gérer les chemins de classe incomplets
val database = jacodb {}
val classpath = database.classpath( listOf ( UnknownClasses ))
val clazz = classpath.findClass( " UnknownClasses " ) // will return `JcClassOrInterface` instanceVoir plus
Hiérarchie un peu plus rapide mais consomment plus de mémoire RAM:
val database = jacodb {
install( InMemoryHierarchy )
}Voir plus
Les instances de JcClassOrInterface , JcMethod et JcClasspath sont en file d'infiltration et immuables.
JcClasspath représente un instantané indépendant de classes, qui ne peut pas être modifiée car elle est créée. La suppression ou la modification des fichiers de bibliothèque n'affecte pas la structure des instances JcClasspath . La méthode JcClasspath#close publie tous les instantanés et nettoie les données persistantes si certaines bibliothèques sont obsolètes.
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
}Kotlin
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 S'il existe une demande pour une instance JcClasspath contenant les bibliothèques, qui n'ont pas encore été indexées, le processus d'indexation est déclenché et la nouvelle instance de l'ensemble JcClasspath est renvoyée.
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
}Kotlin
val database = jacodb {
loadByteCode( listOf (lib1))
persistent( " " )
}
val cp = database.classpath( listOf (buildDir)) // database will automatically process buildDir JacoDB est en file d'infiltration. Si l'on demande l'instance JcClasspath tout en chargeant des fichiers JAR à partir d'un autre thread, JcClasspath ne peut représenter qu'un état cohérent des fichiers de jar en cours de charge. C'est le fichier de jar complètement chargé qui apparaît dans JcClasspath . Veuillez noter: il n'y a aucune garantie que tous les fichiers de jar, soumis pour le chargement, seront réellement chargés.
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 ();
}
}Kotlin
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))
}
}Le chargement de bytecode se compose de deux étapes:
JcFeature , etc.) Les instances JacoDB ou JcClasspath sont renvoyées juste après la première étape. Vous récupérez la représentation finale des classes au cours de la deuxième étape. Il est possible que les fichiers .class subissent des modifications à un moment donné entre la première étape et la seconde, et la représentation des classes est affectée en conséquence.
Voici les résultats des références et la migration de suie.