概述
Java反射(Reflection)是Java编程语言的一个特性,它允许在运行时对类、接口、字段和方法进行动态查询和操作。反射提供了一种在运行时查看和修改程序行为的能力,这通常用于实现一些高级功能,如框架(Spring)、ORM(对象关系映射)工具、IDE(集成开发环境)等。
反射的主要用途
动态加载类:可以在运行时加载和使用类,而无需在编写/编译时知道它们。
检查类信息:可以获取类的名称、父类、实现的接口、字段、方法等。
动态调用方法:可以调用类的任何公共方法,即使该方法在编译时未知。
创建和操作对象:可以创建类的实例,并调用其方法或访问其字段。
修改字段值:可以修改类的私有字段的值(但通常不推荐这样做,因为它破坏了封装性,又名暴力反射)。
反射使用场景及理解
1.1. 动态加载类信息及动态调用类中的方法。
假设我们需要报考微软云的证书证书分别有如下几种,而且将来可能还会新增。
Azure900, Azure903, Azure300, Azure380
我们假设有这样一个需求,当用户传入的参数能模糊匹配上如下的方法名时,就算要报名该考试。如我传入 "30", 这个值正好能模糊匹配上方法名“singUpAzure930Exam”和“signUpAzure300Exam”,这时就报名930 和300两门考试,如我传入Azure,很显然,四门课程都会报名。具体需要传入什么样的参数根据用户需求动态而定。
我们这里用伪代码编写一个报名系统,首先我们申明一个报名类。
public class AzureCloudExam {
public void AzureCloudExam(){
}
public String signUpAzure900Exam(){
return "You signed up Azure 900!";
}
public String singUpAzure930Exam(){
return "You signed up Azure 930!";
}
public String signUpAzure300Exam(){
return "You signed up Azure 300!";
}
public String signUpAzure380Exam(){
return "You signed up Azure 380!";
}
}
显然这种场景,使用我们以往的先new 对象,再调用方法的普通模式是很难做到的,而且将来有新增加的考试方法后,我们也需要修改调用考试方法的服务类才能做到,继而需要修改服务代码,重新部署等麻烦。因为这种普通模式需要调用哪个方法是我们编写是就必须按照逻辑提前写好的,显然它不够灵活,然后在我们平时的开发过程中,普通方式new 对象调用方法已经足够用了,但是在面临一些更加灵活的需要时,就不行了,如spring框架等都有大量的使用反射解决,因为我们在使用spring框架开发时,显然编写阶段spring也是不知道需要加载哪些bean的。而是运行时根据注解动态生成的。
因而如果我们使用反射,就可以很好的解决动态调用方法的这个问题,模拟步骤如下:
编写服务代码用于调用如上的考试类。
package com.mycompany.myreflect.refDemo2;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyReflectionServer {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
//接收用户传入参数
String className = args[0];
String examLike = args[1];
//动态加载需要使用class字节码文件
Class<?> clazz = Class.forName(className);
//动态无参构造方法new 对象
Object obj = clazz.getDeclaredConstructor().newInstance();
//获取class字节码中对应的所有方法
Method[] allMethods = clazz.getDeclaredMethods();
//loop所有方法
for (Method method:allMethods){
//如果用户传入的参数能模糊匹配上当前方法名
if (method.getName().contains(examLike)){
//调用报名方法
String result = (String) method.invoke(obj);
System.out.println(result);
}
}
}
}
如我们传入参数
运行结果
如上实现了动态决定调用哪个方法。
如将来还要加入Alicloud 的考试门类,我们也只需要添加Alicloud的报名类就可以了,而服务类的代码不需要做任何改动。这就是反射带来的动态调用方法的灵活性。
1.2. 动态创建类对象
假设我们有这样一个需求。还是如上的报名考试系统,但是由于业务的扩展,现在不仅要支持微软云认证报名,还需要支持阿里云报名。将来还会新增厂家。报名的用户也在爆发增长。
显然我们每次客户请求都new一个对应厂商的的类,然后再在该类中去调用对应厂商的服务接口是报名,会带来不必要的对象创建和销毁。我们这里只需要一个单例的bean对象就可以了。
- 在创建报名类之前我们先创建一个注解@MySupportExam, 用于注解某厂商的某类考试我们是否支持在我们这里报名,因为有的门类报名,对应厂商有授权考虑。
package com.mycompany.myreflect.refDemo2; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MySupportExam { }
- 创建@MyComponent注解,用于标准这是我们需要注入的厂商bean
package com.mycompany.myreflect.refDemo2; import java.lang.annotation.*; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyComponent { }
- 创建对应厂商的报名类。
package com.mycompany.myreflect.refDemo2; @MyComponent public class AzureCloudExam { public void AzureCloudExam(){} @MyEnableSupport public String signUpAzure900Exam(){return "You signed up Azure 900!";} @MyEnableSupport public String singUpAzure930Exam(){return "You signed up Azure 930!";} public String signUpAzure300Exam(){return "You signed up Azure 300!";} public String signUpAzure380Exam(){return "You signed up Azure 380!";} }
package com.mycompany.myreflect.refDemo2; @MyComponent public class AliCloudExam { public void AliCloudExam(){ } public String signUpAli900Exam(){return "You signed up Ali 900!";} @MyEnableSupport public String singUpAli930Exam(){return "You signed up Ali 930!";} @MyEnableSupport public String signUpAli300Exam(){return "You signed up Ali 300!";} public String signUpAli380Exam(){return "You signed up Ali 380!";} }
- 创建一个ClassScanner 扫描添加了@MyCompany注解的厂商类
package com.mycompany.myreflect.refDemo2; import java.io.File; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class ClassScanner { public static Set<Class<?>> scanPackage(String packageName) { Set<Class<?>> classSet = new LinkedHashSet<>(); try { Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(packageName.replace(".", "/")); while (urls.hasMoreElements()) { URL url = urls.nextElement(); String protocol = url.getProtocol(); if ("file".equals(protocol)) { String filePath = URLDecoder.decode(url.getPath(), "UTF-8"); findClassByPackageName(new File(filePath), packageName, classSet); } } } catch (Exception e) { e.printStackTrace(); } return classSet; } private static void findClassByPackageName(File file, String packageName, Set<Class<?>> classSet) throws ClassNotFoundException { if (file.isFile() && file.getName().endsWith(".class")) { String className = file.getName().substring(0, file.getName().length() - 6); String fullClassName = packageName + "." + className; Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(fullClassName); classSet.add(clazz); } else if (file.isDirectory()) { File[] files = file.listFiles(); for (File f : files) { findClassByPackageName(f, packageName, classSet); } } } public static List<Class<?>> getExamCompanyClazz(List<Class<?>> examCompany,String packageName) { Set<Class<?>> classes = scanPackage(packageName); for (Class<?> clazz : classes) { if (clazz.getDeclaredAnnotation(MyComponent.class) !=null){ examCompany.add(clazz); } } return examCompany; } }
- 创建一个容器类用于管理所有的厂商bean,和方法bean
package com.mycompany.myreflect.refDemo2; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ExamContainer { private Map<Class<?>,List<Method>> methodsSupportMap; private Map<Class<?>,Object> servicesMap; private List<Class<?>> examCompanies; public void init() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { examCompanies = new ArrayList<>(); servicesMap = new HashMap<>(); methodsSupportMap = new HashMap<>(); ClassScanner.getExamCompanyClazz(examCompanies,"com.mycompany.myreflect.refDemo2"); for (int i = 0;i<examCompanies.size();i++){ Class<?> clazz = examCompanies.get(i); //将获取到的厂商字节码,判断容器中是否存在该对象,如果不存在则创建该对象并放入容器中 setServiceInstanceByClass(clazz); setMethod(clazz); } } public void setServiceInstanceByClass(Class<?> clazz) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { Object obj = clazz.getDeclaredConstructor().newInstance(); servicesMap.put(clazz,obj); } public void setMethod(Class<?> clazz){ Method[] methods =clazz.getDeclaredMethods(); // 如果方法加了@MySupportExam注解,说明我们这里支持该科目考试,加入容器 for (Method method: methods){ if (method.getDeclaredAnnotation(MySupportExam.class) != null){ if (methodsSupportMap.get(clazz)!=null){ methodsSupportMap.get(clazz).add(method); }else { List<Method> methodList = new ArrayList<>(); methodList.add(method); methodsSupportMap.put(clazz,methodList); } } } } public void signUpExam(String examLike) throws InvocationTargetException, IllegalAccessException { for (Class<?> clazz:examCompanies) { Object object = servicesMap.get(clazz); List<Method> methodList = methodsSupportMap.get(clazz); for (Method method : methodList) { if (method.getName().contains(examLike)) { System.out.println(method.invoke(object)); } } } } }
- 用户请求服务方法调用
package com.mycompany.myreflect.refDemo2; import java.lang.reflect.InvocationTargetException; public class MyReflectionServer { public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, ClassNotFoundException { ExamContainer examContainer = new ExamContainer(); examContainer.init(); String examLike = args[0]; examContainer.signUpExam(examLike); } }
请求参数
运行结果
请求参数
运行结果
1.3. 暴力反射
最近在开发过程中使用到暴力反射的一个案例就是,当我去给一个service类写UT实,发现某些方法是private的,但是这些private的方法,又是需要写UT的,这个时候我们就可以利用反射的暴力反射去为这个方法写UT调用。只需要设置如下属性即可,这里不在代码举案例。
method.setAccessible(true);
注意事项
性能:反射通常比直接方法调用要慢得多,因为涉及到了额外的类型检查和安全性检查。因此,在性能敏感的代码中应谨慎使用。
安全性:反射允许绕过访问控制检查(例如,可以访问和修改私有字段),这可能导致安全问题。因此,在使用反射时应格外小心。
可读性:使用反射的代码通常比直接代码更难理解和维护。因此,在可以使用直接代码的情况下,应避免使用反射。