JAVA是怎么从静态语言兼容动态语言编程的

类加载机制

基础知识

每个Java程序至少拥有三个类加载器:

  1. 引导类加载器
  2. 平台类加载器
  3. 系统类加载器(应用加载器)

引导类加载器负责加载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 抽象。
URIClassLoad继承结构

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)。
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;
   }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值