《面试王者系列》Java反射

149 阅读7分钟

面试题相关

说一下你对反射机制的理解

反射是Java提供的动态加载类和执行方法的能力,本质上是通过调用Class类提供的API,在程序运行期间动态的加载类、创建实例和执行方法。

反射有什么使用场景

需要动态加载类或者动态执行某个方法的场景,都可以考虑用反射。

例如我们可以利用反射机制,把开放给外部的接口收拢成一个入口,通过传入的参数来决定要执行的具体服务。

还有JDBC在加载驱动的时候,也用到了反射,在配置数据库连接信息的参数里,有一个参数负责指定驱动的全路径名,JDBC就是通过反射来加载这个驱动的。

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

反射执行方法的时候,为什么要传递实例

因为一个类在加载完成后,代码的字节流信息在内存中,只有一份,是共享的。

类的实例却可以有无数份,当执行一个方法时,需要知道应该从哪个实例里面取数据和设置数据。

当通过对象实例去调用方法的时候,JVM会隐性的把对应实例传给方法。

但当我们使用反射的形式去调用的时候,没有了这层隐形机制,所以得显式的告诉方法是哪个实例执行的方法。

相关知识

先说重点

反射,让Java语言拥有了动态加载实例和动态执行方法的能力。

它能够通过字符串对应的全类名,反射得到这个类的实例,也可以把字符串对应的方法名,匹配到对应的方法,通过反射去执行它。

反射不是万能的,当我们需要反射去执行方法的时候,首先得知道方法具体的参数是什么,否则会匹配不到方法从而执行失败。

引入

Java的反射其实是很抽象的概念,这很难找到恰当的比喻来形容它,所以接下来我会用一些实例,把它描绘清楚。

通常情况下,我们是这样创建实例,然后去执行它的方法

UserService userService = new UserServiceImpl();
userService.getUserInfo(uid);

但是利用反射,我们可以这样玩:

通过一个字符串的全类名,得到这个类的Class对象

再通过这个Class对象,取到和字符串对应的方法实例(Method)

最后执行方法(invoke),返回信息

public static void main(String[] args) {
    reflex("com.xxx.UserService", "getUserInfo", 100001);
}

/**
 * 反射执行(仅支持带有无参构造函数的类)
 * @param className 类全路径名
 * @param methodName 方法名
 * @param params 参数列表
 */
public Object reflex(String className, String methodName, Object... params) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    // 1、先通过 类全路径名 取到类的class信息
    Class clazz = Class.forName(className);
    // 2、创建实例对象
    Object instance = clazz.newInstance();
    // 3、获取方法
    // 3.1取参数类型
    Class<?>[] clazzList = new Class[params.length];
    for (int i = 0; i < params.length; i++) {
        clazzList[i] = params[i].getClass();
    }
    // 3.2取方法
    Method method = clazz.getMethod(methodName, clazzList);
    // 最后,执行 methodName、params 对应的方法名和参数后,返回
    return method.invoke(instance, params);
}

和常规 new 的方式对比起来,却更复杂且代码也多了很多对不对

那反射的好处是啥?

划重点了,是 动态!

举个项目中实际场景的例子:

现在的开发模式,基本都是前后端分离了,Java作为后端,避免不了给前端开放HTTP接口

通常情况,是这样做的

@RestController
@RequestMapping(value = "/xxx/")
public class XXXController {
    @RequestMapping(value = "checkToken.json", method = {RequestMethod.POST})
    @ResponseBody
    public Object checkToken(@RequestParam("token") String token){
        UserService userService = new UserServiceImpl();
        return userService.checkToken(uid);
    }
    
    @RequestMapping(value = "login.json", method = {RequestMethod.POST})
    @ResponseBody
    public Object login(@RequestParam("userName") String userName,
                        @RequestParam("password") String password){
        UserService userService = new UserServiceImpl();
        return userService.login(userName, password);
    }
}

以上代码,每个业务,都需要对外开放一个接口

一个项目,可能就得开放几十甚至几百个接口,重复写那么多遍差不多的代码,确实也不太优雅

有办法解决这个问题吗? 有的,反射就是其中之一

把请求的入口收拢成一个 runService.json ,由请求参数 className 和 methodName 来决定具体执行那个接口

@RequestMapping(value = "runService.json", method = {RequestMethod.POST})
@ResponseBody
public Object runService(@RequestBody RequestModel request){
    return reflex(request.className, request.methodName, request.params.toArray());
}

