反射

94 阅读8分钟

image.png 反射机制就是在程序的运行过程中被允许对程序本身进行操作,比如自我检查,进行装载,还可以获取类本身,类的所有成员变量和方法,类的对象,还可以在运行过程中动态的创建类的实例,通过实例来调用类的方法,这就是反射机制一个比较重要的功能了。

首先需要了解一下JVM,也就是Java虚拟机,Java之所以能跨平台就是因为jvm,上图是jvm的内存模型。比如我们写了一段代码:Student student = new Student() ,运行了起来!

首先JVM会启动,你的代码会编译生成一个.class文件,然后被类加载器加载进JVM的内存中,类Student被加载到方法区中,创建了Student类的class对象到堆中,注意这个不是new出来的对象,而是类的类型对象,每个类只有一个class对象,作为方法区类的数据结构的接口。JVM创建对象前,会先检查类是否加载,寻找类对象的class对象,若加载好,则作为你的对象分配内存,初始化也就是代码new Student();上面的流程就是你写好代码扔给JVM去跑。

反射在什么场景去使用呢,我们写的程序对象都是自己new出来的,程序相当与于写死了丢给Jvm去跑。假如一个服务器上突然遇到某个请求需要用到某个类,但是这个类没有加载进JVM,我们总不能让程序停下来再写段代码然后new一下再重启服务吧。

当程序运行时,需要动态的加载一些类,这些类之前用不到所以不用加载到JVM,而是在运行时根据需要才加载。比如我们项目中有时用到mysql,有时用的oracle,需要动态的根据实际情况加载驱动类,这个时候反射就有用了,假设com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection这两个类我们要用,这时候我们的程序就写的比较动态化,通过 Class tc = Class.forName("com.java.dbtest.myqlConnection"),通过类的全类名让JVM在服务器中找到并加载这个类,而如果时Oracle就是另一个了,这个时候就看到反射的好处了。

image.png 我们创建一个实体类


package tt.test1; 
public class User extends Person{
	
	public String name;
	private int age;
	
	public User(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	
	private User(int age)
	{
		super();
		this.age = age;
		
	}
	
	public User(String name)
	{
		super();
		this.name = name;
	}
	
	public User() {
		super();
	}
	@Override
	public String toString() {
		return "User [name=" + name + ", age=" + age + "]";
	}
	
	public void exit()
	{
		System.out.println(name+"退出系统");
	}
	
	public void login(String username,String password)
	{
		System.out.println("用户名:"+username);
		System.out.println("密码:"+password);
	}
	
	private String CheckInfo()
	{
		return "年龄:"+age;
	}
 
}

该实体类包含成员变量,成员方法和一些成员方法

利用反射可以获取类对象,有三种办法:

  1. 类名.class;
  2. 对象名.getCalss();
  3. Class.forName(具体的类名); 如以下代码: 第一行就是所获取的类对象,下面两行true的结果表示类对象只有一个(这里要注意的是第三种方法中类名一定要写全称,包名也要包括进去,不然JVM会无法定位这个类的具体位置)。下面在用一张图来解释这三种方法的执行时机:

image.png 在类加载的三个阶段里都可以获取类对象,其中第三种方法,在源码中获取类对象是最常用的,也是反射机制在框架中的应用,鉴于我目前的理解,在框架中的应用可以是通过配置文件写入所创建的类名,再利用第三种方法获取类对象。

获取类对象之后就可以对类进行一些创建对象、调用方法、访问成员变量的操作了:

