0%

Java 动态性

“不要慌张、不要停止思考、不要放弃生存。”——罗伊·马斯坦

简介

动态语言可以在程序运行时改变程序的结构和变量类型,典型的语言包括 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
2
3
4
5
6
String path = "com.group.className";

Class clazz1 = Class.forName(path);
Class clazz2 = path.getClass();

Class clazz3 = String.class;

使用反射机制获取类信息

反射机制提供了一些用于获取类信息的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String path = "com.group.className";

try {
Class<ClassName> clazz = (Class<ClassName>) Class.forName(path);

// 通过反射 API 调用构造方法
ClassName classOne = clazz.newInstance(); // 调用无参构造器
Constructor<ClassName> c = clazz.getDeclaredConstructor(String.class); // 调用有参构造器
ClassName classTwo = c.newInstance("This is a test of reflection");

// 通过反射 API 调用普通方法
Method method = clazz.getDeclaredMethod("methodName", String.class);
method.invoke(classOne, "This is a test of reflecting method"); // 相当于在 classTwo 对象中调用了 methodName(String) 方法,并且传入了参数

// 通过反射 API 操作属性
Field f = clazz.getDeclaredFiled("filedName");
f.setAccessible(true); // 如果是私有属性,先修改访问权限才能进行接下来的操作。似有方法同理
f.set(classOne, "This is a test of reflecting field"); // 设置 classOne 对象的 filedName 属性设置值
f.get(classOne); // 读取 classOne 的 filedName 属性

} catch (Exception e) {
e.printStackTrace();
}

反射机制被大量应用于各种开源框架中。很多框架都使用无参构造器构造对象,因此在 Java Bean 中总是应该写上无参构造器

使用反射机制操作泛型

Java 采用泛型擦出机制来引入泛型。只有编译器 javac会使用泛型,以确保数据的安全性和免去强制类型转换的麻烦。一旦编译完成,和泛型有关的类型信息会被全部擦出(这样的条件下反射机制是无法读取泛型信息的)。

