反射和动态代理

1. 反射

Spring框架中也大量使用了动态代理,而动态代理的实现依赖于反射。

引入:(请看完有助于你理解)

在程序运行期间,系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。(一个类可能有多个运行时类型标识跟踪)可以使用一个特殊的Java类访问这些信息。保存这些信息的类名叫Class

Class对象实际上描述的是一个类型,这可能是类,也可能不是类。例如,int不是类,但int.Class确实是一个Class类型的对象

注意:虚拟机为每个类型管理一个唯一的Class对象(一个类只能有一个Class对象)

1.1 反射的概述:

反射可以获取到字段(成员方法or构造方法),并对其解剖,得到该变量的修饰符、变量名、类型等

注意:上面提到的“获取”是在类的字节码文件中获取(所以使用反射的第一步就是要得到该类的字节码文件对象)

1.2 获取字节码文件对象的三种方式

  • Class.forName("全类名")

  • 类名.class

  • 对象.getClass()

代码示例:

Class clazz1 = Class.forName("com.itheima.reflectdemo.Student");
//clazz1 就是当Student.class这个文件加载到内存之后,产生的字节码文件对象
​
Class clazz2 = Student.class;
​
Student s = new Student();
Class clazz3 = s.getClass();

上面生成的字节码文件对象都是同一个,即clazz1 == clazz2

1.3 字节码文件和字节码文件对象

java文件:自己编写的.java文件

字节码文件:编译之后的.class文件

字节码文件对象:当.class文件加载到内存之后,虚拟机自动创建出来的对象。

1.4 获取构造器

方法名说明
Constructor<?>[] getConstructors()获得所有的构造(只能public修饰)
Constructor<?>[] getDeclaredConstructors()获得所有的构造(包含private修饰)
Constructor<T> getConstructor(Class<?>... parameterTypes)获取指定构造(只能public修饰)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)获取指定构造(包含private修饰)

代码示例:

public class Student {
    private String name;
    private int age;
​
    public Student() {
    }
​
    public Student(String name) {
        this.name = name;
    }
​
    private Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}
    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}
//1.获得class字节码文件对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
//获取空参构造
Constructor con1 = clazz.getConstructor();
//获取指定参数构造器
Constructor con2 = clazz.getConstructor(String.class,int.class);

1.6 获取构造器应用

利用获取到的构造器,创建对象:

第一步:获取整体的字节码文件对象clazz

第二步:获取有参构造方法con

第三步:创建对象(注意要修改构造方法的访问权限)

//1
Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
//2
Constructor con = clazz.getDeclaredConstructor(String.class, int.class);
//3
con.setAccessible(true);//暴力访问
Student stu = (Student) con.newInstance("zhangsan", 23);
System.out.println(stu);

1.7 获取成员变量

方法名说明
Field[] getFields()返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields()返回所有成员变量对象的数组,存在就能拿到
Field getField(String name)返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name)返回单个成员变量对象,存在就能拿到

代码示例:

public class Student {
    private String name;
    private int age;
    public String gender;
    public String address;
​
    public Student() {
    }
​
    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
​
    public Student(String name, int age, String gender, String address) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.address = address;
    }
    //省略get和set方法
}
//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
​
Field[] fields = clazz.getFields();

1.8 获取成员变量应用

方法说明
void set(Object obj, Object value)赋值(修改obj对象的变量值为value)
Object get(Object obj)获取值

利用获取到的成员变量,获取变量值和修改变量值

代码示例:

Student s = new Student("zhangsan",23,"广州");
Student ss = new Student("lisi",24,"北京");
​
//1.获取class对象
Class clazz = Class.forName("com.itheima.reflectdemo.Student");
​
//2.获取name成员变量
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
​
field.set(s,"wangwu");
​
String result = (String)field.get(s);

1.9 获取成员方法

方法名说明
Method[] getMethods( )返回所有成员方法对象的数组(只能拿public的)
Method[] getDeclaredMethods( )返回所有成员方法对象的数组,存在就能拿到
Method getMethod(String name, Class<?>... parameterTypes)返回单个成员方法对象(只能拿public的)
Method getDeclaredMethod(String name, Class<?>... parameterTypes)返回单个成员方法对象,存在就能拿到