  1. 创建对象
Object obj = 类对象.newInstance();

2.调用方法:

Method md = 类对象.getMethod("类中的公有方法名");

获取公有方法,其中md是Method类型的方法名,

Object obj1 = lm.invoke(u2,"老赵","aixiaoba");

为获取到的方法命名,方便调用。

Method dm = 类对象.getDeclaredMethod("类中的私有方法名");

获取私有方法名,又叫暴力获取,此方法无视方法的访问权限,即使是被private修饰的方法也会被获取到。

Method lm = 类对象.getMethod("有参方法方法名",参数的类对象...);

获取有参方法,同时要获取参数的类对象,格式为:参数类型.class

Object obj1 = lm.invoke(类的对象,"参数1","参数2");

调用获取到的方法,使用invoke关键字,此处表示调用有参方法。 以上方法中由于不确定获取到的对象类型,所以用Object接收。

User user = new User("ll", 23);
Class class1 = user.getClass();
System.out.println(class1);


Class class2 = User.class;
System.out.println(class2);

Class class3 = Class.forName("com.example.demo.test.User");
System.out.println(class3);


// 拿到对象
Object object = class1.newInstance();
// 获得公有方法
Method method = class1.getMethod("login", String.class, String.class);
Object object1 = method.invoke(object, "laowang", "123456");
System.out.println(object1);
// 暴力获取
Method method1 = class1.getDeclaredMethod("CheckInfo");
// 访问时会忽略访问修饰符的检查
method1.setAccessible(true);
Object object2 = method1.invoke(object);
System.out.println(object2);

在Java中可以通过反射进行获取实体类中的字段值,当未设置Field的setAccessible方法为true时,会在调用的时候进行访问安全检查,会抛出IllegalAccessException异常 解决方案就是设置Field对象的Accessible的访问标志位为Ture,就可以通过反射获取私有变量的值,在访问时会忽略访问修饰符的检查。

3.访问成员变量: 最重要的关键字Field

Field nf = 类对象.getField("成员变量名");

和调用私有方法一样,要访问私有成员变量也要通过暴力获取的的方式,同时也要获取访问私有成员变量的权限。

Field af = 类对象.getDeclaredField("私有成员变量名");
af.setAccessible(true);

一些常用的方法

方法名功能说明
static Class forName(String name)返回指定类名 name 的 Class 对象
Object newInstance()调用缺省构造函数,返回该Class对象的一个实例
Object newInstance(Object []args)调用当前格式构造函数,返回该Class对象的一个实例
getName()返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass()返回当前Class对象的父类的Class对象
Class [] getInterfaces()获取当前Class对象的接口
ClassLoader getClassLoader()返回该类的类加载器
Class getSuperclass()返回表示此Class所表示的实体的超类的Class

利用Class类的newInstance()方法可以创建一个类的实例,如果类中有多个构造方法,那调用的是类的无参构造器,所以我们在定义一个类的时候不能干掉类的无参构造,它的作用就是反射的时候用。

我们自定义一个方法 1. 把类对象和类方法名作为参数,执行方法

     * 
     * @param obj: 方法执行的那个对象. 
     * @param methodName: 类的一个方法的方法名. 该方法也可能是私有方法. 
     * @param args: 调用该方法需要传入的参数
     * @return: 调用方法后的返回值
     *  
     */
      public Object invoke(Object obj, String methodName, Object ... args) throws Exception{
        //1. 获取 Method 对象
        //   因为getMethod的参数为Class列表类型,所以要把参数args转化为对应的Class类型。
        
        Class [] parameterTypes = new Class[args.length];
        for(int i = 0; i < args.length; i++){
            parameterTypes[i] = args[i].getClass();
            System.out.println(parameterTypes[i]); 
        }
        
        Method method = obj.getClass().getDeclaredMethod(methodName, parameterTypes);
        //如果使用getDeclaredMethod,就不能获取父类方法,如果使用getMethod,就不能获取私有方法         //     //2. 执行 Method 方法
        //3. 返回方法的返回值
        return method.invoke(obj, args);
      }

调用

 @Test
        public void testInvoke() throws Exception{
            Object obj = new Person();            
            invoke(obj, "test", "wang", 1);             
        }

这样就通过对象名,方法名,方法参数执行了该方法

  1. 把全类名和方法名作为参数,执行方法
         * @param className: 某个类的全类名
         * @param methodName: 类的一个方法的方法名. 该方法也可能是私有方法. 
         * @param args: 调用该方法需要传入的参数
         * @return: 调用方法后的返回值
         */
        public Object invoke(String className, String methodName, Object ... args){
            Object obj = null;
            
            try {
                obj = Class.forName(className).newInstance();
                //调用上一个方法
                return invoke(obj, methodName, args);
            }catch(Exception e) {
                e.printStackTrace();
            }            
            return null;
        }
@Test
        public void testInvoke() throws Exception{
                
            invoke("com.atguigu.java.fanshe.Person", 
                    "test", "zhagn", 12);         
        }

使用系统方法(前提是此类有一个无参的构造器(查看API))

@Test
        public void testInvoke() throws Exception{
            Object result = 
                    invoke("java.text.SimpleDateFormat", "format", new Date());
            System.out.println(result);          
        }

这种反射实现的主要功能是可配置和低耦合。只需要类名和方法名,而不需要一个类对象就可以执行一个方法。如果我们把全类名和方法名放在一个配置文件中,就可以根据调用配置文件来执行方法

如何获取父类定义的(私有)方法

