Java反射原理和实际用法
背景
反射在Java中非常重要,是Java区别于其他编程语言的一大特性。Java中的AOP切面、动态代理等看起来像黑魔法一样的技术,就离不开反射、字节码等。这些技术能在不侵入原有代码的情况下,做一些增强的非功能性需求。多提一句,千万不要把业务逻辑放在AOP切面、动态代理里,否则后人绝对会骂。
- AOP切面:在方法执行前后增加逻辑,可决定方法如何执行、甚至不执行。
- 动态代理:在运行时生成目标类的代理类,可增强目标类的功能。
本文总结一下反射的原理和实际用法。后续有空再介绍AOP切面、动态代理。
什么是反射?
看一下官方的原文定义:
Reflection is a feature in the Java programming language. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.
翻译过来就是:反射是指一个运行中的Java程序可以检查、修改自己内部的属性,也可称之为自省。反射是Java有别于其它编程语言的一大特性。
从reflection的字面意思看,就是倒影、反射,就好比你照镜子,通过倒影就能知道自己长什么样,理一理头发就能改变发型。
反射的原理
一句话概况就是:JVM会动态加载Class,一个Class实例包含了该类的所有完整信息,如:包名、类名、各个字段、各个方法、父类、实现的接口等。
因此,如果获取了某个类或对象的Class实例,就可以通过它获取到对应类的所有信息。
动态加载是指,JVM在第一次读取到一种Class类型时,才将其加载进内存,而不是一启动就加载所有类的信息。
每加载一种类,JVM就为其创建一个Class类型的实例,并关联起来。也即一个类的不同对象实例,背后对应的是同一个Class实例。
详细介绍可参考:https://www.liaoxuefeng.com/wiki/1252599548343744/1264799402020448
怎么使用反射?
需要熟练使用反射中常见的几个类:Class、Field、Method、Constructor。其它还有参数Parameter类等。可以这么理解,凡是Java对象中出现的东西都能在java.reflect包中找到对应的类。
Class
通过3种方式获取:
- 类.class:类的class静态变量
- 对象.getClass()
- Class.forName("类的全路径名")
Field
- Class实例.getField(name):根据字段名获取某个public的Field(包括父类)
- Class实例.getDeclaredField(name):根据字段名获取该类声明的某个Field(不包括父类)。常用
- Field[] getFields():获取所有的public字段,包括父类的字段。不常用,因为按照Java规范,一般都会定义private字段,然后通过public的getter、setter方法来获取字段值
- Field[] getDeclaredFields():获取该类声明所有的字段,不包括父类的字段。常用。
Method
- Class实例.getMethod(name, Class...):获取某个public的Method(包括父类)
- Class实例.getDeclaredMethod(name, Class...):获取该类的某个Method(不包括父类)
- Method[] getMethods():获取所有public的Method(包括父类)。常用
- Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
AnnotatedElement
Class、Field、Method都是AnnotatedElement的子类,有这些常用方法:
- getAnnotation(Class):根据Class获取对应的注解
- isAnnotationPresent(Class):判断是否被某个注解修饰,等效于getAnnotation(annotationClass) != null,是一种简便的写法
- getAnnotations():获取所有修饰的注解
反射用法举例
1、反射获取类的所有字段,方便后续运行时读写
如果一个类没有实现toString()方法,或者某些private字段没有提供getter方法,那么想要遍历所有字段,就只能通过反射来实现了。获取到所有反射字段后,想遍历或者修改就很容易了。
- 通过Field和对象,获取对应字段的值:
field.get(object)
- 通过Field和对象,修改对应字段的值:
- 先设置为可访问,这样即使是private字段,也能修改:
field.setAccessible(true);
- 反射修改字段值:
field.set(Object, Object)
- 先设置为可访问,这样即使是private字段,也能修改:
反射获取类的所有字段,分成2部分:
- 一是该类的父类的字段
- 二是该类引用的其它类的字段
getDeclaredFields方法只能获取到该类声明的字段,如果该类还继承了父类(可能有多层继承关系),那怎么获取所有这些继承而来的字段呢?所有类默认都是Object的子类,或者说Object是根上的父类。利用这一特性,可以通过getSuperclass不断往上找父类,获取父类的字段,然后直至父类为Object。同时也需了解,Object类本身没有任何字段。
于是,可以这么实现:
// 获取类的所有字段,包括父类
private List<Field> getAllFields(Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> cls = clazz;
while (cls != null) { // 也可以写成 while (cls != null && cls != Object.class)
result.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return result;
}
把某个类涉及的所有字段的反射信息(包括引用的其他类),都缓存至本地。
之所以要把反射信息缓存至本地,是因为反射是一个耗时操作。如果在初始化阶段就缓存起来,在后续要用到时性能更快。
使用广度优先遍历算法获取类引用的所有反射字段。以下是带注释的代码。
/**
* 获取类的所有反射字段示例(包含引用的类)
*/
public class GetAllFields {
// 构建反射字段的缓存:key是全路径类名,value是该类的所有字段
public static Map<String, List<Field>> buildReflectCache(Class<?> clazz) {
Map<String, List<Field>> result = new HashMap<>();
List<Field> topLevelFields = getAllFields(clazz);
result.put(clazz.getName(), topLevelFields);
// 广度优先遍历类的字段,通过队列来实现
Queue<Field> queue = new LinkedList<>(topLevelFields);
while (!queue.isEmpty()) {
Field field = queue.poll();
// 如果是集合或Map类型的字段,需要提取出泛型
if (Collection.class.isAssignableFrom(field.getType())
|| Map.class.isAssignableFrom(field.getType())) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericType;
for (Type type : parameterizedType.getActualTypeArguments()) {
Class<?> actualClass = (Class<?>) type;
if (!isBasicClass(actualClass)) {
List<Field> subFields = getAllFields(actualClass);
result.putIfAbsent(actualClass.getName(), subFields);
queue.addAll(subFields);
}
}
}
} else if (!isBasicClass(field.getType())) { // 只处理自定义类型,不处理Java基本类型
List<Field> subFields = getAllFields(field.getType());
result.putIfAbsent(field.getType().getName(), subFields);
queue.addAll(subFields);
}
}
return result;
}
// 获取类的所有字段,包括父类
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> cls = clazz;
while (cls != null) {
result.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return result;
}
// 是否Java基本类型:通过加载类的class loader是否为null得知,null为Java基本类型,否则为自定义类型
private static boolean isBasicClass(Class<?> clazz) {
return clazz != null && clazz.getClassLoader() == null;
}
public static void main(String[] args) {
List<Field> allFields = getAllFields(SomeClass.class);
System.out.println(allFields);
Map<String, List<Field>> reflectCache = buildReflectCache(SomeClass.class);
System.out.println(reflectCache);
}
}
2、结合反射和Spring,初始化时找到对应bean的对应方法
需要结合Spring来获取bean:Map<String, Object> applicationContext.getBeansWithAnnotation(Class)
,key是bean名称,value是bean Class实例。
假设有2个注解,@PrintInfoClass修饰在类上,@PrintInfoMethod修饰在方法上,代表要打印方法的签名。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.Type})
@Documented
public @interface PrintInfoClass {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface PrintInfoMethod {
}
@Component
public class PrintInfo implements ApplicationContextAware {
@PostConstruct
public void init() {
// 通过Spring框架,方便地获取带PrintInfoClass注解的bean
Map<String, Object> beansWithAnnotationMap = applicationContext.getBeansWithAnnotation((PrintInfoClass.class);
for (Map.Entry<String, Object> entry : beansWithAnnotationMap.entrySet()) {
Object bean = entry.getValue();
for (Method method : bean.getMethods()) {
// 只获取带PrintInfoMethod注解的方法
if (method.isAnnotationPresent(PrintInfoMethod.class) {
// 打印方法签名
StringBuilder paramString = new StringBuilder();
Class<?>[] paramClassList = method.getParameterTypes();
for (int i = 0; i < paramClassList.length; ++i) {
Class<?> paramClass = paramClassList[i];
paramString.append(paramClass.getSimpleName());
if (i != paramClassList.length - 1) {
paramString.append(",");
}
}
System.out.print(Modifier.toString(method.getModifiers()) + " " + method.getReturnType().getSimpleName()
+ " " + method.getName() + "(" + paramString.toString() + ")\n");
}
}
}
}
@Autowired
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}
Java反射原理和实际用法的更多相关文章
- java中this的用法?
2008-07-28 08:10cztx5479 | 分类:JAVA相关 | 浏览4533次 java中this的用法? import java.awt.*; import java.awt.even ...
- 转:十八、java中this的用法
http://blog.csdn.net/liujun13579/article/details/7732443 我知道很多朋友都和我一样:在JAVA程序中似乎经常见到“this”,自己也偶尔用到它, ...
- 设计模式 - 装饰者模式(Decorator Pattern) Java的IO类 用法
装饰者模式(Decorator Pattern) Java的IO类 用法 本文地址: http://blog.csdn.net/caroline_wendy/article/details/26716 ...
- JAVA反射原理
什么是反射? 反射,一种计算机处理方式.是程序可以访问.检测和修改它本身状态或行为的一种能力.java反射使得我们可以在程序运行时动态加载一个类,动态获取类的基本信息和定义的方法,构造函数,域等.除了 ...
- Java-Runoob-高级教程-实例-方法:09. Java 实例 – continue 关键字用法-un
ylbtech-Java-Runoob-高级教程-实例-方法:09. Java 实例 – continue 关键字用法 1.返回顶部 1. Java 实例 - continue 关键字用法 Java ...
- Java-Runoob-高级教程-实例-方法:08. Java 实例 – break 关键字用法
ylbtech-Java-Runoob-高级教程-实例-方法:08. Java 实例 – break 关键字用法 1.返回顶部 1. Java 实例 - break 关键字用法 Java 实例 Ja ...
- Java-Runoob-高级教程-实例-方法:07. Java 实例 – instanceOf 关键字用法
ylbtech-Java-Runoob-高级教程-实例-方法:07. Java 实例 – instanceOf 关键字用法 1.返回顶部 1. Java 实例 - instanceof 关键字用法 ...
- Java 中 synchronized的用法详解(四种用法)
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.本文给大家介绍java中 synchronized的用法,对本文感兴趣的朋友一起看看吧 ...
- java成神之——java中string的用法
java中String的用法 String基本用法 String分割 String拼接 String截取 String换行符和format格式化 String反转字符串和去除空白字符 String获取 ...
随机推荐
- Pandas:添加修改、高级过滤
1.添加修改数据 Pandas 的数据修改是进行赋值,先把要修改的数据筛选出来,然后将同结构或者可解包的数据赋值给它: 修改数值 df.Q1 = [1, 3, 5, 7, 9] * 20 # 就会把值 ...
- 且看这个Node全栈框架,实现了个Cli终端引擎,可无限扩充命令集
背景介绍 一般而言,大多数框架都会提供Cli终端工具,用于通过命令行执行一些工具类脚本 CabloyJS提供的Cli终端工具却与众不同.更确切的说,CabloyJS提供的是Cli终端引擎,由一套Cli ...
- 为什么要使用TypeScript(Why Typescript?)
客观原因 静态类型. 在编译期即可进行静态类型分析, 减少JS运行时类型错误. 语法功能强大 对于大型项目具有更好构建机制,加入了类.接口.泛型.模块等概念. 兼容JavaScript 与现存的Jav ...
- 【Spring】AOP实现原理(二):Advisor获取
@EnableAspectJAutoProxy @EnableAspectJAutoProxy注解可以用来开启AOP,那么就从@EnableAspectJAutoProxy入手学习一下Spring A ...
- 机器学习中 TP FP TN FN的概念
二分类 在二分类问题中,TP FP TN FN 是非常清楚且易于理解的. TP (True Positive) : 预测为 1 ,真实值也为 1 -> 真阳性 FP (False Positiv ...
- 面向对象的封装(粘贴Markdown代码解决缩进问题)
直接粘贴idea的代码会导致缩进错乱,建议先粘贴到记事本再粘贴到笔记!!! 1.先将属性私有化,再对外提供简单的接口可以访问内部.如set.get方法 2.set方法:修改年龄 public void ...
- 实践GoF的23种设计模式:装饰者模式
摘要:装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力.理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性. 本文分享自华为云社区<[Go实现]实践GoF的2 ...
- LeetCode. 812. 最大三角形面积
812. 最大三角形面积 鞋带公式 鞋带公式,用于计算任意多边形的面积,可用于计算三角形的面积 已知 ΔABC 三个顶点的坐标 A:(x1,y1). B:(x2,y2). C:(x3,y3) 对应的矩 ...
- React key究竟有什么作用?深入源码不背概念,五个问题刷新你对于key的认知
壹 ❀ 引 我在[react]什么是fiber?fiber解决了什么问题?从源码角度深入了解fiber运行机制与diff执行一文中介绍了react对于fiber处理的协调与提交两个阶段,而在介绍协调时 ...
- 写了个 Markdown 命令行小工具,希望能提高园友们发文的效率!
写了个 Markdown 命令行小工具,希望能提高园友们发文的效率! 前言 笔者使用 Typora 来编写 Markdown 格式的博文,图片采用的是本地相对路径存储(太懒了不想折腾图床). 时间久了 ...