Java 反射与注解
反射与注解
Java 从源码到执行一般需要三个过程:
- 编译并生成字节码文件,即 class 文件或者 jar 包
- JVM 加载字节码文件并初始化运行环境,例如将字节码翻译成机器指令、初始化对象、加载依赖包等
- 执行 Java 程序
和 C/C++
这类系统级编程语言相比,Java 多了生成字节码文件与翻译字节码文件这些中间步骤,这是 Java 实现“一次编译处处执行”的基础,也是反射和注解的底层基础。相同的字节码在不同的平台下会被 JVM 翻译成不同的机器指令,从而实现跨平台执行。
Java 提供了一种机制,允许我们在载入(创建)类对象时读取与修改对象中的属性,这种机制基于 JVM。程序员可以通过 Java 内置的一些方法使用 JVM 的这部分特性。这是 Java 反射和注解的原理。
反射与类中的 Class 对象
维基百科对计算机科学中的反射解释如下:
In computer science, reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
在计算机科学中,反射是运行时查看与修改自身结构和行为的能力。
举个例子,Java 中运行时可以通过反射修改属性和方法的访问限制(例如从 private 修改为 public )。
stackoverflow 上点赞较多的回答如下:
The ability to inspect the code in the system and see object types is not reflection, but rather Type Introspection. Reflection is then the ability to make modifications at runtime by making use of introspection. The distinction is necessary here as some languages support introspection, but do not support reflection. One such example is C++
探视代码和对象类型不是反射。在运行时通过类型检查来做一些修改才是反射。C++ 可以查看对象的类型(例如使用 typeid)但不能在运行时对对象做修改,故C++不支持反射。(非直译)
上面两个解释中都强调了反射运行时修改的特点。
Java 是面向对象的语言,除了内置的 POD(Plain Old Data)类型,其他所有数据类型都是对象,而且这些对象中有着很多相同的方法,例如:equal、toString 等等。每一个 Java 类中都有一个 Class 对象 class
(类似于静态成员变量),Class 对象保存了类本身的信息,例如类有多少属性,这些属性的类型是什么;还有就是类有哪些方法,这些方法的参数又是什么等等。Class 对象是 Java 反射的基础,只要提供一个类的 Class 对象我们就可以不用 new 而是使用 Java 提供的方法构造一个对应的对象。假设我们已经有了一个 Dog 类,那么我们就可以使用下面的方式在运行时构造一个 Dog 对象:
Class pClass = Class.forName(Dog.class); // 获得 Dog 类的 Class 对象
Constructor c = pClass.getConstructor(); // 通过 Class 对象获得 Dog 类的构造函数
Dog xiaohei = (Dog) c.newInstance(); // 构造一个 Dog 对象小黑
注解与类中的 Class 对象
注解信息会保存在类的 Class 对象中,Java 提供了读取这些信息的方法,例如 Class.getAnnotation(...)
。
综合上面的介绍可知:
- Java 可以通过 Class 对象获得一个类的详细信息
- 注解信息也保留在了 Class 对象中
- Java 提供了在生成类对象时修改对象属性方法的机制
举个简单的例子来说明反射和注解的一些功能。假设我们有一个 Dog 类,Dog 类中有 name、gender、color 等属性,这些属性在 Dog 的 Class 对象中是有记录的。现在我们有了一个 DogInit 注解,这个注解中也有若干个属性,例如 name、gender、color等。使用 Java 提供的注解语法将 DogInit 和 Dog 关联起来:
@DogInit(name="xiaohei", gender="boy", color="black")
class Dog{
public static Dog getDefaultDog()
{
DogInit dogInit = Dog.class.getAnnotation(DogInit.class); // 通过 Class 对象获取注解信息
Dog dog; // 通过反射而非构造函数的形式初始化了一个 Dog 对象
dog.name = dogInit.getName(); // 从注解中提取数据
dog.gender = dogInit.getGender();
dog.color = dogInit.getColor();
return dog;
}
...
private name;
private gender;
private color;
}
上面的代码中,我们从注解中提取了数据并构造了 Dog 对象,按照传统的方法我们一般使用构造函数。以 Hibernate 为例,在关联对象和数据库表的时候我们需要使用注解 @Table(name = "table_name")
来指明当前类关联的表。类对象和数据库表本不该有任何的耦合关系,所以不应该在构造函数中指定关联数据库表名,使用注解可以实现对象和表的解耦。测试的时候可以当做这些注解信息不存在,因为使用 new 创建对象的时候默认不解析注解信息。
Spring 中的依赖注入机制所依赖的就是 Java 的反射与注解。我们经常会在 Spring 代码中看到类被加上了 @Bean
这个注解,Spring 项目在启动时,Spring 会扫描合法的字节码文件并搜索所有类的 Class 对象,如果发现一个类的 Class 对象中包含 @Bean
注解信息,则会自动创建这个类的一个对象,其他没有 @Bean
相关注解的类不会在系统中创建对象,除非你手动 new 一个。
示例
下面的例子源自 how2j,我截取了部分,感谢原作者,侵删。要想完整理解下面的例子,最好先了解一下 Hibernate,可以参考 how2j 中介绍 Hibernate 的第一小节hello hibernate。
定义注解
package hibernate_annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义实体注解,以标识使用当前注解的对象为实体对象
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyEntity {
}
// 下面注解用于指明需要映射的数据库表
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
String name();
}
添加注解
@MyEntity // 标识当前类为实体
@MyTable(name="hero_") // 指明映射的数据库表
public class Hero {
private int id;
private String name;
private int damage;
private int armor;
...
}
解析与使用注解
Class<Hero> clazz = Hero.class;
// 尝试读取实体注解以判断当前对象是否是数据库实体对象
MyEntity myEntity = (MyEntity) clazz.getAnnotation(MyEntity.class);
if (null == myEntity) {
System.out.println("Hero类不是实体类");
} else {
System.out.println("Hero类是实体类");
// 从 MyTable 注解中提取需要关联的数据库表
MyTable myTable= (MyTable) clazz.getAnnotation(MyTable.class);
String tableName = myTable.name();
System.out.println("其对应的表名是:" + tableName);
... // 关联数据库表和实体对象的代码
}
在上面的例子中,我们从注解中提取了信息并使用修改了原始对象属性。