文章目录
- 1. 作为程序员的最高追求
- 2.如何实现动态编译
- 2.1 生成源码
- 2.2 调用编译器API对Test源码文件进行编译生成字节码
- 2.3 调用类加载器对字节码进行加载得到Class对象
- 2.4 使用Class对象创建对象进行使用
- 3. Java编译API学习
- 4. 类加载机制
- 4.1 类加载过程
- 4.2 类加载器的层次结构
- 4.2.1 使用URLClassLoader加载jar包中的类
- 4.2.2 干涉类加载过程
- 4.2.3 自定义类加载器
- 5.项目代码地址
1. 作为程序员的最高追求
当我们习惯了编写重复的业务代码,是否有时候会感觉到无聊至极!!
有时候,作为程序员,是否脑子中会不时的闪现出一个想法,如果我能写一个程序让系统能自动的写代码,
然后再自动的装载到系统中实时编译运行就好了。
实际上,这并不是不能实现,在JDK8中提供了编译相关的API供我们使用,
通过JDK8,我们可以实现程序自动生成源代码 ,然后自动进行编译加载,在不停掉系统的前提下新增类并使用它!!
这就是动态编译。
2.如何实现动态编译
简单来说分为四步:
生成源码–>编译源码生成字节码文件–>加载字节码得到Class对象–>使用Class对象创建对象并使用
2.1 生成源码
这一步就是根据业务的不同,我们可以灵活处理,是我们自由发挥的关键。
我们先写一个例子,加入我们打算在系统编译这个源文件并加载它,我们应该怎么做?
Test.java
package Progress.exa37.complier;
public class Test {
public static void main(String[] args) {
System.out.println("这是要用Java编译器Api进行编译的java源文件");
}
public void printInfo(){
System.out.println("成功加载并生成对象,执行printInfo完成");
}
}
2.2 调用编译器API对Test源码文件进行编译生成字节码
这一步我们需要详细学习,这是实现源码到字节码的关键,我们先按简单的来,怎么简单怎么来,
我们可以用JavaCompiler这个类对指定源文件进行编译:
public class Test{
public static void main(String[] args) throws FileNotFoundException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
OutputStream outputStream = new FileOutputStream("output.txt");
OutputStream errStream = new FileOutputStream("error.txt");
//注意这里的路径就是源文件的路径
int result = compiler.run(null,outputStream,errStream,
"../study/Java基础学习/src/main/java/Progress/exa38/complier/Test.java");
if(result==0){
System.out.println("编译成功!!!");
}
}
}
当运行完成后,会打印出下面的内容,并在程序同级目录下生成一个class文件:
到这里,我们已经完成了源码的编译。
2.3 调用类加载器对字节码进行加载得到Class对象
这一步非常关键,这一步的成功与否标志着我们能否顺利的创建对象。
一般用类加载器对字节码进行加载,
我们一般都选择自己写一个自定义加载器, 实现findClass方法用来得到此类的Class对象.
package Progress.exa37.loader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyClassLoader extends ClassLoader{
/**
* 根据路径和类全名对字节码文件进行读取并加载
* @param pathName 字节码文件路径
* @param className 类包名
* @return 返回这个类的Class对象
*/
protected Class<?> findClass(String pathName,String className) {
// 声明字节码数组
byte[] cLassBytes = null;
Path path = null;
try {
path = Paths.get(new URI(pathName));
// 读取字节码文件的字节码
cLassBytes = Files.readAllBytes(path);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
// 根据类的包名,字节码数组构建class对象
Class<?> clazz = defineClass(className, cLassBytes, 0, cLassBytes.length);
return clazz;
}
}
通过上面的方法我们可以获取到Class对象:
package Progress.exa37.loader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
/**
* 使用自定义的类加载器对class文件加载并创建实例使用之
*/
public class LoaderStudy {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
MyClassLoader loader = new MyClassLoader();
Class<?> aClass = loader.findClass("file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class","Progress.exa37.complier.Test");
}
}
2.4 使用Class对象创建对象进行使用
通过上面的一步,我们获取到Test.class字节码对应的Class对象后,我们就可以使用Class来创建实例对象了:
package Progress.exa37.loader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
/**
* 使用自定义的类加载器对class文件加载并创建实例使用之
*/
public class LoaderStudy {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, MalformedURLException {
MyClassLoader loader = new MyClassLoader();
Class<?> aClass = loader.findClass("file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class","Progress.exa37.complier.Test");
try {
Object obj = aClass.newInstance();
Method method = aClass.getMethod("printInfo");
method.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:
控制台打印: 成功加载并生成对象,执行printInfo完成
到这里,我们应该大概明白如何利用java编译器api和类加载器实现动态编译了。
那么对于类加载器,我们为何要去自定义一个呢? 难道不能使用Jdk原有的api去实现相应的功能吗?
要理解这一点,我们就得学习一些Java中的类加载机制。
3. Java编译API学习
通过上面的动手,我们可以发现在Java程序中对某个源文件进行编译可以这样:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//注意这里的路径就是源文件的路径
int result = compiler.run(null,outputStream,errStream,
"../study/Java基础学习/src/main/java/Progress/exa38/complier/Test.java");
if(result==0){
System.out.println("编译成功!!!");
}
通过JavaCompiler对象的run方法可以对指定路径的源文件进行编译为字节码文件。
4. 类加载机制
下面了解Java的类加载机制。
我们知道Java程序的执行流程是先将Java源文件编译成字节码文件(存储虚拟机代码),
然后由虚拟机去加载这些字节码文件将其转换为对应平台的机器码进行执行。
而这个虚拟机加载字节码文件的过程,我们有必要进行了解
4.1 类加载过程
虚拟机只加载程序执行时所需要的类文件。
我们假设程序从MyProgram.class开始运行,那么虚拟机的执行步骤如下:
-
虚拟机有一个用于加载类文件的机制,例如从磁盘中读取文件或者请求Web上的文件,虚拟机会使用该机制来加载MyProgram类文件中的内容。
一个类的加载流程如下:- 如果MyProgram类拥有类型为另一个类的域,或者超类,那么这些类文件也会被加载,这一过程被称为类的解析(加载某个类所依赖的所有类的过程被称为类的解析)
- 接着,虚拟机执行MyProgram中的静态的main方法
- 如果main方法或者main方法调用的方法要用到更多的类,那么接下来就会加载这些类.
-
然而,类加载机制并非只使用单个的类加载器,每个Java程序至少拥有三个类加载器:
-
系统类加载器(BootStrap):
系统类加载器负责加载系统类(对rt.jar中的类进行加载,为java程序运行时必须的类)。系统类时虚拟机不可分隔的一部分,通常是一些有C语言是实现的底层类。系统类加载器没有对应的ClassLoader对象,它是虚拟机的一部分。 -
扩展类加载器(ExtClassLoader):
扩展类加载器用于从jre/lib/ext目录加载 ”标准的扩展“。 我们可以将Jar文件放到该目录下,这样即使没有任何类路径,扩展类加载器也能找到其中的各个类。 -
应用类加载器(AppClassLoader):
应用类加载器用于加载应用类。 它由CLASSPATH环境变量或者-classpath命令行选项设置的类路径中的目录里或者jar/zip文件里找到这些类。
-
注意:在Java中,扩展类加载器和系统类加载器都是用Java实现的,他们都是URLClassLoader类的实例。
4.2 类加载器的层次结构
类加载器有一种父/子关系。除了系统类加载器外,其他的每个类加载器都有一个父类加载器。
根据虚拟机规定,类加载器会为它的父类加载器提供一个机会,以便加载任何给定的类,并且只有在其父类
加载器加载失败时,它才会加载给定的类。
例如:当要求应用类加载器加载一个类(例如java.util.ArrayList)时,应用类加载器会先去请求扩展类加载器
对ArrayList进行加载,然后扩展类会再去请求系统类加载器进行加载,系统类加载器会对其进行加载,如果加载
失败,则扩展类加载器会对其进行加载,如果扩展类加载器加载失败,则应用类加载器会对其进行加载并返回。
(有点类似责任链模式)
4.2.1 使用URLClassLoader加载jar包中的类
某些程序具有插件架构,其中代码的某些部分是作为可选的插件打包的。
如果插件被打包为JAR文件,那就可以直接用URLClassLoader类的实例去加载这些类。
URL url = new URL("file:///path/to/plugin.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class<?> cl = loader.loadClass("mypackage.myClass");
Object obj = cl.newInstance();
由于在URLClassLoader构造器中没有指定父类加载器,所以loader的父类加载器就是应用类加载器。
在Java中,所有的类加载器都应该继承ClassLoader抽象类。
大多数的时候,我们不需要去干预类加载的层次结构,通常,类是由于其他的类需要它而被加载的,这个过程对我们是透明的。
4.2.2 干涉类加载过程
偶尔,有时需要干涉指定类的加载过程。
思考下面例子:
应用的代码包含一个help方法,它需要调用Class.forName(classNameString), 而这个方法是从一个插件类中被调用的,
更巧的是,classNameString指定的正是一个包含在这个插件的Jar包的类。
插件的作者很合理的期望这个类应该被加载,但是,help方法是由应用类加载器加载的,而classNameString对于应用类
加载器是不可视的,这个类无法被正常的加载!!
要解决这个问题,help方法在调用Class.forName(classNameString)之前需要用恰当的类加载器先将这个类加载。
解决方案:
每个线程都有一个类加载器的引用,这个引用被称之为上下文类加载器。
主线程的上下文类加载器是应用类加载器。 当新线程创建时,它的上下文类加载器会被设置为创建该线程的上下文类加载器。
因此,如果不做额外的操作,那么所有的线程就都会将自己的上下文类加载器设置为应用类加载器!!
所以我们可以这样做:
Thread t = Thread.currentTherad();
t.setContextClassLoader(selfloader);
然后help这个方法就能用自定义的类加载器进行类加载了:
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class cl = loader.loadClass(className);
那么问题来了,我们该如何去编写自定义的类加载器呢?
4.2.3 自定义类加载器
我们可以编写自己的用于特殊目的的类加载器,这使得我们可以在向虚拟机传递字节码之前执行定制的检查。
例如我们可以编写一个类加载器,它可以拒绝加载没有标记为 piadfor的类。
如果要编写自己的类加载器,只需要继承ClassLoader类,然后覆盖这个类的findClass方法:
ClassLoader的loadClass方法用于将类的加载操作委托给其父类加载器进行,
只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果类加载器存在父类,先让父类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果所有的父类都加载失败,调用rt加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父类加载器和rt加载器都加载失败,则直接调用自己的类加载器加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 这里是最后的防线
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass方法的实现前提是:
-
为来自本地文件系统或者其他源的类加载其字节码
-
调用ClassLoader的defineClass方法向虚拟机提供字节码
这样我们就能使用自定义加载器顺利加载自定义类,
示例:
public class MyClassLoader extends ClassLoader {
/**
* 根据路径和类全名对字节码文件进行读取并加载
* @param pathName 字节码文件路径 file:///E:/Personal/MyRepository/study/Java基础学习/src/main/java/Progress/exa37/complier/Test.class
* @return 返回这个类的Class对象
*/
@Override
protected Class<?> findClass(String pathName) throws ClassFormatError {
// 声明字节码数组
byte[] cLassBytes = null;
Path path = null;
try {
path = Paths.get(new URI(pathName));
// 读取字节码文件的字节码
cLassBytes = Files.readAllBytes(path);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}
String className = pathName.substring(pathName.indexOf("java")+5,pathName.indexOf(".class")).replace("/",".");
// 根据类的包名,字节码数组构建class对象
Class<?> clazz = defineClass(className, cLassBytes, 0, cLassBytes.length);
return clazz;
}
}
5.项目代码地址
https://gitee.com/yan-jiadou/study/tree/master/Java%E5%9F%BA%E7%A1%80%E5%AD%A6%E4%B9%A0/src/main/java/Progress/exa37