spring-bootjava-compiler-apiruntime-compilationjavacompiler

Compile Java class in runtime with dependencies to nested jar


In a Spring Boot app I'm doing the following at runtime:

  1. Generating a Java class
  2. Compiling it
  3. Accessing some static fields of the compiled class using reflection.

I've based my code on this post and got a problem compiling my generated class in runtime. When running in the IDE compilation works just fine but when running from a Spring Boot jar compilation fails saying symbols are missing or some package does not exist. The class I'm compiling has dependencies to other classes that reside in a jar under \BOOT-INF\lib\ and it seems the compiler fails to load those classes using the existing class loader.

I've followed this post which suppose to address this specific problem but I got UnsupportedOperationException coming from method

default Iterable<Set<Location>> listLocationsForModules(Location location) throws IOException {
    throw new UnsupportedOperationException();
}

of interface JavaFileManager.

I've encountered another possible solution given here but I'm not clear exactly with the full implementation.

This seems like a well known issue when compiling a class in runtime, is there any clear solution for that?

I'm currently using Java 10.0.2.


Solution

  • Although you haven't mentioned it explicitly I think you are running a version of Java with modules (JDK 9+), but the guides you have been following are for earlier versions starting from Java 6. This is why you are getting the error about unsupported listLocationsForModules because the JDK developers retrofitted the FileManager with with default methods that throw UnsupportedOperationException.

    If you don't actually want to use a version of Java greater than 8, I would stick with JDK8 it will be much easier!

    I'll continue assuming you do want to use Java 9 and above (tested my code in Java 11) however:

    For handling modules it is sufficient your file manager to delegate to the standard file manager:

    @Override
    public Location getLocationForModule(Location location, String moduleName) throws IOException {
        return standardFileManager.getLocationForModule(location, moduleName);
    }
    
    @Override
    public Location getLocationForModule(Location location, JavaFileObject fo) throws IOException {
        return standardFileManager.getLocationForModule(location, fo);
    }
    
    @Override
    public Iterable<Set<Location>> listLocationsForModules(Location location) throws IOException {
        return standardFileManager.listLocationsForModules(location);
    }
    
    @Override
    public String inferModuleName(Location location) throws IOException {
        return standardFileManager.inferModuleName(location);
    }
    

    I also found it necessary to modify Atamur's code to check explicitly for the base java module (so that we can resolve java.lang in Java 9+!) and delegate to the standard file manager as you would have for the platform class path in previous versions:

    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
        boolean baseModule = location.getName().equals("SYSTEM_MODULES[java.base]");
        if (baseModule || location == StandardLocation.PLATFORM_CLASS_PATH) { // **MODIFICATION CHECK FOR BASE MODULE**
            return standardFileManager.list(location, packageName, kinds, recurse);
        } else if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
            if (packageName.startsWith("java") || packageName.startsWith("com.sun")) {
                return standardFileManager.list(location, packageName, kinds, recurse);
            } else { // app specific classes are here
                return finder.find(packageName);
            }
        }
        return Collections.emptyList();
    
    }
    

    Updates

    Some other points:

    Extracting embedded spring boot classes:

    Get the jarUri by looking for the last index of '!' in each packageFolderURL as in Taeyun Kim's comment and not the first as in the original example.

     private List<JavaFileObject> processJar(URL packageFolderURL) {
      List<JavaFileObject> result = new ArrayList<JavaFileObject>();
      try {
        // Replace:
        // String jarUri = packageFolderURL.toExternalForm().split("!")[0];
        // With:
        String externalForm = packageFolderURL.toExternalForm();
        String jarUri = externalForm.substring(0, externalForm.lastIndexOf('!'));
    
    
        JarURLConnection jarConn = (JarURLConnection) packageFolderURL.openConnection();
        String rootEntryName = jarConn.getEntryName();
        int rootEnd = rootEntryName.length()+1;
        // ...
    

    This allows package PackageInternalsFinder to return CustomJavaFileObject with full URIs to classes in embedded spring jars (under BOOT-INF/lib) which are then resolved with spring boot jar URI handler which is registered in similar way to as explained in this answer. The URI handling should just happen automatically through spring boot.