类加载机制
基础知识
每个Java程序至少拥有三个类加载器:
- 引导类加载器
- 平台类加载器
- 系统类加载器(应用加载器)
引导类加载器负责加载jdk内部模块中的平台类,没有对应得ClassLoader对象。
java9之前,java平台类位于rt.jar。
java9之后,java平台模块化,每个平台模块都包含一个JMOD文件。平台类加载器会加载引导类加载器没有加载得Java平台所有的类。
系统类加载器会从模块路径和类路径中加载应用类。
除了引导类加载器外,每个类加载器都有一个父类加载器。类加载,优先使用父类,只有父类加载器加载失败,子类才会加载。
类加载器抽象(ClassLoader)
本文章jdk为adopt-openj9-11.0.10
java.lang.ClassLoader 为一个抽象类并没有实现任何借口,其中最重要的一个实现类是java.net.URLClassLoader,从java.security.SecureClassLoader(安全机制不在这讨论)中继承ClassLoader 抽象。
javadoc描述第一句:
This class loader is used to load classes and resources from a search path of URLs referring to both JAR files and directories.
最重要的加载的动作是由java.net.URLClassLoader#defineClass完成的,647行:
private Class<?> defineClass(String name, Resource res) throws IOException { }
这个函数入参一个字符串name,一个资源,出参一个Class对象。defineClass 还有重载方法,更细的重载方法中Resource变成了byte[]。
基本上可以得出这样的结论,通过ClassLoader#defineClass可以把字节数组转化为class代码。而字节数组的来源,理论上是编译器编译出来的.class文件。
类编译器
Java提供在代码中调用java编译器能力。
类编译器抽象(JavaCompiler)
JavaCompiler 是一个接口,实现类就是一个java版本的javac(com.sun.tools.javac.api.JavacTool)。
编译时可以简单的调用run方法,或者是使用Task的方式。其中会涉及到大致有以下的抽象:
- javax.tools.StandardJavaFileManager
- javax.tools.JavaFileObject
- javax.tools.DiagnosticCollector
- javax.tools.JavaCompiler.CompilationTask
- javax.tools.Diagnostic
拥有编译器api,让java边执行,边编译成为了可能。再通过类加载器让边执行,边编译成为了可能。由此引出了APT,AOP,ASM等编程模型。
APT
APT关注的是编译时注解,理论上APT还是保持了JAVA静态语言的特性。APT是编译器提供一种特性:
编译器会定位源文件中的注解。每个注解处理器会依次执行,并得到它表示感兴趣的注解。如果某个注解处理器创建了一个新的源文件,那么上述过程将重复执行。如果某次处理循环没有再产生任何新的源文件,那么就编译所有的源文件。
通常通过扩展AbstractProcessor类实现Processor接口实现APT。
APT在编译时有一些特性:
- 只能产生新的源文件,无法修改已有的源文件。
- 在每一轮中,process 方法都会被调用一次,调用时会传递给由这一轮在所有文件中发现的所有注解构成的集。process方法在后续轮次中之前处理过的文件,注解列表是可空的,防止多次创建源文件,陷入死循环。
- 注解处理器可以写入任何文档,不限于XML,属性文件,Shell脚本,HTML文档等。
要想查看轮次,可以用 -XprintRounds 标记javac命令。
具体示例可以参考本人之前的文章: 安卓apt开发kotlin 利用编译时注解生成源码Demo
移动端对性能比较看重,一般会选择APT方式。
AOP
AOP是一种编程思想,也可以说成是编程模型。Java AOP 一般是指不改变原有二进制实现的基础上去扩展功能(面向对象原则)。这种手法,最具有静态转动态的特性。无论是jdk动态代理,AspectJ,cglib等都是通过字节码操作来完成aop的。通过代理原来的对象,在原有的方法前(before),后(after),包裹(around)进行功能扩展。
JDK动态代理
java.lang.reflect.Proxy#newProxyInstance(java.lang.ClassLoader, java.lang.Class<?>[], java.lang.reflect.InvocationHandler)中
第一个入参ClassLoader加载器,第二个入参代理接口(里氏替换),第三个参数可以看成是around。代理对象需要自己注入,判断切面逻辑都不明显。所以,jdk动态代理刚接触是比较难理解的。
cglib字节码提升,在Spring框架下利用关键类org.springframework.cglib.proxy.Enhancer,实现起来非常简单,一般用于具体类而不是接口的扩展,通过setSuperclass设置被提升对象Class,通过setInterfaces指定拦截接口,通过setCallback传入MethodInterceptor进行具体动作。
AspectJ 原生方式是通过自身编译器进行提升,又是AspectJ语法,又是编译器。在现在,个人感觉复杂度成本高于收益了。Spring框架放弃了编译器,桥接了AspectJ 动态方面的实现。
Spring 框架在AOP上做的非常优秀,主要关注的是org.springframework:spring-aop模块,主要抽象有
- org.springframework.aop.framework.ProxyFactory
- org.aopalliance.aop.Advice
- org.aopalliance.intercept.MethodInterceptor
- org.springframework.aop.Pointcut
以上只是个人推荐的关注点,当然还有其它很多,比如:适配器怎么适配advice和Interceptor,advice和Advisor关系,AspectJ 怎么桥接等等。
扩展
可以自定义编译器和加载器,实现class文件加密,防止反编译。不过,需要衡量这么做的收益。
把字符串变成class
javax.tools.SimpleJavaFileObject 实现了JavaFileObject,继承SimpleJavaFileObject可以很方便地实现自定义java文件对象的扩展。比如,在代码中动态组装了String对象的java代码,可以提供给一个自定义对象交给编译器编译出class文件。
//此片段代码来自《java核心技术卷二第11版》
public class StringSource extends SimpleJavaFileObject{
private String code;
StringSource(String name, String code){
super(URI.create("string:///" + name.replace('.', '/') + ".java"), Kind.SOURCE);
this.code = code;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors){
return code;
}
}
List<StringSource> sources = List.of(new StringSources(ClassName,ClassCode));
task = compiler.getTask(null,fileManager,collector,null,null,sources);
在内存中持有字节码
自定义一个JavaFileObject实现,在类中组合ByteArrayOutputStream持有文件读入时的字节流。
//此片段代码来自《java核心技术卷二第11版》
public class ByteArrayClass extends SimpleJavaFileObject{
private ByteArrayOutputStream out;
ByteArrayClass(String name){
super(URI.create("bytes:///" + name.replace('.', '/') + ".class"),
Kind.CLASS);
}
public byte[] getCode(){
return out.toByteArray();
}
@Override
public OutputStream openOutputStream() throws IOException{
out = new ByteArrayOutputStream();
return out;
}
}