5

I would like to compile and load new classes at runtime within a weblogic 10.3 server. Class loading seems to be somewhat straightforward:

class ClassFileManager 
extends ForwardingJavaFileManager<StandardJavaFileManager> {

  Map<String, JavaClassObject> classes = new HashMap<String, JavaClassObject>();

  public ClassFileManager(StandardJavaFileManager standardManager) {
    super(standardManager);
  }

  @Override
  public ClassLoader getClassLoader(Location location) {
    return new SecureClassLoader(currentThread().getContextClassLoader()) {
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = classes.get(name).getBytes();
        return super.defineClass(name, b, 0, b.length);
      }
    };
  }

  @Override
  public JavaFileObject getJavaFileForOutput(
      Location location, String className, Kind kind, FileObject sibling)
      throws IOException {
    JavaClassObject result = new JavaClassObject(className, kind);
    classes.put(className, result);
    return result;
  }
}

The simplest way to perform class loading seems to be to initialise a SecureClassLoader and have it use the contextClassLoader as the parent.

But when setting up the -classpath option for the JDK's runtime compiler, I cannot seem to find a "context classpath" in a string form. The following is a bit of a hack that works "well enough":

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
ClassFileManager fileManager = 
    new ClassFileManager(compiler.getStandardFileManager(null, null, null));
List<String> options = new ArrayList<String>();
options.add("-classpath");
options.add(System.getProperty("java.class.path") + ";" +
    getClass().getProtectionDomain()
              .getCodeSource().getLocation()
              .toURI().toString()
              .replace("file:/", "").replace("/", "\\"));

But it doesn't generate the complete class path of the context class loader. How can I do it, reliably? Can I?

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • 1
    Few issues, hashmap should be some Concurrent type and a check of the likes of `if (classes.get(name)==null) return super.findClass(name);` should be in the beginning of the method. Also having the method returning a new ClassLoader is very likely wrong (as you'd like only one loader for all the files) – bestsss May 05 '14 at 14:10
  • @bestsss: *concurrent hashmap*: That isn't necessary in the "real world case", as we're synchronizing access to `ClassFileManager`. *null checks* and *new classloader per call*: Yes, you're right. The code sample is not up to date. But the question is more about the compilation part, not the class-loading part. – Lukas Eder May 05 '14 at 14:13
  • Why do you need to pass a classpath to the compiler at all? If you're passing the compiler a classloader that delegates to the context classloader, shouldn't that already make all of the classes in the context resolvable? – Peter G May 07 '14 at 19:59
  • I don't have WebLogic at hands and I'm not familiar with it, so I can't guarantee if that would return the expected paths for your WebLogic setup, but the `ClassLoader#getResources()` passing an empty string gives you an enumeration of all URLs to classpath resources. See also among others http://stackoverflow.com/questions/3222638/get-all-of-the-classes-in-the-classpath/3223019#3223019 – BalusC May 08 '14 at 05:06
  • @BalusC: Hmm, I did come across this method, but in a simple check, this didn't seem to provide a complete listing of all resources that *should* be listed... I will check again, though – Lukas Eder May 08 '14 at 07:50
  • @BalusC: Passing an empty String to `ClassLoader.getResources` returns an empty enumeration, at least in our WLS instance. – Lukas Eder May 09 '14 at 09:07
  • @PeterG: How can you pass a class loader to the compiler? The `compiler.getTask()` method only takes this options argument... – Lukas Eder May 09 '14 at 09:08
  • Even on the context class loader? Okay, good to know. It works at least that way for us in JBoss and Tomcat. – BalusC May 09 '14 at 09:08
  • @LukasEder You are passing fileManager to getTask, aren't you? You've overridden its getClassLoader method, but perhaps it's delegating to the wrong classloader. – Peter G May 09 '14 at 14:13

5 Answers5

2

WebLogic 10.3.6 has a fairly complex ClassLoader implementation. Fortunately the classloader used for web applications exposes a getClassPath method.

ClassLoader cl = Thread.currentThread().getContextClassLoader();
String classPath = ((weblogic.utils.classloaders.GenericClassLoader)cl).getClassPath();

// Once we have a classpath it's standard procedure
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager sfm = compiler.getStandardFileManager(null, null, null);
List<String> optionList = new ArrayList<String>();
optionList.addAll(Arrays.asList("-classpath", classPath));
compiler.getTask(null, sfm, null, optionList, null, sources).call();
anttix
  • 7,709
  • 1
  • 24
  • 25
  • Hmm, that would be surprisingly simple! I'll have to check again, if my own WLS `ClassLoader` instance has a `getClassPath()` method. If so, I'll beat myself for having overlooked it ;-) – Lukas Eder May 08 '14 at 07:44
  • That's it! Awesome man, this thing lists everything that's on the current classpath. – Lukas Eder May 09 '14 at 09:03
1

Maybe this can help you. It works for my project on WebLogic.