  前面说一般使用getDeclaredMethod获取方法(因为此方法可以获取类的私有方法,但是不能获取父类方法)

  如何获取父类方法呢,上一个例子format方法其实就是父类的方法(获取的时候用到的是getMethod)

  首先我们要知道,如何获取类的父亲:

  比如有一个类,继承自Person

  使用

    @Test
    public void testGetSuperClass() throws Exception{
        String className = "com.atguigu.java.fanshe.Student";
        
        Class clazz = Class.forName(className);
        Class superClazz = clazz.getSuperclass();
        
        System.out.println(superClazz); 
    }
}
//结果是 “ class com.atguigu.java.fanshe.Person ”

此时如果Student中有一个方法是私有方法method1(int age); Person中有一个私有方法method2();
怎么调用

  定义一个方法,不但能访问当前类的私有方法,还要能父类的私有方法

     * 
     * @param obj: 某个类的一个对象
     * @param methodName: 类的一个方法的方法名. 
     * 该方法也可能是私有方法, 还可能是该方法在父类中定义的(私有)方法
     * @param args: 调用该方法需要传入的参数
     * @return: 调用方法后的返回值
     */
    public Object invoke2(Object obj, String methodName, 
            Object ... args){
        //1. 获取 Method 对象
        Class [] parameterTypes = new Class[args.length];
        for(int i = 0; i < args.length; i++){
            parameterTypes[i] = args[i].getClass();
        }
        
        try {
            Method method = getMethod(obj.getClass(), methodName, parameterTypes);
            method.setAccessible(true);
            //2. 执行 Method 方法
            //3. 返回方法的返回值
            return method.invoke(obj, args);
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
    /**
     * 获取 clazz 的 methodName 方法. 该方法可能是私有方法, 还可能在父类中(私有方法)
     * 如果在该类中找不到此方法,就向他的父类找,一直到Object类为止
   * 这个方法的另一个作用是根据一个类名,一个方法名,追踪到并获得此方法     * @param clazz
     * @param methodName
     * @param parameterTypes
     * @return
     */
    public Method getMethod(Class clazz, String methodName, 
            Class ... parameterTypes){
        
        for(;clazz != Object.class; clazz = clazz.getSuperclass()){
            try {
                Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
                return method;
            } catch (Exception e) {}            
        }
        
        return null;
    }

如何描述字段-Field

    public void testField() throws Exception{
        String className = "com.atguigu.java.fanshe.Person";        
        Class clazz = Class.forName(className); 
        
        //1.获取字段
      //  1.1 获取所有字段 -- 字段数组
        //     可以获取公用和私有的所有字段,但不能获取父类字段
        Field[] fields = clazz.getDeclaredFields();
        for(Field field: fields){
            System.out.print(" "+ field.getName());
        }
        System.out.println();
        
        //  1.2获取指定字段
        Field field = clazz.getDeclaredField("name");
        System.out.println(field.getName());
        
        Person person = new Person("ABC",12);
        
        //2.使用字段
      //  2.1获取指定对象的指定字段的值
        Object val = field.get(person);
        System.out.println(val);
        
        //  2.2设置指定对象的指定对象Field值
        field.set(person, "DEF");
        System.out.println(person.getName());
        
        //  2.3如果字段是私有的,不管是读值还是写值,都必须先调用setAccessible(true)方法
        //     比如Person类中,字段name字段是公用的,age是私有的
        field = clazz.getDeclaredField("age");
        field.setAccessible(true);
        System.out.println(field.get(person));        
    }

但是如果需要访问父类中的(私有)字段:

     * //创建 className 对应类的对象, 并为其 fieldName 赋值为 val
     * //Student继承自Person,age是Person类的私有字段/     public void testClassField() throws Exception{
        String className = "com.atguigu.java.fanshe.Student";
        String fieldName = "age"; //可能为私有, 可能在其父类中. 
        Object val = 20;        
        
        Object obj = null;
        //1.创建className 对应类的对象
        Class clazz = Class.forName(className);
        //2.创建fieldName 对象字段的对象
        Field field = getField(clazz, fieldName);
        //3.为此对象赋值
        obj = clazz.newInstance();
        setFieldValue(obj, field, val);
        //4.获取此对象的值
        Object value = getFieldValue(obj,field);
    }
    
    public Object getFieldValue(Object obj, Field field) throws Exception{
        field.setAccessible(true);
        return field.get(obj);
    }

    public void setFieldValue(Object obj, Field field, Object val) throws Exception {
        field.setAccessible(true);
        field.set(obj, val);
    }

    public Field getField(Class clazz, String fieldName) throws Exception {
        Field field = null;
        for(Class clazz2 = clazz; clazz2 != Object.class;clazz2 = clazz2.getSuperclass()){        
                field = clazz2.getDeclaredField(fieldName);
        }
        return field;
    }

如何描述构造器-Constructor

    public void testConstructor() throws Exception{
        String className = "com.atguigu.java.fanshe.Person";
        Class<Person> clazz = (Class<Person>) Class.forName(className);
        
        //1. 获取 Constructor 对象
        //   1.1 获取全部
        Constructor<Person> [] constructors = 
                (Constructor<Person>[]) Class.forName(className).getConstructors();
        
        for(Constructor<Person> constructor: constructors){
            System.out.println(constructor); 
        }
        
        //  1.2获取某一个,需要参数列表
        Constructor<Person> constructor = clazz.getConstructor(String.class, int.class);
        System.out.println(constructor); 
        
        //2. 调用构造器的 newInstance() 方法创建对象
        Object obj = constructor.newInstance("zhagn", 1);                
    }

如何描述注解 -- Annotation

定义一个Annotation

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface AgeValidator {
    public int min();
    public int max();
}

此注解只能用在方法上

@AgeValidator(min=18,max=35)
    public void setAge(int age) {
        this.age = age;
    }

那么我们在给Person类对象的age赋值时,是感觉不到注解的存在的

@Test
    public void testAnnotation() throws Exception{
        Person person = new Person();    
        person.setAge(10);
    }

必须通过反射的方式为属性赋值,才能获取到注解

         * 1. 获取 Annotation
         * 
         * getAnnotation(Class<T> annotationClass) 
         * getDeclaredAnnotations() 
         * 
         */
    @Test
    public void testAnnotation() throws Exception{
        String className = "com.atguigu.java.fanshe.Person";
        
        Class clazz = Class.forName(className);
        Object obj = clazz.newInstance();    
        
        Method method = clazz.getDeclaredMethod("setAge", int.class);
        int val = 6;
        
        //获取指定名称的注解
        Annotation annotation = method.getAnnotation(AgeValidator.class);
        if(annotation != null){
            if(annotation instanceof AgeValidator){
                AgeValidator ageValidator = (AgeValidator) annotation;                
                if(val < ageValidator.min() || val > ageValidator.max()){
                    throw new RuntimeException("年龄非法");
                }
            }
        }        
        method.invoke(obj, 20);
        System.out.println(obj);          
    }

如果在程序中要获取注解,然后获取注解的值进而判断我们赋值是否合法,那么类对象的创建和方法的创建必须是通过反射而来的

反射小结

 1. Class: 是一个类; 一个描述类的类.

  封装了描述方法的 Method,

       描述字段的 Filed,

              描述构造器的 Constructor 等属性.

2. 如何得到 Class 对象:
2.1 Person.class
2.2 person.getClass()
2.3 Class.forName("com.atguigu.javase.Person")

3. 关于 Method:
3.1 如何获取 Method:
1). getDeclaredMethods: 得到 Method 的数组.
2). getDeclaredMethod(String methondName, Class ... parameterTypes)