/**
 * 反射执行
 * @param className 类全路径名
 * @param methodName 方法名
 * @param params 参数列表
 */
public Object reflex(String className, String methodName, Object... params) {
    try {
        // 1、先通过 类全路径名 取到类的class信息
        Class clazz = Class.forName(className);
        // 2、创建实例对象
        Object instance = clazz.newInstance();
        // 3、获取方法
        // 3.1取参数类型
        Class<?>[] clazzList = new Class[params.length];
        for (int i = 0; i < params.length; i++) {
            clazzList[i] = params[i].getClass();
        }
        Method method = clazz.getMethod(methodName, clazz);
        // 最后,执行 methodName、params 对应的方法名和参数后,返回
        return method.invoke(instance, params);
    }catch (Exception e){
        return new HashMap<String, Object>(){{ put("success", false); put("message", e.getMessage()); }};
    }
}

static class RequestModel{
    /** 类全路径名 */
    public String className;
    /** 方法名 */
    public String methodName;
    /** 参数列表 */
    public List params;
}

使用反射后,好处很明显,以后无论新增多少请求接口,只需要关注业务实现类即可,不需要去controller层开放接口了。

但这样做也是有缺点的

1、安全性,因为是开放接口,所以理论上说外部可以调用我们的任何类和方法(解决方案是定义一个ControllerService接口类,让对外开放的服务都实现这个接口,然后在reflex里做个判断,只有这个类型的实例才能调用。)

2、Spring依赖注入失效,现在的项目,基本都是用Spring了,反射出来的实例,和 new 出来的实例一样,都不是Spring容器创建的,所以Spring的依赖注入无效。(如果是这样的场景,解决方案是直接从Spring容器获取实例,然后通过反射调用方法即可。)

3、反射执行的方法不支持基本类型参数,因为params用的List,基本数据类型会自动装箱。

解决以上问题,可以这样优化一下代码:

/** spring上下文 */
public static ApplicationContext CONTEXT;

/** 通用的服务接口 */
@RequestMapping(value = "runService.json", method = {RequestMethod.POST})
@ResponseBody
public Object runScript(@RequestBody RequestModel request){
    try {
        // 1、获取实例
        Object instance = StringUtils.isNotBlank(request.className) ?
                reflexInstance(request.className) :
                getSpringContainerInstance(request.serviceCode);
        // 2、权限判断
        if (!(instance instanceof ControllerService)){
            return new HashMap<String, Object>(){{ put("success", false); put("message", "无权限访问此服务"); }};
        }
        // 3、执行方法
        return reflexMethod(instance, request.methodName, request.params);
        
    }catch (Exception e){
        return new HashMap<String, Object>(){{ put("success", false); put("message", e.getMessage()); }};
    }
}

/** 反射获取实例 */
public Object reflexInstance(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    // 1、先通过 类全路径名 取到类的class信息
    Class clazz = Class.forName(className);
    // 2、再通过class,创建和返回实例对象
    return clazz.newInstance();
}

/** 获取Spring容器里的实例 */
public Object getSpringContainerInstance(String serviceCode) {
    return CONTEXT.getBean(serviceCode);
}

/** 反射执行方法 */
public Object reflexMethod(Object instance, String methodName, Object... params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    // 取参数类型
    Class<?>[] clazzList = new Class[params.length];
    for (int i = 0; i < params.length; i++) {
        clazzList[i] = params[i].getClass();
    }
    // 获取方法
    Method method = instance.getClass().getMethod(methodName, clazzList);
    // 执行 methodName、params 对应的方法,返回
    return method.invoke(instance, params);
}

static class RequestModel{
    /** Spring服务编码 */
    public String serviceCode;
    /** 类全路径名 */
    public String className;
    /** 方法名 */
    public String methodName;
    /** 参数列表 */
    public List params;
}

/** 实现 ApplicationContextAware 接口,通过重写setApplicationContext获取上下文 */
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    CONTEXT = applicationContext;
}
public interface ControllerService{}

因为是示例代码,所以没有写的很严谨,比如一些非空、必填参数校验等等

重点想表达的是思路

对于依赖Spring的,根据 serviceCode 获取Spring容器内的实例

不依赖Spring的,也就是传递的是className参数的,还是使用反射获取实例

然后增加一个 ControllerService 接口和对应的判断,只有这个接口的实例,才能执行,否则提示无权限。

反射还运用在很多地方,比如 JDBC 的数据库驱动加载,也是使用了反射

#MySql驱动加载配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

还有很多的场景,比如一些工厂模式也可以使用等

