java基础知识之注解、反射(二)
前言
继续上文java基础知识之注解、反射(一),上文讲了一**解和自定义注解已经java反射的基础应用。本文继续介绍一下动态编译、字节码操作类库Javassist和类加载过程。文章大多是学习尚学堂官网而来的总结,可能有点枯燥,大家感兴趣请自行官网搜索视频学习。
正文
动态编译
java提供动态编译有以下两个使用场景:
1.浏览器端编写java代码,上传服务器编译和运行的在线评测系统,比如牛客网的编程题。
2.服务器动态加载某些类文件进行编译。
动态编译的两种做法:
1.通过Runtime调用javac,启动新的进程去执行。
Runtime run = Runtime.getRuntime();
Process process = run.exec("javac -cp d:/myjava/ HelloWorld.java");
2.通过JavaCompiler动态编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//参数分别是:
//1为java编译器提供参数(InputStream)
//2得到java编译器输出信息(OutputStream)
//3接收编译器的错误信息(OutputStream)
//4可变参数 java文件路径
int result = compiler.run(null, null, null, "e:/test/HelloWorld.java");
System.out.println(result==0?"success":"fail");
代码:
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**动态编译实现方式
* @author XXX
*/
public class Demo01 {
public static void main(String[] args) throws IOException {
//1.通过Runtime调用javac,启动新的进程去执行。
Runtime runtime = Runtime.getRuntime();
Process process =
runtime.exec("java -cp e:/test HelloWorld");
InputStream in = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String info = "";
while ((info = reader.readLine())!=null){
System.out.println(info);
}
//2.通过JavaCompiler动态编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null, "e:/test/HelloWorld.java");
System.out.println(result==0?"success":"fail");
}
}
字节码类库
运行时操作字节码可以让我们实现以下功能:
1.动态生成新的类
2.改变某个类的结构
和反射相比的优势:比反射开销小,性能高;
常见的字节码操作类库有BCEL、ASM、CGLIB、Javassist
很多开源框架都使用了Javassist字节码类库,操作相对简单,性能比反射要高。
字节码操作主要是体现在动态生成和修改类上,参看代码:
import javassist.*;
public class Demo01 {
public static void main(String[] args) throws Exception {
//创建池
ClassPool pool = ClassPool.getDefault();
//获取类
CtClass c = pool.makeClass("com.bean.Emp");
//设置字段
CtField f1 = CtField.make("private int enum;", c);
//添加字段到类
c.addField(f1);
//设置方法
CtMethod m = CtMethod.make("public void printInfo(){
System.out.println(\"hello\");}",c);
c.addMethod(m);
//构造方法
CtConstructor constructor = new CtConstructor(
new CtClass[]{CtClass.intType,
pool.get("java.lang.String")}, c);
c.addConstructor(constructor);
//将动态创建的类写出为.class文件
c.writeFile();
System.out.println("生成成功");
}
}
类加载过程
类加载过程非常有意思,特别是自定义类加载器,还可以实现加密class文件。
类加载机制
JVM把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成
JVM可以直接使用的Java类型的过程。
主要就是概括为:加载 -> 链接 -> 使用:
1.加载 这个过程需要类加载器参与
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,
在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。
2.链接 将Java类的二进制代码合并到JVM的运行状态之中的过程。
验证:
确保加载的类信息符合JVM规范,没有安全方面的问题。
准备;
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,
注意是设置变量的值为初始值,
这些内存都将在方法区(特殊的堆)中进行分配。
解析:
虚拟机常量池内的符号引用替换为直接引用的过程。
3.初始化
- 初始化阶段是执行类构造器<clinit>()方法的过程,
它是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句,合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先出发其父类的初始化。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
- 当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化。
类的主动引用(一定会发生类的初始化):
– new一个类的对象
– 调用类的静态成员(除了final常量)和静态方法
– 使用java.lang.reflect包的方法对类进行反射调用
– 当虚拟机启动,java Hello,则一定会初始化Hello类。说白了就是先启动main方法所在的类
– 当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类
类的被动引用(不会发生类的初始化) :
– 当访问一个静态域时,只有真正声明这个域的类才会被初始化
• 通过子类引用父类的静态变量,不会导致子类初始化
– 通过数组定义类引用,不会触发此类的初始化
– 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
不进行初始化也就是说不会执行静态块内容,更不会执行构造函数了,不涉及上面说的类加载过程的第三步初始化。
类加载器的作用
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法
区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class
对象,作为方法区类数据的访问入口。
java.class.ClassLoader类
它是加载器的基础,除了引导类加载器,其它的类加载器几乎都是继承java.class.ClassLoader类。java.class.ClassLoader类的作用:
– java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,
找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个
Java 类,即 java.lang.Class类的一个实例。
– 除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文
件和配置文件等。
整体来看,加载器的层次结构为树状结构,分为下面四个类别:
1.引导类加载器(bootstrap class loader)
– 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,或sun.boot.class.path路径下的
内容),是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
– 加载扩展类和应用程序类加载器。并指定他们的父类加载器。
2.扩展类加载器(extensions class loader)
– 用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容)。
Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。
– 由sun.misc.Launcher$ExtClassLoader实现
3.应用程序类加载器(application class loader)
– 它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类
一般来说,Java 应用的类都是由它来完成加载的。
– 由sun.misc.Launcher$AppClassLoader实现
4. 自定义类加载器
– 开发人员可以通过继承 java.lang.ClassLoader类的方式
实现自己的类加载器,以满足一些特殊的需求。
如图
类加载器存在代理模式,代理模式指定了在类加载的时候使用哪一个加载器来加载我们的类:
1.代理模式
– 交给其他加载器来加载指定的类
2.双亲委托机制
– 就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委
托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器
可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载
任务时,才自己去加载。
– 双亲委托机制是为了保证 Java 核心库的类型安全。
这种机制就保证不会出现用户自己能定义java.lang.Object类的情况。
– 类加载器除了用于加载类,也是安全的最基本的屏障。
注:
双亲委托机制是代理模式的一种
– 并不是所有的类加载器都采用双亲委托机制。
– tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,
如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的
下面自定义一个自己的类加载器,使用类加载器去加载指定路径下的class文件。
import java.io.*;
/**
* 自定义类加载器
* @author XXX
*/
public class FileSystemClassLoader extends ClassLoader{
//加载路径
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//在已经加载的类中查找
Class<?> c = findLoadedClass(name);
//如果找到了直接返回,否则先让父加载器查找,父加载器没找到再由自己加载。
if (c!=null){
return c;
}else {
//调用父类的加载器AppClassLoader
try {
c = this.getParent().loadClass(name);
} catch (ClassNotFoundException e) {
// e.printStackTrace();
}
if (c!=null){
return c;
}else {
//自己加载,首先读取该class文件并转换为字节数组,
// 然后调用defineClass转换为需要的java类
byte[] classData = getClassData(name);
c = defineClass(name, classData, 0, classData.length);
}
}
return c;
}
private byte[] getClassData(String name) {
//得到实际路径
String path = rootDir + "/" + name.replaceAll("\\.", "/") + ".class";
InputStream is = null;
ByteArrayOutputStream bs = new ByteArrayOutputStream();
//读取文件转换为字节数组
try {
is = new FileInputStream(path);
byte[] buffer = new byte[1024];
int length = -1;
while ((length=is.read(buffer)) != -1){
bs.write(buffer,0, length);
}
return bs.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally {
if (is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bs != null){
try {
bs.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
代码中每个过程都给了注释,应该不难看懂,下面使用自定义加载器加载类:
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
FileSystemClassLoader classLoader =
new FileSystemClassLoader("E:/testjava");
Class c = classLoader.loadClass("annotation.classloader.HelloWorld");
System.out.println(c);
System.out.println(c.hashCode());
}
}
不难得出,对应加密的自定义类加载器的实现就是先将class文件使用算法进行加密生成新的class文件,然后在使用自定义类加载器进行读取为字节数组时使用加密算法对应的解密算法进行解密,这样就基本实现了自定义加解密的类加载器。
结束
最后,再次感谢尚学堂的视频。