代码示例:

public class Student {
    private String name;
    private int age;
    public String gender;
    public String address;
    
    public void sleep(){
        System.out.println("sleep");
    }
    
    public String eat(String something){
        System.out.println("学生在吃" + something);
        return "学生已经吃完了,非常happy";
    }
    
    public void playGame(){
        System.out.println("playGame");
    }
}
Class<?> clazz = Class.forName("com.itheima.reflectdemo.Student");
​
Method[] methods1 = clazz.getMethods();
Method[] methods2 = clazz.getDeclaredMethods();
Method method3 = clazz.getMethod("sleep");
Method method4 = clazz.getMethod("eat",String.class);
Method method5 = clazz.getDeclaredMethod("playGame");

1.10 获取成员方法并运行

方法

Object invoke(Object obj, Object... args) :运行方法

参数一:用obj对象调用该方法

参数二:调用方法的传递的参数(如果没有就不写)

返回值:方法的返回值(如果没有就不写)

代码示例:

public class Student {
    private String name;
    private int age;
    public String gender;
    public String address;
    
    public String eat(String something){
        System.out.println("学生在吃" + something);
        return "学生已经吃完了,非常happy";
    }
}
public class ReflectDemo6 {
    public static void main(String[] args) throws Exception {
        //1.获取字节码文件对象
        Class clazz = Class.forName("com.itheima.a02reflectdemo1.Student");
        
        //2.获取一个对象
        Student s = new Student();
        
        //3.获取一个指定的方法
        //参数一:方法名
        //参数二:参数列表,如果没有可以不写
        Method eatMethod = clazz.getMethod("eat",String.class);
        
        //运行
        //参数一:表示方法的调用对象
        //参数二:方法在运行时需要的实际参数
        //注意点:如果方法有返回值,那么需要接收invoke的结果
        //如果方法没有返回值,则不需要接收
        String result = (String) eatMethod.invoke(s, "重庆小面");
        System.out.println(result);
​
    }
}

面试题:

你觉得反射好不好?好,有两个方向

第一个方向:无视修饰符访问类中的内容。但是这种操作在开发中一般不用,都是框架底层来用的。

第二个方向:反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。

1.11 练习泛型擦除(掌握概念,了解代码)

概念:

集合中的泛型只在java文件中存在,当编译成class文件之后,就没有泛型了。

在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。

例2.通过反射添加其它类型元素

package com.itheima.reflectdemo;
​
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
​
public class ReflectDemo8 {
    public static void main(String[] args) throws Exception {
        //1.创建集合对象
        ArrayList<Integer> list = new ArrayList<>();
        list.add(123);
​
        //2.利用反射运行add方法去添加字符串
        //因为反射使用的是class字节码文件
​
        //获取class对象
        Class clazz = list.getClass();
​
        //获取add方法对象
        Method method = clazz.getMethod("add", Object.class);
​
        //运行方法
        method.invoke(list,"aaa");//相当于:list.add("aaa");
    }
}

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型Object。

下面的代码中类Pair使用泛型。在编译阶段会发生泛型擦除。JVM最后看到的只是原始类型

例3.原始类型Object

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
} 

Pair的原始类型为:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

从上面的例2中,我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变为Object,所以通过反射我们就可以存储字符串了。

如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。

比如: Pair这样声明的话

public class Pair<T extends Comparable> {}

那么原始类型就是Comparable

1.12 练习:强行修改字符串的内容(掌握概念,了解代码)

在这个练习中,我需要你掌握的是字符串不能修改的真正原因。

字符串,在底层是一个byte类型的字节数组,名字叫做value

private final byte[] value;

真正不能被修改的原因:final和private

final修饰value表示value记录的地址值不能修改。

private修饰value而且没有对外提供getvalue和setvalue的方法。所以,在外界不能获取或修改value记录的地址值。是不是不太明白为甚么?看看下面的文章就好了。相关阅读:如何理解 String 类型值的不可变? - 知乎提问

