头条创作挑战赛 Java的一个重要特性是动态的类加载机制。通过在运行时动态地加载类,Java程序可以实现很多强大的功能。下面通过一个具体的实例来说明Java程序中,如何动态地编译Java源代码、加载类和执行类中的代码。这里的代码示例适用的版本是Java8。 示例所实现的功能很简单,就是对表达式求值。输入的是类似11或3(23)这样的表达式,返回的是表达式的值。示例的做法是动态创建一个Java源文件,编译该文件生成class文件,加载class文件之后再执行。比如,需要求值的表达式是11,那么所生成的Java源文件如下所示,其中11的部分是动态的。publicclassCalculator{publicstaticObjectcalculate(){return11;}} 我们只需要编译该源文件,加载编译之后的class文件,再通过反射API来调用其中的calculate方法就可以得到表达式求值的结果。编译 第一步是动态生成Java源代码并编译。生成Java源代码比较简单,直接用字符串连接就可以了。当然了,在生成逻辑比较复杂时,推荐的做法是使用字符串模板引擎,如Handlebars。在下面的代码中,getJavaSource方法生成Java源代码,compile方法进行编译。 在进行编译的时候,使用的是JDK标准的JavaCompiler接口。从源代码字符串中创建了一个JavaFileObject对象作为编译时的源代码单元。编译时的选项d指定了编译结果的输出路径,这里是一个临时文件夹。compile方法的返回值是一个Pair对象,包含了class文件的路径,以及随机生成的Java包的名称。publicclassDynamicCompilation{privatestaticfinalStringCLASSNAMECalculator;publicstaticPairPath,Stringcompile(Stringexpr)throwsIOException{StringpackageNamezUUID。randomUUID()。toString()。replace(,);PathoutputPathFiles。createTempDirectory(expr);JavaCompilercompilerToolProvider。getSystemJavaCompiler();StandardJavaFileManagerfileManagercompiler。getStandardFileManager(null,null,null);compiler。getTask(null,fileManager,null,ImmutableList。of(d,outputPath。toAbsolutePath()。toString()),null,Collections。singletonList(newStringContentJavaFileObject(CLASSNAME,getJavaSource(packageName,expr))))。call();returnPair。of(outputPath,packageName。CLASSNAME);}privatestaticStringgetJavaSource(StringpackageName,Stringexpr){returnpackagepackageName;publicclassCLASSNAME{publicstaticObjectcalculate(){returnexpr;}};}} 上面的代码用到了一个帮助类StringContentJavaFileObject,表示从字符串创建的JavaFileObject对象。publicclassStringContentJavaFileObjectextendsSimpleJavaFileObject{privatefinalStringcontent;publicStringContentJavaFileObject(Stringname,Stringcontent){super(URI。create(string:nameKind。SOURCE。extension),Kind。SOURCE);this。contentcontent;}OverridepublicCharSequencegetCharContent(booleanignoreEncodingErrors){returncontent;}} 加载 编译完成之后的第二步是动态加载类。这一步并没有实现自定义的类加载器,而且使用内置的系统类加载器。系统类加载器通过ClassLoader。getSystemClassLoader()方法来获取。系统类加载器在classpath上查找类。这里用了一个比较hack的技巧来动态修改系统类加载器的classpath。 在下面的代码中,ClasspathUpdater的addPath方法可以把一个Path对象表示的路径,添加到系统类加载器的查找路径中。这是因为系统类加载器自身是URLClassLoader类型的加载器,其中的addURL方法可以添加新的查找路径。只不过addURL方法是protected,这里通过反射API来进行调用。publicclassClasspathUpdater{publicstaticvoidaddPath(Pathpath){URLClassLoaderclassLoader(URLClassLoader)ClassLoader。getSystemClassLoader();try{MethodmethodURLClassLoader。class。getDeclaredMethod(addURL,URL。class);method。setAccessible(true);method。invoke(classLoader,path。toUri()。toURL());}catch(Exceptione){thrownewRuntimeException(e);}}} 上面介绍的ClasspathUpdater类中的使用技巧,只对Java8生效。在Java9引入模块系统时,对系统类加载器进行了修改。系统类加载器被替换成了应用类加载器。应用类加载器不再是URLClassLoader类型了,就不能使用这个技巧了。执行 最后一步就是执行动态加载的Java类。这一步比较简单,只需要用Class。forName方法来查找Java类,再找到对应的Method对象,直接调用即可。下面的代码给出了示例。publicclassInvoker{publicstaticObjectinvoke(StringclassName){try{MethodmethodClass。forName(className)。getDeclaredMethod(calculate);returnmethod。invoke(null);}catch(Exceptione){thrownewRuntimeException(e);}}}完整的执行过程 最后把整个流程串起来。在下面的代码中,需要求值的表达式是(11)35。0。首先调用DynamicCompilation。compile方法进行动态编译,得到class文件的路径和完整的类名。class文件的路径通过ClasspathUpdater。addPath方法添加到classpath中。完整的类名则传递给Invoker。invoke方法来执行。最后输出的结果是表达式的值。publicclassMain{publicstaticvoidmain(String〔〕args)throwsIOException{PairPath,StringresultDynamicCompilation。compile((11)35。0);ClasspathUpdater。addPath(result。getLeft());System。out。println(Invoker。invoke(result。getRight()));}}