Basic usage

    JacoDB API has two levels:

    • the one provides you with information represented in filesystem, i.e., with bytecode and classes;
    • the other one provides you with information appearing at runtime, i.e., with types.

    bytecode and classes represent data from .class files: class with methods, fields, etc.
    types represent types that can be nullable, parameterized, etc.

    Classes diagramClasses diagram
    Types diagramTypes diagram

    Both levels are connected to JcClasspath. You can't modify classes retrieved from pure bytecode. types may be constructed manually by generic substitution.

    JcClasspath is an entry point for both classes and types.

    JcClassOrInterface represents a class file. An array or a primitive gets no JcClassOrInterface.

    JcType represents a type from JVM runtime.

    JcClassType#methods contains:

    • all the public/protected/private methods of an enclosing class
    • all the ancestor methods visible at compile time
    • only constructor methods from the declaring class

    JcClassType#fields contains:

    • all the public/protected/private fields of an enclosing class
    • all the ancestor fields visible at compile time

    Get declared methods

    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).body();
        }
    }

    Note: the body method returns null if the to-be-processed JAR-file is changed or removed. If the class environment is incomplete (i.e., a superclass, an interface, a return type or a parameter of a method is not found in classpath), then the API throws NoClassInClasspathException at runtime.

    Watch for file system changes

    The database can watch for file system changes in the background and refresh the JAR-files explicitly:

    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.

    Get type information

    The represented types include

    • primitives,
    • classes,
    • arrays,
    • bounded and unbounded wildcards.

    The types level represents runtime behavior according to parameter substitution in the given generic type.

    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"
    }

    Multithreading

    The instances of JcClassOrInterface, JcMethod, and JcClasspath are thread-safe and immutable.

    JcClasspath represents an independent snapshot of classes, which cannot be modified since it is created. Removing or modifying library files does not affect JcClasspath instance structure. The JcClasspath#close method releases all snapshots and cleans up the persisted data if some libraries are outdated.

    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
        }

    If a request for a JcClasspath instance contains the libraries, which haven't been indexed yet, the indexing process is triggered and the new instance of the JcClasspath set is returned.

    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
            
    }

    JacoDB is thread-safe. If one requests JcClasspath instance while loading JAR-files from another thread, JcClasspath can represent only a consistent state of the JAR-files being loaded. It is the completely loaded JAR-file that appears in JcClasspath. Please note: it is not guaranteed that all the JAR-files, submitted for loading, will be actually loaded.

    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();
        }
    }

    Bytecode loading

    Bytecode loading consists of two steps:

    • retrieving information about the class names from the JAR-files or build directories,
    • reading classes bytecode from the JAR-files or build directories and processing it (persisting data, setting up JcFeature implementations, etc.).

    JacoDB or JcClasspath instances are returned right after the first step is performed. You retrieve the final representation of classes during the second step. The .class files may undergo changes at some moment between these two steps, and the classes representation is affected accordingly.