如果要强行修改可以用反射:

代码示例:(了解)

String s = "abc";
String ss = "abc";
​
Class clazz = s.getClass();
Field field = clazz.getDeclaredField("value");
field.setAccessible(true);
byte[] bytes = (byte[]) field.get(s);
bytes[0] = 100;
​
System.out.println(s);//dbc
System.out.println(ss);//dbc

1.13 练习:反射和配置文件结合动态获取的练习(重点)

需求: 利用反射根据文件中的不同类名和方法名,创建不同的对象并调用方法。

分析:

①通过Properties加载配置文件

②得到类名和方法名

③通过类名反射得到Class对象

④通过Class对象创建一个对象

⑤通过Class对象得到方法

⑥调用方法

代码示例:

public class ReflectDemo9 {
    public static void main(String[] args) throws Exception {
        //1.读取配置文件的信息
        Properties prop=new Properties();
        FileInputStream inputStream=new FileInputStream(
            new File("reflect_property.txt")
        );
        prop.load(inputStream);
​
        String classname = prop.get("classname") + "";
        String methodname = prop.get("methodname") + "";
​
        //2.获取字节码文件对象
        Class clazz = Class.forName(classname);
​
        //3.要先创建这个类的对象
        Constructor con = clazz.getDeclaredConstructor();
        con.setAccessible(true);
        Object o = con.newInstance();
​
        //4.获取方法的对象
        Method method = clazz.getDeclaredMethod(methodname);
        method.setAccessible(true);
​
        //5.运行方法
        method.invoke(o);
    }
}
​
配置文件中的信息:
classname=com.itheima.a02reflectdemo1.Student
methodname=sleep

1.14 利用反射保存对象中的信息(重点)

Teacher类

public class Teacher {
    private String name;
    private double salary;
​
    public Teacher(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

Student类

public class Student {
    private String name;
    private int age;
    private char gender;
    private double height;
    private String hobby;
​
    public Student(String name, int age, char gender, double height, String hobby) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.height = height;
        this.hobby = hobby;
    }
}

利用反射获取对象(s 、t)中的属性信息。将信息存储在磁盘文件中

public class MyReflectDemo {
    public static void main(String[] args) throws Exception {
    /*
        对于任意一个对象,都可以把对象所有的字段名和值,保存到文件中去
    */
       Student s = new Student("小A",23,'女',167.5,"睡觉");
       Teacher t = new Teacher("播妞",10000);
       saveObject(s);
    }
​
    //把对象里面所有的成员变量名和值保存到本地文件中
    public static void saveObject(Object obj) throws Exception {
        //1.获取字节码文件的对象
        Class clazz = obj.getClass();
        //2. 创建IO流
        BufferedWriter bw = new BufferedWriter(new FileWriter("myreflect\\a.txt"));
        //3. 获取所有的成员变量
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            //获取成员变量的名字
            String name = field.getName();
            //获取成员变量的值
            Object value = field.get(obj);
            //写出数据
            bw.write(name + "=" + value);
            bw.newLine();
        }
​
        bw.close();
    }
}

2. 动态代理

2.1 好处

无侵入式的给方法增强功能

2.2 为什么要代理?

现在有一个大明星需要举办一场六安演唱会。大明星发现一场演唱会涉及到的很多事情比如:场地准备、门票的发布和宣传。大明星头疼了,他说他只想唱歌和跳舞。这些杂七杂八的事情我们找一个中介公司来处理。

那么问题又来了,代理中要有哪些方法?才能满足大明星的要求?

答案:代理中的方法一定是大明星需要被代理的方法。比如,大明星现在需要被代理的方法是sing和dance。那么,代理的代码中一样要有相对应的方法sing和dance。在对应的方法中调用大明星的sing和dance

现在明白了吗?那么,问题来了:代理是如何知道大明星有哪些方法需要被代理?

答案:我么通过定义一个接口,接口中的方法都是大明星要被代理的方法。而且,大明星和代理人都要实现该接口。

