乐趣区

你以为反射真的无所不能至少JDK8以后很强大

[TOC]

之前我们已经介绍了 Java 中框架常用的技术 — 反射。可以这么说反射方便了我们的开发。今天我们来说说他的短板,或者说我们今天在反射的基础上在进行方便化。

反射操作方法

在上一章节中我们学会了通过反射去调用方法。

public class App {public void test(String str, Integer integer) {System.out.println(str);
        System.out.println(integer);
    }
}

这个时候如果我想获取 test 方法对象的话应该这么做

Method testMethod = App.class.getMethod("test", String.class, Integer.class);

这里就不在赘述如何通过 Method 对象调用方法了。文章末尾会给出上一章节的地址。今天我们要研究的是 Method 如何获取方法参数这一块。看似简单却又是那么的传奇。我们看看下面一段代码执行的效果

public static void main(String[] args) throws ParseException, NoSuchMethodException {Method[] methods = App.class.getMethods();
        Method testMethod = App.class.getMethod("test", String.class, Integer.class);
        Class<?>[] parameterTypes = testMethod.getParameterTypes();
        Parameter[] parameters = testMethod.getParameters();
        for (Parameter parameter : parameters) {System.out.println(parameter.getName());
        }
    }

那么输出的两个参数名称是什么呢?一开始笔者这里想当然的认为是 str , name。相信此时的你应该和我一样认为是 str , name。

对的,你没看错返回的居然是无意义的名称,arg0 , arg1. 这就奇怪了。至于为什么呢?我现在还不想告诉你。下面会慢慢告诉你。

Spring 的方法的优点

做过 Javaweb 开发的肯定都用过 spring,springmvc , 在写 controller 层的时候我们都会在方法里直接写 key 值的名称,然后在请求地址中给相应的 key 赋值。

@RequestMapping(value = "/deptId", method = RequestMethod.GET)
public PagedResult<SysDept> selectSysDeptsByPK(Integer pageNumber, Integer pageSize) {return sysDeptService.selectSysDeptsByPK(deptId, pageNumber,  pageSize);
}

上述的 controller 我们在前端发送请求后会这样发送
http://{ip}:{port}/{projectName}/deptId?pageNumber=1&pageSize=5

这里我问一下你们有没有想过为什么 springmvc 它能够通过你传递的参数一一进行对应呢?我们上面已经尝试过通过反射是无法获取方法参数名称的。而 springmvc 无非就是反射操作方法的。这里是不是很神奇,不得不佩服 springmvc 的强大。强大到让人害怕。

反射如何实现 Spring 的方法

上面两个案例揭露了反射的缺点以及 springmvc 的强大。这里需要借助 springmvc 提供的一个工具ParameterNameDiscoverer。这个类顾名思义就是发现参数名称。在使用这个类之前我们先来了解下为什么反射获取不到方法名称。

这里需要简单说说 Java 执行过程,Java 之所以可以跨容器是因为 Java 针对各个系统提供了不同 jvm,所以我们开发前都需要安装不同版本的 jdk,jdk 里面提供了 jvm,java 代码运行期间是通过 jvm 去操作 class 文件的。但是我们平时都是开发 java 文件的。所以在 jvm 执行之前会有一个编译期间。javac 就是用来变异 java 文件为 class 文件的。

package com.zxhtom.test;

/**
 * Hello world!
 */
public class App {public void test(String str, Integer integer) {}}

针对上述代码我们通过 javac 进行编译下试试看看效果。
javac App.java
编译完成之后会出现一个同名的 class 文件

然后我们在通过命令查看下这个 class 文件
javap -verbose App.class

通过查看 App.java 对应的字节码发现在 javac 编译的时候对于方法的名称根本不会去记录的。想想也对我执行方法的时候只需要按顺序将参数放进去就行了。根本不需要关心参数名称是什么。那么问题显而易见了 jvm 不需要参数名所以编译时过率了。但是我们反射想通过参数名称一一对应这样效率更快。那么是 springmvc 是如何解决的呢。

private static final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
public static void main(String[] args) throws ParseException, NoSuchMethodException {java.lang.reflect.Method testMethod = App.class.getMethod("test", String.class, Integer.class);
    String[] parameterNames = parameterNameDiscoverer.getParameterNames(testMethod);
    for (String parameterName : parameterNames) {System.out.println(parameterName);
    }
}


对,就是 ParameterNameDiscoverer 这个方法帮助了我们。这里简单说说 ParameterNameDiscoverer 作用。springmvc 中会有一个默认的 ParameterNameDiscoverer 解释器 DefaultParameterNameDiscoverer 该类继承 PrioritizedParameterNameDiscovererPrioritizedParameterNameDiscoverer 这个类就是 getParameterNames 去获取方法名的。在 springmvc 中通过 addDiscoverer 方法有三个类注册到PrioritizedParameterNameDiscoverer

  • KotlinReflectionParameterNameDiscoverer:Spring5.0 提供,但是也得 jdk8 及以上版本使用
  • StandardReflectionParameterNameDiscoverer:Spring4.0 提供,但是也得 jdk8 及以上版本使用
  • LocalVariableTableParameterNameDiscoverer:Spring2.0 就有了,对 JDK 版本没啥要求,完全 Spring 自己实现的获取字段名称,逻辑复杂些,效率稍微低一点

总结一下就是在 springmvc4.0 之前 springmvc 都是通过自己实现的一套代码去获取字节码然后分析的。这里能力有限就不分析了。
在 4.0 以后因为 Java8 的推出弥补了这个 bug.springmvc 也就都采用 jdk 提供的功能获取参数名了。下面我们来看看 jdk8 是如何解决这个问题的。

Java 字节码

在上一节我们通过 javac , javap 命令进行了 Java 的编译了查看。我们发现 class 字节码中记录的信息有【常量区,类,方法】其中对于代码的记录有位置,堆,栈,行号等等。这也是我们 jvm 调优的依据。但是这仅仅是我们使用简单的 javac 的编译。

javac -g:编译更加全面点

经过对比发现 javac 和 javac -g 的区别好像是 javac -g 编译信息多出 LocalVariableTable 信息。

名称 解释
LineNumberTable 属性表存放方法的行号信息
LocalVariableTable 属性表中存放方法的局部变量信息

上图中通过 javac -g 编译的信息中 LocalVarableTable 有三条数据,是因为在编译期间每个非静态方法第一个参数都是 this. 去除第一条剩下的其实就是我们需要的参数信息。但是我们这个时候去执行一下看看效果。

高级反射注意点

所谓的高级反射其实就是对 jdk 版本的要求,只要是 jdk8 的版本,就可以用 jdk 提供的 parameter 方法获取参数名了。在编译的时候需要加上 -parameters

javac 的彩蛋

续点

每日一笑

今天公司放假,和老婆商量好一起去她家,出发前夕,老婆说:给我家里人的礼物买好了么?然后我去卧室全搬出来了;给她说:这是你舅的,这是你叔的,这是你爷爷的,这是你奶奶的,这是你爸的,这是你妈的,这是…………然后我俩就打起来了!

上期答案

加入战队

<span id=”addMe”> 加入战队 </span>

微信公众号

退出移动版