背景

反射在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)

反射获取类的所有字段,分成2部分:

  • 一是该类的父类的字段
  • 二是该类引用的其它类的字段

getDeclaredFields方法只能获取到该类声明的字段,如果该类还继承了父类(可能有多层继承关系),那怎么获取所有这些继承而来的字段呢?所有类默认都是Object的子类,或者说Object是根上的父类。利用这一特性,可以通过getSuperclass不断往上找父类,获取父类的字段,然后直至父类为Object。同时也需了解,Object类本身没有任何字段。

于是,可以这么实现:

  1. // 获取类的所有字段,包括父类
  2. private List<Field> getAllFields(Class<?> clazz) {
  3. List<Field> result = new ArrayList<>();
  4. Class<?> cls = clazz;
  5. while (cls != null) { // 也可以写成 while (cls != null && cls != Object.class)
  6. result.addAll(Arrays.asList(cls.getDeclaredFields()));
  7. cls = cls.getSuperclass();
  8. }
  9. return result;
  10. }

把某个类涉及的所有字段的反射信息(包括引用的其他类),都缓存至本地。

之所以要把反射信息缓存至本地,是因为反射是一个耗时操作。如果在初始化阶段就缓存起来,在后续要用到时性能更快。

使用广度优先遍历算法获取类引用的所有反射字段。以下是带注释的代码。

  1. /**
  2. * 获取类的所有反射字段示例(包含引用的类)
  3. */
  4. public class GetAllFields {
  5. // 构建反射字段的缓存:key是全路径类名,value是该类的所有字段
  6. public static Map<String, List<Field>> buildReflectCache(Class<?> clazz) {
  7. Map<String, List<Field>> result = new HashMap<>();
  8. List<Field> topLevelFields = getAllFields(clazz);
  9. result.put(clazz.getName(), topLevelFields);
  10. // 广度优先遍历类的字段,通过队列来实现
  11. Queue<Field> queue = new LinkedList<>(topLevelFields);
  12. while (!queue.isEmpty()) {
  13. Field field = queue.poll();
  14. // 如果是集合或Map类型的字段,需要提取出泛型
  15. if (Collection.class.isAssignableFrom(field.getType())
  16. || Map.class.isAssignableFrom(field.getType())) {
  17. Type genericType = field.getGenericType();
  18. if (genericType instanceof ParameterizedType) {
  19. ParameterizedType parameterizedType = (ParameterizedType) genericType;
  20. for (Type type : parameterizedType.getActualTypeArguments()) {
  21. Class<?> actualClass = (Class<?>) type;
  22. if (!isBasicClass(actualClass)) {
  23. List<Field> subFields = getAllFields(actualClass);
  24. result.putIfAbsent(actualClass.getName(), subFields);
  25. queue.addAll(subFields);
  26. }
  27. }
  28. }
  29. } else if (!isBasicClass(field.getType())) { // 只处理自定义类型,不处理Java基本类型
  30. List<Field> subFields = getAllFields(field.getType());
  31. result.putIfAbsent(field.getType().getName(), subFields);
  32. queue.addAll(subFields);
  33. }
  34. }
  35. return result;
  36. }
  37. // 获取类的所有字段,包括父类
  38. private static List<Field> getAllFields(Class<?> clazz) {
  39. List<Field> result = new ArrayList<>();
  40. Class<?> cls = clazz;
  41. while (cls != null) {
  42. result.addAll(Arrays.asList(cls.getDeclaredFields()));
  43. cls = cls.getSuperclass();
  44. }
  45. return result;
  46. }
  47. // 是否Java基本类型:通过加载类的class loader是否为null得知,null为Java基本类型,否则为自定义类型
  48. private static boolean isBasicClass(Class<?> clazz) {
  49. return clazz != null && clazz.getClassLoader() == null;
  50. }
  51. public static void main(String[] args) {
  52. List<Field> allFields = getAllFields(SomeClass.class);
  53. System.out.println(allFields);
  54. Map<String, List<Field>> reflectCache = buildReflectCache(SomeClass.class);
  55. System.out.println(reflectCache);
  56. }
  57. }

2、结合反射和Spring,初始化时找到对应bean的对应方法

需要结合Spring来获取bean:Map<String, Object> applicationContext.getBeansWithAnnotation(Class),key是bean名称,value是bean Class实例。