2.3 上面实例的代码实现

大明星类

public class BigStar implements Star{
    private String name;//大明星名字
​
    public BigStar (String name) {
        this.name = name;
    }
​
    /**
     * 唱歌
     * @param name 歌曲的名字
     */
    public String sing(String name){
        System.out.println(this.name +"正在唱"+name);
        return "谢谢大家来参加我的演唱会";
    }
​
    /**
     * 跳舞
     */
    public void dance(){
        System.out.println(this.name +"正在跳舞"+name);
    }
}

接口

public interface Star {
    String sing(String name);
    void dance();
}

现在到了本次案例的重点:大明星的代理类如何创建?

答案:java.lang.reflect.Proxy类,提供了为对象产生代理对象的方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
//参数一:用于指定用哪个类加载器,去加载生成的代理类
//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
//参数三:用来指定生成的代理对象要干什么事情

类加载器:将类的字节码文件加载到内容中

创建代理

/**
 * 类的作用:创建一个代理
 */
public class ProxyUtil {
    /**
     * 方法作用:给大明星对象创建一个代理
     * @param bigStar 被代理的明星对象
     * @return 给明星创建的代理
     */
    public static Star createProxy(BigStar bigStar){
        Star star = (Star)Proxy.newProxyInstance(
                ProxyUtil.class.getClassLoader(),
                new Class[]{Star.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke (Object proxy, 
                                          Method method, 
                                          Object[] args) throws Throwable {
                        //....省略一万字
                    }
                }
        );
        return star;
    }
}

好了,我们把架子搭好了。现在有必要说明一下:代理类的用法

假设,现在有人想要大明星唱一首歌。他应该完成下面的步骤:

  1. 获取代理对象

    • 代理对象 = ProxyUtil.createProxy(大明星对象)

  2. 调用代理类的唱歌方法

    • 代理对象.唱歌的方法()

    • 好了,有必要说明一下,此时,底层就是去调用invoke方法(invoke方法在上面代码)

创建代理续写

//用来指定生成的代理对象要干什么事情
new InvocationHandler() {
    @Override
    public Object invoke (Object proxy, 
                          Method method, 
                          Object[] args) throws Throwable {
        /**
        * proxy:代理对象
        * method:要运行的方法
        * args:调用方法时,传递的参数
        */
        if("sing".equals(method.getName())){
            System.out.println("准备话筒,收钱");
        }else if("dance".equals(method.getName())){
            System.out.println("准备场地,收钱");
        }
        //去找大明星唱歌和跳舞(调用大明星类中相应的方法)
        //这是在前面反射中提到的方法
        return method.invoke(bigStar,args);
    }
}

测试

public class test {
    public static void main (String[] args) {
        BigStar bigStar = new BigStar("程浩韵");
        Star proxy = ProxyUtil.createProxy(bigStar);
        //唱歌
        String result = proxy.sing("如果爱忘了");
        System.out.println(result);
        //跳舞
        proxy.dance();
    }
}

结果

2.4 代码示例(上面的书写方式不常用到,这里的才是常用的)

接口

public interface Star {
    String sing(String name);
    void dance();
}

大明星类

public class BigStar implements Star{
    private String name;//大明星名字
​
    public BigStar (String name) {
        this.name = name;
    }
​
    public String sing(String name){
        System.out.println(this.name +"正在唱"+name);
        return "谢谢大家来参加我的演唱会";
    }
​
    public void dance(){
        System.out.println(this.name +"正在跳舞");
    }
}

定义一个 JDK 动态代理类

public class DebugInvocationHandler implements InvocationHandler {
    //被代理的对象(前面说的大明星对象)
    private final Object target;
​
    public DebugInvocationHandler (Object target) {
        this.target = target;
    }
​
    @Override
    public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
        /**
         * proxy:代理对象
         * method:要运行的方法
         * args:调用方法时,传递的参数
         */
        if("sing".equals(method.getName())){
            System.out.println("准备话筒,收钱");
        }else if("dance".equals(method.getName())){
            System.out.println("准备场地,收钱");
        }
​
        Object result = method.invoke(target, args);//相当于:target.method(args)
​
        return result;
    }
}

invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。

生成代理类的工厂

//代理类工厂
public class JdkProxyFactory {
    public static Object getProxy(Object target){
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), //目标类的类加载器
                target.getClass().getInterfaces(),//代理需要实现的接口
                new DebugInvocationHandler(target)//代理对象自定义InvocationHandler
        );
    }
}

getProxy():主要通过Proxy.newProxyInstance()方法获取某个类的代理对象

测试

BigStar star = new BigStar("孟晏城");
Star proxy = (Star) JdkProxyFactory.getProxy(star);
proxy.dance();
​
​
proxy.sing(" 因为刚好遇见你 ");

结果

准备场地,收钱
孟晏城正在跳舞
准备话筒,收钱
孟晏城正在唱 因为刚好遇见你 
​
Process finished with exit code 0

2.5 额外扩展

动态代理,还可以拦截方法

比如:

在这个故事中,如果别人让邀请大明星去唱歌,打篮球,代理人就增强功能。

但是如果别人让大明星去扫厕所,经纪人就要拦截,不会去调用大明星的方法。

/*
* 类的作用:创建一个代理
* */
public class ProxyUtil {
    public static Star createProxy(BigStar bigStar){
        public static Object newProxyInstance(ClassLoader loader, 
            Class<?>[] interfaces, InvocationHandler h)
            
        Star star = (Star) Proxy.newProxyInstance(
                ProxyUtil.class.getClassLoader(),
                new Class[]{Star.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, 
                                         Method method, 
                                         Object[] args) throws Throwable {
                        
                        if("cleanWC".equals(method.getName())){
                            System.out.println("拦截,不调用大明星的方法");
                            return null;
                        }
                        //如果是其他方法,正常执行
                        return method.invoke(bigStar,args);
                    }
                }
        );
        return star;
    }
}

2.6 动态代理的练习

动态代码可以增强也可以拦截

对add方法进行增强,对remove方法进行拦截,对其他方法不拦截也不增强

public class MyProxyDemo1 {
    public static void main(String[] args) {
        
        ArrayList<String> list = new ArrayList<>();
        
        List proxyList = (List) Proxy.newProxyInstance(
            
                MyProxyDemo1.class.getClassLoader(),
                new Class[]{List.class},
                new InvocationHandler() {
                    
                    @Override
                    public Object invoke(Object proxy,Method method, 
                                         Object[] args) throws Throwable {
                        
                        //对add方法做一个增强,统计耗时时间
                        if (method.getName().equals("add")) {
                            long start = System.currentTimeMillis();
                            //调用集合的方法,真正的添加数据
                            method.invoke(list, args);
                            long end = System.currentTimeMillis();
                            System.out.println("耗时时间:" + (end - start));
                            //需要进行返回,返回值要跟真正增强或者拦截的方法保持一致
                            return true;
                        }else if(method.getName().equals("remove") && args[0] instanceof Integer){
                            System.out.println("拦截了按照索引删除的方法");
                            return null;
                        }else if(method.getName().equals("remove")){
                            System.out.println("拦截了按照对象删除的方法");
                            return false;
                        }else{
                            //如果当前调用的是其他方法,我们既不增强,也不拦截
                            method.invoke(list,args);
                            return null;
                        }
                    }
                }
        );
​
        proxyList.add("aaa");
        proxyList.add("bbb");
        proxyList.add("ccc");
        proxyList.add("ddd");
​
        proxyList.remove(0);
        proxyList.remove("aaa");
​
​
        //打印集合
        System.out.println(list);
    }
}

本篇文章是我结合黑马阿伟老师的JavaSE课程的笔记和自己的理解写的关于反射和动态代理知识点。如有不对的地方请在评论区指正。

参考:

Java基础常见面试题总结(中) | JavaGuide

Java泛型中的类型擦除详解 - 知乎 (zhihu.com)

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值