面试题相关
说一下你对反射机制的理解
反射是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会隐性的把对应实例传给方法,这样方法就知道要把数据设置给哪个实例。
但当我们使用反射的形式去调用的时候,没有了这层隐形机制,所以得显式的告诉方法,应该操作是哪个实例的数据。