为迎合反射操作泛型的需要,Java 新增了 ParameterizedType、GenericArrayType、TypeVariable 和 WildcardType 类型来代表,不能归一到 Class 类,但是又与原始类型齐名的类型。

  • ParameterizedType:一种参数化类型,例如Collection<String>
  • GenericArrayType:一种元素类型是参数化类型或类型变量的数组类型
  • TypeVariable:各种类型变量的公共父接口
  • WildcardType:一种通配符类型表达式,例如?? extends Number? super Integer

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Demo {
public void test1(Map<String, User> map, List<User> list) {
System.out.println("Demo.test1"());
}

public Map<Integer, User> test2() {
System.out.println("Demo.test2()");
return null;
}

public static void main(String[] args) {
try {
// 获得指定方法参数泛型信息
Method m = Demo.class.getMethod("test1", Map.class, List.class);
Type[] t = m.getGenericParameterTypes();

for(Type ParamType : t) {
System.out.println("#" + paramType);
if(paramType instanceof ParameterizedType) {
Type[] genericTypes = ((ParameterizedType) paramType).getActualTypeArguments();
for (Type genericType : genericTypes) {
System.out.println("泛型类型:" + genericType);
}
}
}

// 获得指定方法返回值泛型信息
Method m2 = Demo.class.getMethod("test2", null);
Type returnType = m2.getGenericReturnType();
if(returnType instanceof ParameterizedType) {
Type[] genericTypes = ((ParameterizedType) returnType).getActualTypeArguments();

for(Type genericType : genericTypes) {
System.out.println("返回值泛型类型:" + genericType);
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

输出:

x
1
2
3
4
5
6
7
#java.util.Map<java.lang.String, com.group.test.bean.User>
泛型类型:class java.lang.String
泛型类型:class com.group.test.bean.User
#java.util.List<com.group.test.bean.User>
泛型类型:class com.group.test.bean.User
返回值泛型类型:class java.lang.Integer
返回值泛型类型:class com.group.test.bean.Use

使用反射机制操作注解

可以通过 getAnnotations 和 getAnnotation 获取注解信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取类的所有有效注解
Annotation[] annotations = clazz.getAnnotations();
for(Annotation a : annotations) {
System.out.println(a);
}

// 获取类的指定的注解
Table t = (Table)clazz.getAnnotation(Table.class);
System.out.println(t.value());

// 获取类的属性的注解
Filed f = clazz.getDeclaredField("name");
MyField myField = f.getAnnotation(myField.class);
System.out.println(myField.columnName());

反射机制性能

setAccessible 本质是启用和禁用访问安全检查开关,值为 true 则指示反射的对象取消 Java 语言访问检查,值为 false 则进行访问检查。并非是为 true 就能访问而为 false 就不能访问。禁用安全检查可以提高反射的运行速度。

执行访问检查大概会带来三十倍的性能降低。非常依赖性能时可以考虑使用字节码操作。在大部分开发中开发效率比运行效率更重要,因此反射得到了大规模的应用。字节码操作将会在后面的内容中进行介绍

动态编译

Java 6.0 引入了动态编译机制。在 6.0 之前实现类似动态编译效果的手段是通过 Runtime 调用 javac,启动新的进程去运行:

1
2
Runtime run = Runtime.getRuntime();
Process process = run.exec("javac -cp /path/to/file HelloWorld.java");

现在的做法是使用 JavaCompiler 实现动态编译。

动态编译源文件

1
2
3
4
5
public static int compileFile(String sourceFile) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null, sourceFile); // 参数 null 使用 System.in 和 System.out 作为信息输入输出流
return result;
}
  • 第一个参数:为 Java 编译器提供的参数的输入流
  • 第二个参数:接收 Java 编译器的输出信息的输出流
  • 第三个参数:接收 Java 编译器的错误信息的输出流
  • 第四个参数:Java 源文件,String 数组,可以输入多个文件
  • 返回值:0 表示编译成功,非 0 表示失败

动态运行已经编译的类

通过 Runtime 启动新的进程运行

1
2
3
4
5
6
7
8
9
10
Runtime run = Runtime.getRuntime();
Process process = run.exec("javac -cp /path/to/file HelloWorld.java");

// 输出文本
InputStream in = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String info = "";
while((info=reader.readLine()) != null) {
System.out.println(info);
}

通过反射运行已经编译的类

1
2
3
4
5
6
7
8
9
10
11
public static void runJavaClassByReflect(String dir, String classFile) throws Exception {
try {
URL[] urls = new URL[] {new URL("file:/" + dir)};
URLClassLoader loader = new URLClassLoader(urls);
Class c = loader.loadClass(classFile);
// 这里需要强转为 Object,因为编译器会将 String[] 编译成多个参数,和 main 函数参数数量不匹配
c.getMethod("main", String[].class).invoke(null, (Object)new String[]{});
} catch(Exception e) {
e.printStackTrace();
}
}

URL 和 URLClassLoader 将会在后面的内容中进行介绍

脚本引擎

Java 脚本 API 提供了一套接口实现与各种脚本引擎的交互。获得脚本引擎的方法是:

1
2
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript"); // 获取 javascript 脚本引擎

脚本引擎执行 JavaScript 代码

Java 脚本 API 提供了如下功能:

  • 获取脚本程序输入,通过脚本引擎执行并返回结果(最核心的接口):
    • API 提供的是接口,可以选择不同的实现
      • JavaScript 使用 Rhino
  • 通过脚本引擎在脚本和 Java 上下文间交换数据
  • 通过 Java 应用程序调用脚本函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Demo {
public static void main(String[] args) throws Exception {
// 获取脚本引擎对象
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("javascript");

// 定义变量,存储到引擎上下文中,脚本和 Java 都能获取
engine.put("msg", "Hello JavaScript!");
String str = "var user = {name: 'Link'};";
str += "println(user.name)";

// 执行脚本
engine.eval(str); // 输出 Link

engine.eval("msg = 'Hello World!';");
System.out.println(engine.get("msh")); // 输出 Hello World!

// 定义 js 函数
engine.eval("function add(a, b) {var sum = a + b; return sum;}");
// 执行 js 函数
Invocable jsInvoke = (Invocable) engine;
Object result = jsInvoke.invokeFunction("add", new Object[]{13, 20});
System.out.println(result); // 输出 33

// 导入其它 Java 包,使用其中的类
String jsCode = "importPackage(java.util); var list = Arrays.asList([\"Java\", \"JavaScript\"]);";
engine.eval(jsCode);
List<String> list = (List<String>)engine.get("list");
for(String temp : list) {
System.out.println(temp);
}

// 执行 js 文件
URL url = Demo.class.getClassLoader().getResource("a.js"); // 放在 src 目录下即可
FileReader fr = new FileReader(url.getPath());
engine.eval(fr);
fr.close(); // 实际生产中要使用 try catch finally
}
}

字节码操作

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Demo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("com.group.bean.Emp");

// 创建属性
CtField f1 = CtField.make("private int empno;", cc);
CtField f2 = CtField.make("private String ename;", cc);
cc.addField(f1);
cc.addField(f2);

// 创建方法
CtMethod m1 = CtMethod.make("public int getEmpno(){return empno;}", cc);
CtMethod m2 = CtMethod.make("public void setEmpno(int empno){this.empno = empno;}", cc);
cc.addMethod(m1);
cc.addMethod(m2);

// 添加构造器
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType, pool.get("java.lang.String")}, cc);
constructor.setBody("{this.empno = empno; this.ename = ename;}");
cc.addConstructor(constructor);

cc.writeFile("/path/to/file");
}
}

TODO:字节码操作 API 和 JVM 核心机制