Tuesday, January 30, 2018

load a java class with a different parent classloader

in the process of debugging issues with an incorrect classpath for the compiler api when run inside maven, i wanted to be able to create a duplicate instance of an existing object (that had been produced by a factory) by loading the class using a custom classloader with a different parent. it didn't fix the classpath problem, so i also tried loading directly by the path that i found, to no greater success my solution is somewhere between kludgy and elegant, so i wanted to publish it in case some other sucker goes down a similar road less traveled (and it doesn't deserve it's own repository), so here it is: public static void demoJailbreak() throws Exception { ClassLoader parent = Thread.currentThread().getContextClassLoader(); String jre = System.getProperty("java.home"); String jdk = jre.endsWith("/jre") ? jre.substring(0,jre.length()-3) : jre; JavaCompiler broken, byPath=null, compiler; try { byPath = (JavaCompiler) loadByPath( "com.sun.tools.javac.api.JavacTool", "file:" + jdk + "lib/tools.jar", parent ); } catch (Exception ex) {} compiler = ToolProvider.getSystemJavaCompiler(); broken = (JavaCompiler) jailbreak(compiler.getClass(),parent); System.out.println("jailbreak : " + broken); System.out.println("loadByPath: " + byPath); } /** * create a new instance of a class * using a new url classloader with a specified parent * and the same resource path as the original class instance */ public static Object jailbreak(Class klass,ClassLoader parent) throws Exception { String cname = klass.getName(); String path = getPathForClass(cname,klass.getClassLoader()); Object dup = loadByPath(cname,path,parent); return dup; } /** return the path at which an original classloader finds a class at */ public static String getPathForClass(String cname,ClassLoader orig) throws Exception { String rname = cname.replace(".","/") + ".class"; java.net.URL url = orig.getResource(rname); String full = url.getPath(); // need to split the jar url on an exclamation mark per: // https://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html String path = full.split("!",0)[0]; return path; } /** * create a new instance of a class by name * from a new classloader using a file:/path/to/jar style path * and the designated parent */ public static Object loadByPath(String cname,String path,ClassLoader parent) throws Exception { java.net.URL url2 = new java.net.URL(path); java.net.URL [] urls = new java.net.URL [] {url2}; ClassLoader cl = new java.net.URLClassLoader(urls,parent); Class klass = cl.loadClass(cname); return klass.newInstance(); } all static methods, no maven dependencies and shouldn't require importing anything, so cut and paste should work. note: this example will only work if you have a jdk installed (the compiler isn't in the jre), and the byPath version will only work if the jre path is "jdk_path/jre". that said, the jailbreak and loadByPath will work fine anywhere

No comments: