“不要慌张、不要停止思考、不要放弃生存。”——罗伊·马斯坦
简介
动态语言可以在程序运行时改变程序的结构和变量类型,典型的语言包括 Python、Ruby、Javascript 等。Java 是一种非动态语言,但其也包含一定的动态特性。Java 动态特性体现在注解和反射机制、动态编译、脚本引擎、动态字节码操作等方面。
注解
注解(Annotation)是 JDK5.0 引入的技术。注解作为对程序的解释,不属于程序本身,但是可以被其它程序读取并处理。注解的格式为 @AnnotationName,例如@SuppressWarnings(value="unchecked")
。注解可以被附加在 package、class、method、field 等元素之上,为它们添加额外的辅助信息。通过反射机制可以实现对这些元数据的访问。
部分内置注解
注解 | 作用 |
---|---|
@Override | 修饰方法,表示一个方法重写父类方法 |
@Deprecated | 修饰方法、属性、类,表示不鼓励使用 |
@SuppressWarnings | 抑制编译时的警告信息 |
@SuppressWarnings 参数
参数 | 抑制的警告类型 |
---|---|
deprecation | 过时的类或方法 |
unchecked | 未检查的转换 |
fallthrough | switch 语句中的 case 穿透 |
path | 不存在的路径 |
serial | 可序列化类缺少 serialVersionUID 定义 |
finally | finally 子句不能完成 |
all | 以上所有警告 |
使用参数的例子
@SupressWarnings("unchecked")
@SupressWarnings(value={"unchecked, "deprecation"})
自定义注解
自定义注解的格式为public @interface AnnotationName {definition}
。使用 @interface 会自动继承 java.lang.annotation.Annotation 接口。定义体中的每一个方法实际上声明了一个配置参数,其中方法的名称是参数的名称,返回值类型是参数的类型(只能是基本类型、Class、String 和 enum)。可以在方法后使用 default 声明参数的默认值。如果只有一个参数成员,一般命名为 value。
注解元素必须有值。定义时经常使用空字符串或0 作为默认值,或者使用负数表示不存在的含义
元注解
元注解(meta-annotation)被用于注解其它注解。Java 定义了四种元注解:@Target、@Retention、@Documented、@Inherited。
@Target
@Target 描述注解的使用范围,其参数取值为名为 ElementType 的穷举类型:
取值 | 修饰范围 |
---|---|
PACKAGE | package |
TYPE | 类、接口、枚举、Annotation |
CONSTRUCTOR | 构造器 |
FIELD | 域 |
METHOD | 方法 |
LOCAL_VARIABLE | 局部变量 |
PARAMETER | 参数 |
@Retention
@Retention 描述注解的生命周期,参数名称为 RetentionPolicy:
取值 | 作用 |
---|---|
SOURCE | 源文件中有效,在源文件中保留 |
CLASS | 在 class 文件中有效,class 文件中保留 |
RUNTIME | 运行时有效,可以被反射机制读取 |
反射机制
反射机制指的是程序可以在运行时加载、探知和使用在编译期间完全未知的类。程序在运行状态中可以动态加载一个只有名称的类。对于任意一个已加载的类,可以探知其所有属性和方法;对于任意一个对象,可以调用其任意方法和属性。加载类的方法是Class c = Class.forName("className");
。
完成类加载后,程序会在堆内存中放入一个 Class 类型对象(一个类对应一个对象),该对象包含了完整的类的结构信息。这个对象就像一面镜子,映照出类的结构信息。
Class 类
区别于 class(小写),java.lang.Class 是一种特殊的类,用来表示 Java 中的类型本身(class、interface、enum、annotation、primitive type、void等)。
- Class 类等对象包含某个被加载的类的结构。一个被加载的类只对应一个 Class 对象
- 具有相同类型和维度(不是长度)的类对应相同的 Class 对象(例如长度为 10 和 30 的数组,Class 是相同的)
- 当类被加载,或加载器(class loader)的
defineClass()
方法被 JVM 调用,JVM 便自动产生一个 Class 对象 - 想要动态加载或运行某个类,必须先获得相应的 Class 对象
获取 Class 类对象有三种方式:
getClass()
Class.forName()
.class
例如:
1 | String path = "com.group.className"; |
使用反射机制获取类信息
反射机制提供了一些用于获取类信息的 API(clazz 是 Class 对象),以下是其中的一部分:
方法 | 作用 |
---|---|
clazz.getname() | 获取包名 + 类名 |
clazz.getSimpleName() | 获取类名 |
clazz.getField(name) | 获取 name 对应的属性 |
clazz.getFields() | 以列表形式返回全部的公有属性 |
clazz.getDeclaredFields() | 获取全部属性 |
clazz.getMethod(name, Class<?>… parameterType) | 获取 name 对应的方法,后一个参数是该方法参数类型的列表(用于区分重载) |
clazz.getMethods() | 获取全部公有方法 |
clazz.getDeclaredMethods() | 获取全部方法(不包括继承) |
clazz.getDeclaredMethods(name, Class<?>… parameterType) | 获取 name 对应的方法,后一个参数是该方法参数类型(用于区分重载) |
clazz.getConstructors() | 获取全部公有构造器 |
clazz.getDeclaredConstructors() | 获取全部构造器 |
clazz.getDeclaredConstructors(Class<?>… parameterTypes) | 获取与参数类型相匹配的构造器 |
使用反射机制动态操作构造器、方法和属性
1 | String path = "com.group.className"; |
反射机制被大量应用于各种开源框架中。很多框架都使用无参构造器构造对象,因此在 Java Bean 中总是应该写上无参构造器
使用反射机制操作泛型
Java 采用泛型擦出机制来引入泛型。只有编译器 javac会使用泛型,以确保数据的安全性和免去强制类型转换的麻烦。一旦编译完成,和泛型有关的类型信息会被全部擦出(这样的条件下反射机制是无法读取泛型信息的)。
为迎合反射操作泛型的需要,Java 新增了 ParameterizedType、GenericArrayType、TypeVariable 和 WildcardType 类型来代表,不能归一到 Class 类,但是又与原始类型齐名的类型。
- ParameterizedType:一种参数化类型,例如
Collection<String>
- GenericArrayType:一种元素类型是参数化类型或类型变量的数组类型
- TypeVariable:各种类型变量的公共父接口
- WildcardType:一种通配符类型表达式,例如
?
、? extends Number
、? super Integer
例子
1 | public class Demo { |
输出:
1 | #java.util.Map<java.lang.String, com.group.test.bean.User> |
使用反射机制操作注解
可以通过 getAnnotations 和 getAnnotation 获取注解信息。
1 | // 获取类的所有有效注解 |
反射机制性能
setAccessible 本质是启用和禁用访问安全检查开关,值为 true 则指示反射的对象取消 Java 语言访问检查,值为 false 则进行访问检查。并非是为 true 就能访问而为 false 就不能访问。禁用安全检查可以提高反射的运行速度。
执行访问检查大概会带来三十倍的性能降低。非常依赖性能时可以考虑使用字节码操作。在大部分开发中开发效率比运行效率更重要,因此反射得到了大规模的应用。字节码操作将会在后面的内容中进行介绍
动态编译
Java 6.0 引入了动态编译机制。在 6.0 之前实现类似动态编译效果的手段是通过 Runtime 调用 javac,启动新的进程去运行:
1 | Runtime run = Runtime.getRuntime(); |
现在的做法是使用 JavaCompiler 实现动态编译。
动态编译源文件
1 | public static int compileFile(String sourceFile) { |
- 第一个参数:为 Java 编译器提供的参数的输入流
- 第二个参数:接收 Java 编译器的输出信息的输出流
- 第三个参数:接收 Java 编译器的错误信息的输出流
- 第四个参数:Java 源文件,String 数组,可以输入多个文件
- 返回值:0 表示编译成功,非 0 表示失败
动态运行已经编译的类
通过 Runtime 启动新的进程运行
1 | Runtime run = Runtime.getRuntime(); |
通过反射运行已经编译的类
1 | public static void runJavaClassByReflect(String dir, String classFile) throws Exception { |
URL 和 URLClassLoader 将会在后面的内容中进行介绍
脚本引擎
Java 脚本 API 提供了一套接口实现与各种脚本引擎的交互。获得脚本引擎的方法是:
1 | ScriptEngineManager sem = new ScriptEngineManager(); |
脚本引擎执行 JavaScript 代码
Java 脚本 API 提供了如下功能:
- 获取脚本程序输入,通过脚本引擎执行并返回结果(最核心的接口):
- API 提供的是接口,可以选择不同的实现
- JavaScript 使用 Rhino
- API 提供的是接口,可以选择不同的实现
- 通过脚本引擎在脚本和 Java 上下文间交换数据
- 通过 Java 应用程序调用脚本函数
1 | public class Demo { |
字节码操作
Java 动态性的实现有反射和字节码操作两种常用方式。字节码操作可以实现动态生成新类和动态改变类结构的功能(添加、删除、修改新的属性、方法等)。字节码的优势在于其比反射开销小且性能更高。
Java 程序首先由编译器编译为 class 文件,运行时读入虚拟机中。字节码操作便是创建或修改虚拟机中的 class 文件实现动态的类操作。
常见字节码操作类库包括 BCEL、ASM、CGLIB、Javassist。BCEL 和 ASM 基于 JVM 底层操作和指令实现,性能好,但是学习难度大。后两者性能稍弱,但是更容易使用和学习,它们常见于各种开源框架中。本文将会介绍 Javassist,它同时提供源码和底层级别的 API 供不同级别的开发者使用。
Javassist 也经常应用于面向切面编程(Aspect Oriented Programming)。面向切面编程指在程序生命周期的各个阶段(编译、运行等)动态地将代码切入指定代码的指定位置,从而实现业务的解耦
Javassist 简单使用
Javassist 最外层 API 和 Java 反射包中的 API 类似,主要由 CtClass、CtMethod 和 CtField 组成,分别对应 java.lang.Class、java.lang.reflect.Method 和 java.lang.reflect.Method.Field。
使用 Javassist 生成新类
1 | public class Demo { |
TODO:字节码操作 API 和 JVM 核心机制