目录
什么是反射
反射就是在程序运行的过程中,我们可以通过一个类的字节码对象(Class对象),剖析出这个类的一切细节,知道他有哪些属性、方法、构造函数,甚至还可以动态的实例化一个对象。
Class对象是什么?
Java在经过编译后,会生成字节码文件(.class文件)。例如,Student.java编译生成了Student.class。
我们在用到这个类时,比如new一个Student(或者用到了它的静态方法),就会把Student.class加载到内存,读取他的信息,完成对象的实例化。
同时,java会把类的具体信息封装成一个Class类型的对象。一个类它能有啥信息?
属性、构造函数、方法、访问修饰符、也可能会被加上注解。。。。。。等等。
点进Class类,找到了一个内部类,确实是上述的那些东西。只不过字段、方法、构造函数这些比较复杂,想要描述他们只靠简单的字符串是不够的,还需要进一步封装成对象。
这些信息都被抽象出来,封装进了Class类,并提供了一套api供我们使用。
这个api就是反射api,使用这个api来获取类信息、创建实例的过程就叫做反射。
所以说,往简单点想,反射也没那么复杂,说到底还是调用api。只不过我们能用这个api做很多有趣的事,完成一些通过常规手段无法完成的任务。
如何获取字节码对象
三种方式
public static void main(String[] args) {
//通过类获取
Class<Student> studentClass1 = Student.class;
//通过对象获取
Student student = new Student();
Class<? extends Student> studentClass2 = student.getClass();
//Class.forName加载
try {
Class<?> studentClass3 = Class.forName("com.dayrain.entity.Student");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
springframework包下有一个工具类叫BeanUtils,里面有一个实际项目中经常用到的方法:
BeanUtils.copyProperties(obj1, obj2);
该方法可以完成任何对象之间的属性拷贝。因为这个方法太常用了,久而久之就容易忽略其中的原理。
我们点进源码
发现参数的类型是Object,也就是说它不关心对象的具体类型。在无视类型的条件下实现这一功能,正是因为用了反射技术。
今天我们通过反射来实现一个简易的copyProperties。
反射API学习
反射本身并不复杂,我们可简单学习一下常用的api。
除此之外,有一些方法的作用容易混淆
getMethod():获取自身能用所有的公共方法。1.类本身的public 2.继承父类的public 3.实现接口的public
getDeclaredMethod():获取类自身声明的所有方法。
public static void main(String[] args) throws Exception {
Class<Student> studentClass = Student.class;
Student student = new Student();
student.setName("小明");
student.setAge(18);
student.setAddress("地址");
student.setIdCard("1234567123xyz");
student.setGender(1);
student.number = "1000001";
//1、获取属性
//获取公有属性
Field[] fields = studentClass.getFields();
for (Field field : fields) {
//输出所有的变量名
System.out.println(field.getName());
}
//获取所有的属性(公有、私有)
Field[] declaredFields = studentClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
//获取属性名
System.out.println(declaredField.getName());
//获取属性类型的字节码对象,例如 class java.lang.String、
System.out.println(declaredField.getType());
//获取属性的访问修饰符,返回的是数字,0表示default,1表示public, 2表示private
System.out.println(declaredField.getModifiers());
//获取泛型信息
System.out.println(declaredField.getGenericType());
//获取注解类型
System.out.println(Arrays.toString(declaredField.getAnnotations()));
System.out.println("----------------------分割线--------------------");
}
//改变私有属性的值
Field age = studentClass.getDeclaredField("age");
//将访问权限改为可访问
age.setAccessible(true);
age.set(student, 20);
System.out.println("现在的学生信息为: " + student.getAge());
System.out.println("---------------------下面是类的方法信息----------------------");
//2、获取方法信息
Method[] methods = studentClass.getMethods();
for (Method method : methods) {
//方法名
System.out.println(method.getName());
//返回值类型
System.out.println(method.getReturnType());
//获取修饰符
System.out.println(method.getModifiers());
//获取注解信息
System.out.println(Arrays.toString(method.getDeclaredAnnotations()));
System.out.println("-------------------分割线--------------------");
}
//调用方法
//如果是重载方法,需要指定参数类型
Method method = studentClass.getMethod("introduceYourSelf");
//如果是私有方法
method.setAccessible(true);
method.invoke(student);
//3、获取构造信息
for (Constructor<?> declaredConstructor : studentClass.getDeclaredConstructors()) {
//获取参数, 参数做了进一步封装,有名字,访问控制符等
Parameter[] parameters = declaredConstructor.getParameters();
//获取构造方法的访问控制符
int modifier = declaredConstructor.getModifiers();
}
//5、判断对象的类型
//注解
boolean annotation = studentClass.isAnnotation();
//匿名类
boolean anonymousClass = studentClass.isAnonymousClass();
//数组
boolean array = studentClass.isArray();
//枚举
boolean anEnum = studentClass.isEnum();
//接口
boolean anInterface = studentClass.isInterface();
//局部类
boolean localClass = studentClass.isLocalClass();
//内部类
boolean memberClass = studentClass.isMemberClass();
//6、创建对象
Student student1 = studentClass.newInstance();
}
Student类
public class Student {
public String number;
private String name;
private int age;
private String address;
private String idCard;
private int gender;
public String introduceYourSelf() {
return "我叫:" + name + "年龄是:" + age + "地址是:" + address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
public int getGender() {
return gender;
}
public void setGender(int gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Student{" +
"number='" + number + '\'' +
", name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
", idCard='" + idCard + '\'' +
", gender='" + gender + '\'' +
'}';
}
}
实战:完成对象拷贝功能
需求:完成两个对象之间的属性拷贝,只针对变量名、类型(int、float......)和访问控制符都相同的变量。包装类和基本类型视作同类型。
分析:
实现这个功能可能有哪些步骤,我们需要理清:
1、获取两个类的属性对象
2、在操作属性的时候,考虑到有私有属性,所以要先破解属性
3、获取属性对象的修饰符、名称、类型
4、比对,如果满足变量名、类型、访问控制符都相同,则进行值拷贝。
实现:
public class BeanUtilsTest {
private static void copyProperties(Object source, Object target) throws BeansException, IllegalAccessException {
//获取各自的字节码对象
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
Field[] sourceFields = sourceClass.getDeclaredFields();
Field[] targetFields = targetClass.getDeclaredFields();
for (Field sourceField : sourceFields) {
//破解访问权限
sourceField.setAccessible(true);
//获取变量名
String name = sourceField.getName();
//获取访问修饰符
int modifiers = sourceField.getModifiers();
for (Field targetField : targetFields) {
//校验:变量名、访问修饰符、类型
if(name.equals(targetField.getName()) && modifiers == targetField.getModifiers() && typeCheck(sourceField, targetField)) {
//获取变量值
Object value = sourceField.get(source);
targetField.setAccessible(true);
targetField.set(target, value);
}
}
}
}
//判断类型是否一致
private static boolean typeCheck(Field sourceField, Field targetFiled) {
/**只需要比较前三个字母
* Byte byte
* Short short
* Integer int
* Long long
* Float float
* Double double
* Boolean bool
* Character char
*/
Class<?> sourceType = sourceField.getType();
Class<?> targetType = targetFiled.getType();
String sourceTypeName = sourceType.getSimpleName().toLowerCase().substring(0, 3);
String targetTypeName = targetType.getSimpleName().toLowerCase().substring(0, 3);
return sourceType.equals(targetType) || sourceTypeName.equals(targetTypeName);
}
}
基本类型是获取不到字节码对象的,例如int.class(),但是包装类有 Integer.class。所以如果想把基本类型和包装类型视为相同,则需要进行进一步的处理(typeCheck)。
getSimpleName()是获取类型名的缩写,例如java.lang.Byte会缩写成Byte。
另外一个测试类也贴一下:
StudentVO类
public class StudentVO {
public String number;
private String name;
private Integer age;
private String address;
private String gender;
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "StudentVO{" +
"number='" + number + '\'' +
", name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
", gender='" + gender + '\'' +
'}';
}
}
项目中业务写的比较多,直接用到反射的场合不是很多,基本都是间接用(因为有各种方便的框架替我们完成了很多复杂的工作!)。
之前有个需求,实现操作日志功能,用户每点击一次界面、调用一个api,都需要记录下来。用到了自定义注解和反射,有时间也贴出来。