String getClassPath() {
    final String BASE_PATH = "<your_project_folder_name>";
    String path = "";

    String classPathProperty = System.getProperty("java.class.path");
    if (classPathProperty != null) {
        path = classPathProperty + File.pathSeparator;
    }

    URL classLocation = this.getClass().getProtectionDomain().getCodeSource().getLocation();
    URL classesLocation = this.getClass().getClassLoader().getResource("/");
    if (classesLocation == null) {
        path = path + classLocation.getPath();
    }
    else {
        String classesLocationPath = classesLocation.getPath();
        String libsLocationPath = classesLocationPath + "../lib";
        File libsLocation = new File(libsLocationPath);
        if (libsLocation.exists() == false) {
            libsLocationPath = URLDecoder.decode(classesLocationPath + "../" + BASE_PATH + "/WEB-INF/lib/");
            libsLocation = new File(libsLocationPath);
        }

        File[] filesInLibraryPath = libsLocation.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(".jar");
            }
        });
        if (filesInLibraryPath != null) {
            for (File libraryFile : filesInLibraryPath) {
                libsLocationPath += File.pathSeparator + URLDecoder.decode(libraryFile.getAbsolutePath());
            }
        }
        path =  path +
                classLocation.getPath() + File.pathSeparator + 
                classesLocationPath + File.pathSeparator + 
                libsLocationPath;
        path = URLDecoder.decode(path);
    }
    return path;
}
sozal
  • 41
  • 2
  • Yes, we've actually tried this in our project in weblogic as well. It appeared to work but it is very unreliable. It actually relies on the fact that all of our .war file's contained .jars are effectively contained in the same temp directory. Besides, we don't have access to all the other objects on the .war file's class path, e.g. endorsed dirs and much more. So this is really a hack, if anything :-) – Lukas Eder May 05 '14 at 12:41
1

The Open Source Jasper JSP compiler used by Tomcat interrogates context URLClassLoader to generate a classpath string that is passed to the compiler.

If WebLogic does not expose getURLs method, an alternative is to use a custom implementation of JavaFileManager that uses context classloader getResource() method to fetch class files.

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager customFileManager = ...
compiler.getTask(null, customFileManager, null, null, null, sources).call();

There is a complete example available here.

anttix
  • 7,709
  • 1
  • 24
  • 25
  • Unfortunately, I don't think that WLS exposes a `URLClassLoader` (see [this answer](http://stackoverflow.com/a/23481163/521799)). Interesting idea to use the `JavaFileManager` for this, though. Could you elaborate with an example? – Lukas Eder May 07 '14 at 05:28
1

I suggest you make your custom JavaFileManager aware of the context classloader and specify it as an argument to JavaCompiler.getTask, along the lines of @anttix's idea.

For more info and a sample implementation including explanation (which is too verbose to repeat here) see blog post Using built-in JavaCompiler with a custom classloader.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Thanks for the suggestion. Luckily, there was a more robust and straight-forward solution in the end. – Lukas Eder May 09 '14 at 09:05
  • Very well, the solution you chose looks simple - but WebLogic-specific. My suggestion would potentially work in any container if implemented cleanly. So much for robust. :-) – kriegaex May 09 '14 at 10:45
  • This is for a contracting position at a bank. They have used WLS for the last 30 years. They will continue to use it for the next 50 years. And to be fair, I've warned you by putting the [tag:weblogic] tag on the question ;-) But agreed. Robustness depends on the context :-) – Lukas Eder May 09 '14 at 13:39
  • That's fine with me. Maybe in two years when they switch their AS unexpectedly you can make extra money by spicing up that part of the code. ;-) As an Agile Coach and Clean Code proponent I have heard that kind of argument ("it was always like this, it will never change") too often and saw it change too often to still rely on it. I have helped dozens of Scrum teams save their own asses by reminding them that customers (especially those with zero technology knowledge) can change their minds unexpectedly - and in very many cases they did, and the teams were always surprised, but safe. – kriegaex May 10 '14 at 14:19
0

Since "WebApplicationClassLoader" is a kind of "URLClassLoader", maybe you can use this code snippet.

ClassLoader classLoader = getClass().getClassLoader();
System.out.println("ClassLoader: " + classLoader);
URLClassLoader urlClassLoader = (URLClassLoader)classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL u : urls) {
    System.out.println("url: " + u);
}

This code lists all jars and directories in classpath.

sozal
  • 41
  • 2
  • Unfortunately, that will give you a `ClassCastException`, at least in our installation – Lukas Eder May 05 '14 at 20:53
  • 1
    @LukasEder I could, but I downloaded weblogic and posted another answer for a easier way. The issue is with the list method in the interface. ClassLoader does not provide a way to list classes thus you have to use a third party Reflections library to enumerate classes. Fairly large amount of code is required to implement it. – anttix May 08 '14 at 05:19
  • http://stackoverflow.com/questions/2548384/java-get-a-list-of-all-classes-loaded-in-the-jvm - just a reference about the usage of Reflections library – anttix May 08 '14 at 05:28
  • For the reference, our `ClassLoader` is a `ChangeAwareClassLoader`, so this solution doesn't work, unfortunately. @anttix: Nice trickery, but I prefer your other answer ;-) – Lukas Eder May 09 '14 at 09:05