3.2 如何调用 Method
1). 如果方法时 private 修饰的, 需要先调用 Method 的 setAccessible(true), 使其变为可访问
2). method.invoke(obj, Object ... args);

4. 关于 Field:
4.1 如何获取 Field: getField(String fieldName)
4.2 如何获取 Field 的值: 
1). setAccessible(true)
2). field.get(Object obj)
4.3 如何设置 Field 的值:
field.set(Obejct obj, Object val)

5. 了解 Constructor 和 Annotation 

6. 反射和泛型.
6.1 getGenericSuperClass: 获取带泛型参数的父类, 返回值为: BaseDao<Employee, String>
6.2 Type 的子接口: ParameterizedType
6.3 可以调用 ParameterizedType 的 Type[] getActualTypeArguments() 获取泛型参数的数组.


说了这么多,那反射在项目中具体有什么用呢 比如从数据库中获取数据扔到map中

@RestController
@RequestMapping("/hello")
public class TestController {

    @GetMapping("/hh")
    public void print() throws IllegalAccessException {
        Student student = new Student("张三",18,"数学","男","篮球",182.0,"红色");
        Map map = new HashMap();
        Field[] fields = student.getClass().getDeclaredFields();
        for (Field field:fields) {
            field.setAccessible(true);
            map.put(field.getName(),field.get(student));
        }
        System.out.println(map);
    }

}