接下来,看一下反射的底层原理

底层

Java提供Class类来描述一个.class文件

我们编写的.java文件,通过编译后,变成.class文件,经过jvm加载后,变成了Class对象

所以描述一个类所需要的所有信息,都在Class类里,我们可以从Class对象里,取出一个类里所有的 构造器、字段、方法等信息。

反射就在这个基础之上实现的。

一个基本的反射执行的流程,是这样的

public Object reflex(String className, String methodName, Object... params) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    // 1、先通过 类全路径名 取到类的class信息
    Class clazz = Class.forName(className);
    // 2、创建实例对象
    Object instance = clazz.newInstance();
    // 3、获取方法
    // 3.1取参数类型
    Class<?>[] clazzList = new Class[params.length];
    for (int i = 0; i < params.length; i++) {
        clazzList[i] = params[i].getClass();
    }
    // 3.2取方法
    Method method = clazz.getMethod(methodName, clazzList);
    // 最后,执行 methodName、params 对应的方法名和参数后,返回
    return method.invoke(instance, params);
}

我们一步步来解构它

第一步: 通过类全路径名,得到对应的Class对象

Class clazz = Class.forName(className);

看下它的源码

其实还是交给类加载器去加载的,和 new 没有本质区别

第二步: 通过Class对象,创建对应的实例

Object instance = clazz.newInstance();

从源码上看,要使用 Class.newInstance() 实例化对象,那么对象类必须提供无参构造函数

如果对象类没有提供无参构造函数,那就不能使用 Class.newInstance() 啦

得通过 Class.getConstructor() + 对应构造器参数类型(参数类型指的是Class对象),取到构造器(Constructor),再执行构造器的 newInstance() 方法创建实例。

第三步: 通过Class对象,取出要执行的方法

Method method = clazz.getMethod(methodName, clazzList);

来看下 Class.getMethod() 的源码,重点看其内调用的 searchMethods()

调用Class.getMethod() 方法时,其实会遍历这个类所有的method,根据名称和参数类型匹配唯一的Method返回

如果是无参方法,可以不传参数类型

如果是有参方法,必须传递参数类型

参数类型,指的是Class对象。

刚用反射的时候,我有个疑惑,为什么不能传参数值去匹配,由 getMethod() 方法内自行取参数对应的Class

后来想明白了,通过参数值,匹配Method,会出问题,因为参数可能会是基本类型,举个例子:

public Object getUserOrder(long uid, String orderNumber){ return new Object(); }

如果按传递参数内容来匹配方法,那么 getMethod() 方法得这样定义入参

public Method getMethod(String name, Object... parameters)

如下,虽然调用 getMethod() 传递的是long(基本类型),但由于Java会自动装箱,会自动转成Long类型(引用类型),这和 getUserOrder() 方法参数使用的long(基本类型)不一样。就会出现参数没问题,但就是匹配不到方法。

public static void main(String[] args) {
    long uid = 1;
    String orderNumber = "100001";
    Method method = clazz.getMethod("getUserOrder", uid, orderNumber);
}

第四步: 执行

获取到 Method 后,调用invoke() 执行,看下源码。

具体怎么执行的逻辑,因为是调用原生方法,看不到细节,也不需要我们操心。

我们要关注的,是它的参数,分别是 目标实例、执行方法的参数列表

参数列表可以理解,因为方法需要用到这些参数。

目标实例是什么作用?

因为一个类在加载完成后,代码的字节流信息在内存中,只有一份,是共享的。

类的实例却可以有无数份,当执行一个方法时,需要知道应该从哪个实例里面取数据和设置数据。

当通过对象实例去调用方法的时候,JVM会隐性的把对应实例传给方法。

但当我们使用反射的形式去调用的时候,没有了这层隐形机制,所以得显式的告诉方法是哪个实例执行的方法。

比如以下代码

class UserDTO{
    private long uid;
    
    public long getUid() {
        return uid;
    }
    public void setUid(long uid) {
        this.uid = uid;
    }
}

UserDTO user1 = new UserDTO();
UserDTO user2 = new UserDTO();
user1.setUid(101);
user2.setUid(102);

UserDTO 里面有uid这个成员属性,当通过对象实例去调用 setUid() 这个方法的时候,JVM会隐性的把对应实例传给方法,这样方法就知道要把数据设置给哪个实例。

但当我们使用反射的形式去调用的时候,没有了这层隐形机制,所以得显式的告诉方法,应该操作是哪个实例的数据。