假设有2个注解,@PrintInfoClass修饰在类上,@PrintInfoMethod修饰在方法上,代表要打印方法的签名。

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target({ElementType.Type})
  3. @Documented
  4. public @interface PrintInfoClass {
  5. }
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target({ElementType.METHOD})
  8. @Documented
  9. public @interface PrintInfoMethod {
  10. }
  1. @Component
  2. public class PrintInfo implements ApplicationContextAware {
  3. @PostConstruct
  4. public void init() {
  5. // 通过Spring框架,方便地获取带PrintInfoClass注解的bean
  6. Map<String, Object> beansWithAnnotationMap = applicationContext.getBeansWithAnnotation((PrintInfoClass.class);
  7. for (Map.Entry<String, Object> entry : beansWithAnnotationMap.entrySet()) {
  8. Object bean = entry.getValue();
  9. for (Method method : bean.getMethods()) {
  10. // 只获取带PrintInfoMethod注解的方法
  11. if (method.isAnnotationPresent(PrintInfoMethod.class) {
  12. // 打印方法签名
  13. StringBuilder paramString = new StringBuilder();
  14. Class<?>[] paramClassList = method.getParameterTypes();
  15. for (int i = 0; i < paramClassList.length; ++i) {
  16. Class<?> paramClass = paramClassList[i];
  17. paramString.append(paramClass.getSimpleName());
  18. if (i != paramClassList.length - 1) {
  19. paramString.append(",");
  20. }
  21. }
  22. System.out.print(Modifier.toString(method.getModifiers()) + " " + method.getReturnType().getSimpleName()
  23. + " " + method.getName() + "(" + paramString.toString() + ")\n");
  24. }
  25. }
  26. }
  27. }
  28. @Autowired
  29. private ApplicationContext applicationContext;
  30. public void setApplicationContext(ApplicationContext applicationContext) {
  31. this.applicationContext = applicationContext;
  32. }
  33. }

Java反射原理和实际用法的更多相关文章

  1. java中this的用法?

    2008-07-28 08:10cztx5479 | 分类:JAVA相关 | 浏览4533次 java中this的用法? import java.awt.*; import java.awt.even ...

  2. 转:十八、java中this的用法

    http://blog.csdn.net/liujun13579/article/details/7732443 我知道很多朋友都和我一样:在JAVA程序中似乎经常见到“this”,自己也偶尔用到它, ...

  3. 设计模式 - 装饰者模式(Decorator Pattern) Java的IO类 用法

    装饰者模式(Decorator Pattern) Java的IO类 用法 本文地址: http://blog.csdn.net/caroline_wendy/article/details/26716 ...

  4. JAVA反射原理

    什么是反射? 反射,一种计算机处理方式.是程序可以访问.检测和修改它本身状态或行为的一种能力.java反射使得我们可以在程序运行时动态加载一个类,动态获取类的基本信息和定义的方法,构造函数,域等.除了 ...

  5. Java-Runoob-高级教程-实例-方法:09. Java 实例 – continue 关键字用法-un

    ylbtech-Java-Runoob-高级教程-实例-方法:09. Java 实例 – continue 关键字用法 1.返回顶部 1. Java 实例 - continue 关键字用法  Java ...

  6. Java-Runoob-高级教程-实例-方法:08. Java 实例 – break 关键字用法

    ylbtech-Java-Runoob-高级教程-实例-方法:08. Java 实例 – break 关键字用法 1.返回顶部 1. Java 实例 - break 关键字用法  Java 实例 Ja ...

  7. Java-Runoob-高级教程-实例-方法:07. Java 实例 – instanceOf 关键字用法

    ylbtech-Java-Runoob-高级教程-实例-方法:07. Java 实例 – instanceOf 关键字用法 1.返回顶部 1. Java 实例 - instanceof 关键字用法   ...

  8. Java 中 synchronized的用法详解(四种用法)

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.本文给大家介绍java中 synchronized的用法,对本文感兴趣的朋友一起看看吧 ...

  9. java成神之——java中string的用法

    java中String的用法 String基本用法 String分割 String拼接 String截取 String换行符和format格式化 String反转字符串和去除空白字符 String获取 ...

随机推荐

  1. 《Mybatis 手撸专栏》第9章:细化XML语句构建器,完善静态SQL解析

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你只是在解释过程,而他是在阐述高度! 如果不是长时间的沉淀.积累和储备,我一定也没有 ...

  2. 什么!Sentinel流控规则可以这样玩?

    项目源码地址:公众号回复 sentinel,即可免费获取源码 前言 上一篇文章中,我们讲解了关于sentinel基本介绍以及流控规则中直接和快速失败的效果,有兴趣的可以去看上一篇文章,今天,我们给大家 ...

  3. Linux下添加MySql组件后报无权限问题解决

    Tomcat日志报错如下: Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost' (using pas ...

  4. 树莓派使用Docker部署EdgeX(jakarta版本)

    使用Docker部署EdgeX 老师安排我搞边缘计算,搞了很久都没能明白边缘计算是什么,甚至对其兴趣不大,前一阵弄好了lorawan网关,该做网关内部的边缘计算了,发现自己已经慢慢地学了进去,总是想要 ...

  5. 利用Github Action实现Tornadofx/JavaFx打包

    原文地址: 利用Github Action实现Tornadofx/JavaFx打包 - Stars-One的杂货小窝 最近开了个新项目,主要是个工具软件,也算个人的自娱自乐吧,也算开源的一部分,想着都 ...

  6. c++ 超长整数减法 高精度减法

    c++ 超长整数减法 高精度减法 实现思路 和加法类似,设置临时变量记录借位 当对应位数相减得到的结果大于等于0时,该位数字为本身值,否则需要加上借位的10.则\(t=(t+10)%10\) 打卡代码 ...

  7. .Net Core 中使用工厂模式

    什么是工厂模式 工厂模式是最常用的设计模式之一,属于创建型模式. 有点: 解耦,可以把对象的创建和过程分开 减少代码量,易于维护 什么时候用? 当一个抽象类有多个实现的时候,需要多次实例化的时候,就要 ...

  8. 叮,GitHub 到账 550 美元「GitHub 热点速览 v.22.26」

    作者:HelloGitHub-小鱼干 如果你关注 GitHub 官方动态,你会发现它们最近频频点赞世界各地开发者晒出的 GitHub $550 sponsor 截图,有什么比"白嫖" ...

  9. centos系统和Ubuntu系统命令区别以及常见操作

    目录 一.前言 二.系统环境 三.命令区别 3.1 使用习惯和命令区别 3.2 服务管理的区别 3.3 软件包信息区别 四.Ubuntu系统常见操作 4.1 Ubuntu系统apt和apt-get的区 ...

  10. SpringBoot项目集成Swagger启动报错: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is

    使用的Swagger版本是2.9.2.knife4j版本是2.0.4. SpringBoot 版本是2.6.2将SpringBoot版本回退到2.5.6就可以正常启动