Java反射特性总结

Java反射特性的总结

概述

  • 明确一点,Java反射是在运行状态的概念,而不是编译期的概念
  • 元编程的概念

    • 元编程赋予了编程语言更加强大的表达能力,通过编译期间的展开生成代码或者允许程序在运行时改变自身的行为;元编程是一种使用静态代码生成代码的思想,消灭重复的代码,极大地增强编程语言的表达能力
    • 泛型(编译期间展开代码),注解(依靠反射实现,运行时改变自身代码),等实际上都是元编程的概念
    • 编写元程序的语言称之为元语言。被操纵的程序的语言称之为「目标语言」。一门编程语言同时也是自身的元语言的能力称之为「反射」或者「自反」
  • 类的动态加载的概念

    • JVM是按需加载字节码文件(比如在尝试创建类的实例,或者执行类的方法的时候,访问类的成员的时候才会去加载类的字节码文件)的,只有在用到该类了才会尝试去加载其对应的字节码文件并生成对应的Class对象
    • 动态加载之所以对于反射的概念很重要是因为,因为有了动态加载,Java并不是一次性加载所有的字节码文件,从而使得Java程序员可以通过反射手动的编码以控制类的加载,使得整个程序更加灵活
  • JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法对于任意一个对象,都能够调用它的任意一个方法和属性这种动态获取的类信息以及动态调用对象的方法的功能称为 java 语言的反射机制,或者说Java程序中,类是整个程序的基础,由类所产生的一个个对象在程序中发挥着为其设计的功能,但是如果”反过来“将类中的各个成分映射为一个个的对象,就是反射的过程了,如果说类的方法的调用是是Java的正确打开方式的话,那么反射就是Java的后门,只有得到Class,一切都可以获知、调用

    • JVM为每一个被记载的字节码文件产生其对应的唯一的Class实例,包括数组,基本类型也有其对应的Class实例
  • 反射是框架设计的灵魂

    • spring框架中xml配置代替硬编码
    • JDBC使用反射加载数据库的驱动程序
    • springboot中各种注解的使用
    • mybatis
    • 模块化结构,通过反射调用其他模块,解除依赖
    • 动态代理、cglib

Class类

  • Class类是JVM加载Java类字节码文件后生成的一个入口类,Class类实例存储在堆内存中,所谓入口类的意思就是,Java程序可通过获得这个入口类的唯一实例,并调用类中的属性与方法来获取对应的类存储在方法区的元信息

    • Java的反射特性依靠Class类来获取未知的信息
  • 获取Class实例的4种方法

    • 类名.class,此为已知晓具体类的情况(并不常用) 使用此种方式加载获得的类不会被初始化

      • class不是一个属性,而是一个关键字,基础类型也可以以类似的形式调用int.class
      • 需要导入包,才能执行,否则编译时报错,依赖比较大,一般不常用
    • Class.forName(全类名) 内部调用的是forName0这个native方法,默认指定参数为开启类初始化

      • 比较常用,全类名可以通过参数或者配置文件中获得
    • 实例.getClass()获取--无所谓是否执行了类初始化,因为实例业已创建,肯定已经完成了类的加载,必然也经过了类初始化

      • 一般来说已经创建好实例,使用反射的意义就不大了,所以不常用
    • ClassLoader.loadClass(全类名) 注意,通过类加载器来获取Class对象时不会进行类初始化

      // 使用系统类加载器加载自定义的类
      Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.diego.reflect.ReflectDemo");
    • 获取Class实例的过程也需要类的加载,不同的是,执行类初始化的被称作是主动加载,其余的不执行初始化的被称之为被动加载
  • 对于一个已经加载的类,其Class实例是单例存在的,即无论再用什么方式加载,都会得到同一个Class实例,至于类是否经过初始化与Class实例本身无关,如果尝试加载类并且准备进行类初始化,发现类已经加载但并未初始化时,也会随机进行初始化处理

    /**
     * 4种获取Class实例方法的演示
     */
    Class clazz = ReflectDemo.class;
    System.out.println(clazz.getName());
    
    ReflectDemo demo = new ReflectDemo();
    System.out.println(demo.getClass().getName());
    
    Class aClass = Class.forName("com.diego.reflect.ReflectDemo");
    System.out.println(aClass.getName());
    
    Class aClass1 = ClassLoader.getSystemClassLoader().loadClass("com.diego.reflect.ReflectDemo");
    System.out.println(aClass1.getName());
    
    // Class实例是单例的
    // true
    System.out.println(clazz == aClass);
    
    
    // 验证类是否经过初始化可以创建一个类测试
    public class ReflectTest {
    
        public static int var = 122;
        static{
            System.out.println("ReflectTest class initialized");
        }
    
    }
    • 实际上Class的构造器是private的,又因为Class实例是由JVM创建的,所以在Java代码中实际上不能创建JVM实例,JVM也不会重复加载类,这也是Class实例是单例的基础
  • instanceof与Class-----每个class都有属于自己的唯一的Class实例

    Integer n = new Integer(123);
    boolean b1 = n instanceof Integer; // true,因为n是Integer类型
    boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
    
    boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
    boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
    • instanceof会匹配到父类或接口,但是Class实例一定是唯一的
  • Class实例支持泛型,泛型就是Class对应的class类型
  • Class同时具有动态类型的特性,也就是当在多态的环境中获取到的Class实例是运行时类型对应的Class

    public class ChildReflect extends ParentReflect{
    
    }
    public class ParentReflect {
    
    }
    public void method2(ParentReflect o) {
      // 动态编译(动态类型绑定)
      System.out.println(o.getClass().getName());
    }
    
    // com.diego.reflect.ChildReflect
    method2(new ChildReflect());
    • 执行Method的invoke方法时,也能够直接使用成员方法的多态特向,下边有介绍

使用Class实例获取操纵类信息

  • 使用Class实例获取类信息
static void printClassInfo(Class cls) {
  System.out.println("Class name: " + cls.getName()); // 全类名
  System.out.println("Simple name: " + cls.getSimpleName()); // 类名
  if (cls.getPackage() != null) {
    System.out.println("Package name: " + cls.getPackage().getName()); //包名
  }
  System.out.println("is interface: " + cls.isInterface()); //是否是接口
  System.out.println("is enum: " + cls.isEnum()); // 是否是枚举类型
  System.out.println("is array: " + cls.isArray()); // 是否是Array
  System.out.println("is primitive: " + cls.isPrimitive()); //是否是Primitive 也就是判断是否是基础类型
}

printClassInfo("".getClass());
printClassInfo(Runnable.class);
printClassInfo(java.time.Month.class);
// 数组的类名是[Ljava.lang.String [表示是数组,L表示存储的单元是引用类型,其后跟着的就是引用类型的全类名
printClassInfo(String[].class);
// 基本类型也可以获得对应的Class实例
printClassInfo(int.class);
  • 使用Class对象创建对应的类实例

    Integer i = Integer.class.getDeclaredConstructor(int.class).newInstance(12);
    // Integer类没有无参构造器 会报NoSuchMethodException异常
    Integer j = Integer.class.newInstance();
    // 12
    System.out.println(i);
    • 直接使用Class实例调用newInstance方法,只能调用默认的public的无参构造器,一旦有重载的构造方法就会在运行时报错(编译时无法检查出来)
    • 使用getDeclaredConstructor方法获取任意修饰符的构造器函数,并且可以传入任意的构造器的参数

获取构造方法

public static void reflectConstructorTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //1.加载Class对象
        Class clazz = Class.forName("com.diego.reflect.Student");


        //2.获取所有公有构造方法
        System.out.println("**********************所有公有构造方法*********************************");
        Constructor[] conArray = clazz.getConstructors();
        for (Constructor c : conArray) {
            System.out.println(c);
        }


        System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************");
        conArray = clazz.getDeclaredConstructors();
        for (Constructor c : conArray) {
            System.out.println(c);
        }

        System.out.println("*****************获取公有、无参的构造方法*******************************");
        Constructor con = clazz.getConstructor(null);

        //1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
        //2>、返回的是描述这个无参构造函数的类对象。

        System.out.println("con = " + con);
        //调用构造方法
        Object obj = con.newInstance();
        //    System.out.println("obj = " + obj);
        //    Student stu = (Student)obj;

        System.out.println("******************获取符合条件的构造方法,并调用*******************************");
        con = clazz.getDeclaredConstructor(char.class);
        System.out.println(con);
        //调用构造方法
        con.setAccessible(true);//暴力访问(忽略掉访问修饰符)
        obj = con.newInstance('男');
    }
  • 在反射的API中Declared相关的API就是不管修饰符的,但是只能获得本类的组件而不能获得继承的组件对象;而对应的不含Declared的相关API就是只能获得public修饰符修饰的属性与方法等,但是如果可以继承的话,是可以获得从父类继承得到的对象(比如方法属性等,同时注意必须是public的;并且不仅仅包含上一层级父类的组件,而是整个继承树直到Object传下来的组件)
  • 获取构造方法,不需要提供函数名,因为函数名是默认已知的(就是类名)只需要使用构造函数的参数的类型(存在重载)以及是否是public修饰来获得目标构造函数的对象
  • 使用构造函数对象的newInstance方法来调用构造函数获取class实例,私有构造函数调用之前应设置setAccessible(true),以允许访问

    • 私有构造函数调用之前应设置setAccessible(true),可能会设置失败

      • setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对javajavax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全
    • newInstance方法的参数就是构造函数的参数
  • Constructor类与Class类都是支持泛型的,泛型类型就是对应的class类,尤其是在调用Constructor类的newInstance方法时,应注意最好首先获得带泛型的Constructor实例,再调用对应的newInstance方法,否则得到的实例是Object类型需要进行强制转换,看起来不太美观...

    Constructor c = Integer.class.getDeclaredConstructor(int.class);
    Integer integer = c.newInstance(12);
    
    // 丢失类型
    Object instance = clazz.getDeclaredConstructor(char.class).newInstance('男');
    
    // 使用泛型
    Constructor constructor = clazz.getDeclaredConstructor(char.class);
    Student stu = constructor.newInstance('男');
  • 与普通方法一样,也可以通过getModifiers获得修饰符等更加详细的信息
  • 通过getAnnotation等方法获取注解信息,以处理注解

获取类字段

//1.获取Class对象
Class stuClass = Class.forName("com.diego.reflect.Student");
//2.获取字段
System.out.println("************获取所有公有的字段********************");
Field[] fieldArray = stuClass.getFields();
for (Field f : fieldArray) {
  System.out.println(f);
}
System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************");
fieldArray = stuClass.getDeclaredFields();
for (Field f : fieldArray) {
  System.out.println(f);
}
System.out.println("*************获取公有字段**并调用***********************************");
Field f = stuClass.getField("name");
System.out.println(f);
//获取一个对象
Constructor stuCon = stuClass.getConstructor();
Student stu = stuCon.newInstance();
//为字段设置值
f.set(stu, "刘德华"); //为Student对象中的name属性赋值--》stu.name = "刘德华"
//验证
System.out.println("验证姓名:" + stu.name);



System.out.println("**************获取私有字段****并调用********************************");
f = stuClass.getDeclaredField("phoneNum");
System.out.println(f);
f.setAccessible(true);//暴力反射,解除私有限定
f.set(stu, "18888889999");
System.out.println("验证电话:" + stu);
  • 对于类字段,获取到被限制访问的字段,应当是使用反射的目的之一,相当于是突破访问限制,走了后门
  • 对于类的字段采取的操作无非是get与set,无论是执行get还是set都要指定设置的字段所属的对象,这个对象可以使new出来的,也可以是newInstance出来的,当然在反射的使用场景下,应该是newInsatnce生成的对象

    • 同样的对于private属性的修饰,也应该先设置访问权限
  • 可以通过getModifiers获得修饰符等更加详细的信息

    • getType()获取字段的类型,这个类型是Class类型的实例 比如String.class int.class
    • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

      Field f = String.class.getDeclaredField("value");
      int m = f.getModifiers();
      Modifier.isFinal(m); // true
      Modifier.isPublic(m); // false
      Modifier.isProtected(m); // false
      Modifier.isPrivate(m); // true
      Modifier.isStatic(m); // false
  • 通过getAnnotation等方法获取注解信息,以处理注解
  • 对于私有属性的读与写(set与get)在操作之前应setAccessible(true)

获取类成员方法

//1.获取Class对象
Class stuClass = Class.forName("com.diego.reflect.Student");
//2.获取所有公有方法
System.out.println("***************获取所有的”公有“方法*******************");
stuClass.getMethods();
Method[] methodArray = stuClass.getMethods();
for (Method m : methodArray) {
  System.out.println(m);
}
System.out.println("***************获取所有的方法,包括私有的*******************");
methodArray = stuClass.getDeclaredMethods();
for (Method m : methodArray) {
  System.out.println(m);
}
System.out.println("***************获取公有的show1()方法*******************");
Method m = stuClass.getMethod("show1", String.class);
System.out.println(m);
//实例化一个Student对象
Object obj = stuClass.getConstructor().newInstance();
m.invoke(obj, "刘德华");

System.out.println("***************获取私有的show4()方法******************");
m = stuClass.getDeclaredMethod("show4", int.class);
System.out.println(m);
m.setAccessible(true);//解除私有限定
Object result = m.invoke(obj, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参
System.out.println("返回值:" + result);
  • 方法的调用使用invoke,若是静态函数,实例对象传null即可
  • Method类型的对象包含方法的所有的信息,如Field对象一样

    • getName():返回方法名称,例如:"getScore"
    • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
    • getGenericReturnType()同样是得到方法的返回值类型的Class实例,但是是带着泛型的(针对的是返回值是泛型的情况),generic这个单词在Java中就表示泛型相关的

      Method method = Class.class.getMethod("getConstructors");
      // class [Ljava.lang.reflect.Constructor;
      System.out.println(method.getReturnType());
      // java.lang.reflect.Constructor[]
      System.out.println(method.getGenericReturnType());
    • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
    • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。(使用方法见上边的Field对象)
    • getDeclaringClass获取方法所在的类的Class实例
  • invoke调用成员方法时,支持多态

    public class demo {
      public static void main(String[] args) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Method method = Man.class.getDeclaredMethod("demo");
        Student student = new Student();
        // hah
        method.invoke(student);
      }
    }
    class Student extends Man{
      public void demo () {
        System.out.println("hah");
      }
    }
    class Man {
      void demo () {
        System.out.println("lal");
      }
    }

使用反射获取继承、实现的关系

  • 获取父类Class

    Class clazz = Integer.class.getSuperclass();
    Class clazz1 = clazz.getSuperclass();
    Class clazz2 = clazz1.getSuperclass();
    System.out.println(clazz);  //class java.lang.Number
    System.out.println(clazz1); // class java.lang.Object
    System.out.println(clazz2); //null
  • 获取实现的所有接口的Class

    • 因为Java支持多实现,所以返回的是列表

      Class[] clazzs = Integer.class.getInterfaces();
      System.out.println(Arrays.toString(clazzs));
      • 只能得到当前类实现的接口,而不能得到父类实现的接口
      • 如果没有实现任何接口则返回空数组
      • 对于接口类型的Class,如果要活的父接口Class,应该使用getInterfaces,而不是getSuperClass
  • 与instanceof类似的,判断继承或实现关系,判断是否支持向上转型

    // Integer i = ?
    Integer.class.isAssignableFrom(Integer.class); // true,Integer可以赋值给Integer
    // Number n = ?
    Number.class.isAssignableFrom(Integer.class); // true,Integer可以赋值给Number
    // Object o = ?
    Object.class.isAssignableFrom(Integer.class); // true,Integer可以赋值给Object
    // Integer i = ?
    Integer.class.isAssignableFrom(Number.class); // false,Number不能赋值给Integer
    • 对于基本类型来说,只有参数也是完全一样的基本类型Class时才会返回true

反射的重要用途之一——动态代理

代理模式

  • 代理模式就是在不改变原对象的状态下,相当于为原目标对象创建了一个中间代理对象,通过访问这个中间代理对象来间接访问原对象,而代理的过程中又可以加入代理对象自己增强的功能
  • 上述的过程如果发生在静态编译阶段,则是为静态代理模式;如果发生在运行期间,则使用到了反射以创建增强的代理对象,即为动态代理模式

静态代理模式

  • 需要手动为需要的类创建一一对应的代理类,需要在代理类中注入被代理类,接口的变化需要更改被代理类和代理类,不灵活,不常使用
  • 静态代理实现步骤:

    1. 定义一个接口及其实现类;
    2. 创建一个代理类同样实现这个接口(可以使用多态)
    3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情
  • 代码演示

    public interface SmsService {
        String send(String message);
    }
    
    // 被代理类
    public class SmsServiceImpl implements SmsService {
        public String send(String message) {
            System.out.println("send message:" + message);
            return message;
        }
    }
    
    // 代理类
    public class SmsProxy implements SmsService {
    
        private final SmsService smsService;
    
        public SmsProxy(SmsService smsService) {
            this.smsService = smsService;
        }
    
        @Override
        public String send(String message) {
            //调用方法之前,我们可以添加自己的操作
            System.out.println("before method send()");
            smsService.send(message);
            //调用方法之后,我们同样可以添加自己的操作
            System.out.println("after method send()");
            return null;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            SmsService smsService = new SmsServiceImpl();
            SmsProxy smsProxy = new SmsProxy(smsService);
            smsProxy.send("java");
        }
    }

动态代理模式

  • 使用反射在运行时生成代理类的字节码,Spring AOP、RPC 框架的实现都依赖了动态代理,动态代理在日常开发中较少使用,但是是框架中几乎必用的技术

JDK动态代理

  • 最关键的两个对象是newProxyInstanceInvocationHandler

    • 前者是一个方法用来生成一个代理对象,后者是一个接口用于实现代理对象在执行方法时的具体处理逻辑。
    • 通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情
    public static Object newProxyInstance(ClassLoader loader,
                                              Class[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
    {
      ......
    }
    
    
    
    1. loader :类加载器,用于加载被代理对象
    2. interfaces : 被代理类实现的一些接口;

      1. 注意是Class数组,因为可以实现多个接口,所以应该是接口数组
    3. h : 实现了 InvocationHandler 接口的对象;
    public interface InvocationHandler {
    
        /**
         * 当你使用代理对象调用方法的时候实际会调用到这个方法
         */
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
    }

    invoke() 方法有下面三个参数:

    1. proxy :动态生成的代理类
    2. method : 代理类调用的方法
    3. args : 向method 方法传入的参数

    demo案例

    public interface SmsService {
        void send(String message);
    }
    
    public class SmsServiceImpl implements SmsService {
    
        @Override
        public void send(String message) {
    
            System.out.println("发送 " + message + " ...");
    
        }
    
    }
    
    /**
     * @ClassName DebugInvocationHandler
     * @Desc InvocationHandler的实现类
     * 其实可以使用函数式接口,但是由于需要在内部维护一个被代理对象,因此单独设计一个类
     * @Author jialee812@gmail.com
     * @Date 2021/2/18 10:41 上午
     * @Version 1.0
     */
    public class DebugInvocationHandler implements InvocationHandler {
    
        // handle内部维护一个被代理对象,用来在invoke中调用其方法
        private Object object;
    
        public DebugInvocationHandler(Object object) {
            this.object = object;
        }
    
        /**
         * @Author jialee812@gmail.com
         * @Description 代理对象执行方法时都会被转发到此函数来处理
         * @Date 10:43 上午 2021/2/18
         * @Param [proxy, method, args]
         * @return java.lang.Object
         **/
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
            System.out.println("before " + method.getName() + " execute.....");
            Object result = method.invoke(object, args);
            System.out.println("after " + method.getName() + " execute.....");
    
            return result;
        }
    
    }
    
    /**
     * @ClassName JdkProxyFactory
     * @Desc 工厂模式返回代理对象
     * @Author jialee812@gmail.com
     * @Date 2021/2/18 10:46 上午
     * @Version 1.0
     */
    public class JdkProxyFactory {
    
        /**
         * @Author jialee812@gmail.com
         * @Description 参数o是被代理对象
         * @Date 10:48 上午 2021/2/18
         * @Param [o]
         * @return java.lang.Object
         **/
        public static Object getProxyInstance (Object target) {
            return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(),
                    new DebugInvocationHandler(target));
        }
    }
    
    // 测试用例
    public class JdkProxyDemo {
    
        public static void main(String[] args) {
    
            // 强转类型
            SmsService proxyInstance = (SmsService) JdkProxyFactory.getProxyInstance(new SmsServiceImpl());
            proxyInstance.send("hello");
        }
    
    }
    • 使用JDK 动态代理的缺陷就是仍需要手动实现接口,才能将此实现类作为被代理类,如果只是普通的类,没有实现接口,能否实现对其的动态代理呢?使用CGLIB即可
    • 在invoke函数中可以获得生成的代理对象的引用,也就是newProxyInstance返回的家伙,其Class的名字为com.sun.proxy.$Proxy0,而该对象调用方法时又会被转发到invoke函数,所以在invoke函数内部使用该引用调用一些函数时会造成不断的循环回调,直到栈溢出比如

      @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      
              System.out.println("before " + method.getName() + " execute.....");
              Object result = method.invoke(object, args);
              System.out.println("after " + method.getName() + " execute.....");
                      // 以下两个方法都会导致栈溢出
              System.out.println(proxy);
              System.out.println(proxy.hashCode());
              return result;
          }
      • 实际上测试会发现,getClass方法并不会被拦截,因此也就不会造成循环回调以至于出现栈溢出,而除此之外的其余的Object的方法都是会导致栈溢出

CGLIB

  • 使用CGLIB可以代理没有实现接口的类,对于CGLIB的最著名的使用就是:Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
  • CGLIB使用的核心要点是实现MethodInterceptor接口,此接口与InvocationHandler类似,都是用来拦截被代理类的方法的;过 Enhancer类来动态获取被代理类

    public interface MethodInterceptor
    extends Callback{
        // 拦截被代理类中的方法
        public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                                   MethodProxy proxy) throws Throwable;
    }
    • obj :被代理的对象(需要增强的对象)

      • jdk 动态代理中invoke函数的proxy参数是生成的代理对象的引用,不要搞混淆了
    • method :被拦截的方法(需要增强的方法)

      • 可用于获取一些方法的元信息,但是注意不要使用此参数的invoke函数区调用被代理对象的方法
    • args :方法入参
    • methodProxy :用于调用原始方法

      • 注意不要使用method参数像jdk 动态代理中一样调用被代理对象的方法,否则也会最终造成栈溢出
      • 其原因可能在于CGLIB代理的方式是通过生成被代理类的子类来拦截被代理类的方法调用
  • demo

    • 添加CGLIB到依赖中
    
      cglib
      cglib
      3.3.0
    
    // SmsService类,当然也可以是不实现接口的类,这里只是为了复用之前的代码
    public class SmsServiceImpl implements SmsService {
    
        @Override
        public void send(String message) {
    
            System.out.println("发送 " + message + " ...");
    
        }
    
    }
    
    /**
     * @ClassName MethodInterceptor
     * @Desc CGLIB 中的方法拦截器
     * @Author jialee812@gmail.com
     * @Date 2021/2/18 11:52 上午
     * @Version 1.0
     */
    public class DebugMethodInterceptor implements MethodInterceptor {
    
        /**
         * @Author jialee812@gmail.com
         * @Description 方法拦截
         * o是被代理对象
         * method是被拦截的方法
         * args是方法的传入参数
         * methodProxy用于调用原始方法
         * @Date 11:57 上午 2021/2/18
         * @Param [o, method, args, methodProxy]
         * @return java.lang.Object
         **/
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    
            System.out.println("before method " + method.getName());
            // 调用被代理的原方法不用method.invoke
            Object object = methodProxy.invokeSuper(o, args);
            System.out.println("after method " + method.getName());
    
            return object;
        }
    }
    
    /**
     * @ClassName CglibProxyFactory
     * @Desc CGLIB 下获取代理类的工厂方法
     * @Author jialee812@gmail.com
     * @Date 2021/2/18 12:23 下午
     * @Version 1.0
     */
    public class CglibProxyFactory {
    
        // 被代理的对象在CGLIB内部生成,只用传入其Class对象即可
        public static Object getProxy(Class clazz) {
            // 创建动态代理增强类
            Enhancer enhancer = new Enhancer();
            // 设置类加载器
            enhancer.setClassLoader(clazz.getClassLoader());
            // 设置被代理类
            enhancer.setSuperclass(clazz);
            // 设置方法拦截器
            enhancer.setCallback(new DebugMethodInterceptor());
            // 返回创建好的代理类
            return enhancer.create();
        }
    }
    
    /**
     * @ClassName CGLIBDemo
     * @Desc CGLIB 的测试类
     * @Author jialee812@gmail.com
     * @Date 2021/2/18 12:30 下午
     * @Version 1.0
     */
    public class CGLIBDemo {
    
        public static void main(String[] args) {
    
            SmsService service = (SmsService) CglibProxyFactory.getProxy(SmsServiceImpl.class);
            service.send("hello");
    
            // CGLIB 同样也不会拦截getClass方法
            //System.out.println(service.getClass());
    
        }
    
    }
    
    • 调用被代理对象的方法时,不能使用invoke方法,而只能使用invokeSuper方法,否则会陷入循环回调,造成栈溢出

      • 其原因在于:CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用

        • 也正因为上述原因,所以CGLIB不能代理声明为 final 类型的类和方法
    • CGLIB不能代理声明为 final 类型的类和方法,CGLIB代理的对象必须具有默认的构造函数,如果没有重载的构造函数便罢了,如果有重载的构造函数,则一定要把无参的默认构造函数显式的定义出来-------这也是使用spring框架时要注意的事项

静态代理与动态代理的区别

  • 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
  • JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

使用反射的方式获得类路径获得静态资源或配置文件

  • 由于类路径无论是在开发时还是编译打包后都是存在的,可以把一些程序使用的静态资源和配置文件放到类路径下
  • 可以使用Class对象或者是类加载器对象提供的方法加载类路径下的配置文件

    // 使用Class获取时,如果路径前加上/则在路径下的根目录找,如果不加/则在类路径下的同包结构下找也就是在于包的结构一样的文件夹结构中找此文件,找不到就报错
    try(InputStream in = CGLIBDemo.class.getResourceAsStream("/default.properties") ) {
      if(in != null) {
        byte[] data = new byte[128];
        in.read(data);
        System.out.println(Arrays.toString(data));
    
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    
    // 使用classloader获取时,路径前边不能加/
    try(InputStream in = CGLIBDemo.class.getClassLoader().getResourceAsStream("default.properties") ) {
      if(in != null) {
        byte[] data = new byte[128];
        in.read(data);
        System.out.println(Arrays.toString(data));
    
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    • 解析两个同名函数的区别

      • 实际上使用Class或者classloader读取类路径下的资源,就像加载字节码文件一样,不同的类加载器对应的去找自己对应路径下的资源(字节码文件、任意资源)并且同样用到了双亲委派的原则
      • Class.getResourceAsStream

        public InputStream getResourceAsStream(String name) {
          // Add a package name prefix if the name is not absolute Remove leading "/" if name is absolute
          name = resolveName(name);
          // 获取加载该class的classLoader
          ClassLoader cl = getClassLoader0();
          // 为null,表示当前类是系统类,其加载器是引导类加载器
          if (cl==null) {
            // 如果当前类是系统类,则使用系统类加载器(应用类加载器)尝试加载classpath下的资源
            // 继续深入,如果当前的系统类加载器为null,则去引导类加载器的加载目录下去找资源
            // 如果当前系统类加载器不为null,同样的最终还是使用下边的getResource方法去找资源
            return ClassLoader.getSystemResourceAsStream(name);
          }
          // 使用getResource方法加载资源
          return cl.getResourceAsStream(name);
        }
        
        // 典型的使用双亲委派
        // 不断向上委托,直到引导类加载器,引导类加载器肯定找不到,于是扩展类加载器调用findResource去找,也找不到,返回null,最后回归到系统类(应用类)加载器调用findResource方法,最终在自己负责的classpath路径下找到
        
        public URL getResource(String name) {
          URL url;
          if (parent != null) {
            url = parent.getResource(name);
          } else {
            url = getBootstrapResource(name);
          }
          if (url == null) {
            url = findResource(name);
          }
          return url;
        }
      • ClassLoader.getResourceAsStream

        // 使用ClassLoader加载资源时,直接双亲委派找
        public URL getResource(String name) {
          URL url;
          if (parent != null) {
            url = parent.getResource(name);
          } else {
            url = getBootstrapResource(name);
          }
          if (url == null) {
            url = findResource(name);
          }
          return url;
        }
        • 对于直接使用类加载器来说,如果加了/则最中使用应用类加载器在classpath中是无法找到的,不加则可以
  • 如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

    Properties props = new Properties();
    // 先从类路径下读取配置文件
    props.load(inputStreamFromClassPath("/default.properties"));
    // 从程序根目录读取配置文件,可以覆盖类路径中的默认的配置文件中的配置,有重复的则覆盖,未重复的则保留
    props.load(inputStreamFromFile("./conf.properties"));

总结

  • 反射机制的优缺点

    • 优点: 运行期类型的判断,动态加载类,提高代码灵活度
    • 缺点:

      • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多
      • 安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患
  • 反射是框架设计的灵魂的理解

    • 在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制

      举例:

      1. 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;

        public class ConnectionJDBC {  
        
          /** 
             * @param args 
             */  
          //驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中  
          public static final String DBDRIVER = "com.mysql.jdbc.Driver";  
          //连接地址是由各个数据库生产商单独提供的,所以需要单独记住  
          public static final String DBURL = "jdbc:mysql://localhost:3306/test";  
          //连接数据库的用户名  
          public static final String DBUSER = "root";  
          //连接数据库的密码  
          public static final String DBPASS = "";  
        
        
          public static void main(String[] args) throws Exception {  
            Connection con = null; //表示数据库的连接对象  
            Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现 
            con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库  
            System.out.println(con);  
            con.close(); // 3、关闭数据库  
          }  
      2. Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系

        1. Spring 通过 XML 配置模式装载 Bean 的过程(即便是spring的纯注解的方式使用的实际上还是反射机制):

          1. 将程序内所有 XML 或 Properties 配置文件加载入内存中
          2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
          3. 使用反射机制,根据这个字符串获得某个类的Class实例
          4. 动态配置实例的属性
        2. Spring这样做的好处是:

          • 不用每一次都要在代码里面去new或者做其他的事情
          • 以后要改的话直接改配置文件,代码维护起来就很方便了
          • 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现
        public class BeanFactory {
               private Map beanMap = new HashMap();
               /**
               * bean工厂的初始化.
               * @param xml xml配置文件
               */
               public void init(String xml) {
                      try {
                             //读取指定的配置文件
                             SAXReader reader = new SAXReader();
                             ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                             //从class目录下获取指定的xml文件
                             InputStream ins = classLoader.getResourceAsStream(xml);
                             Document doc = reader.read(ins);
                             Element root = doc.getRootElement();  
                             Element foo;
                            
                             //遍历bean
                             for (Iterator i = root.elementIterator("bean"); i.hasNext();) {  
                                    foo = (Element) i.next();
                                    //获取bean的属性id和class
                                    Attribute id = foo.attribute("id");  
                                    Attribute cls = foo.attribute("class");
                                   
                                    //利用Java反射机制,通过class的名称获取Class对象
                                    Class bean = Class.forName(cls.getText());
                                   
                                    //获取对应class的信息
                                    java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
                                    //获取其属性描述
                                    java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
                                    //设置值的方法
                                    Method mSet = null;
                                    //创建一个对象
                                    Object obj = bean.newInstance();
                                   
                                    //遍历该bean的property属性
                                    for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {  
                                           Element foo2 = (Element) ite.next();
                                           //获取该property的name属性
                                           Attribute name = foo2.attribute("name");
                                           String value = null;
                                          
                                           //获取该property的子元素value的值
                                           for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
                                                  Element node = (Element) ite1.next();
                                                  value = node.getText();
                                                  break;
                                           }
                                          
                                           for (int k = 0; k < pd.length; k++) {
                                                  if (pd[k].getName().equalsIgnoreCase(name.getText())) {
                                                         mSet = pd[k].getWriteMethod();
                                                         //利用Java的反射极致调用对象的某个set方法,并将值设置进去
                                                         mSet.invoke(obj, value);
                                                  }
                                           }
                                    }
                                   
                                    //将对象放入beanMap中,其中key为id值,value为对象
                                    beanMap.put(id.getText(), obj);
                             }
                      } catch (Exception e) {
                             System.out.println(e.toString());
                      }
               }
              
               //other codes
        }
    • 使用反射突破编译检查的限制,比如使用反射越过泛型检查

      ArrayList strList = new ArrayList<>();
      strList.add("aaa");
      strList.add("bbb");
      
      //    strList.add(100);
      //获取ArrayList的Class对象,反向的调用add()方法,添加数据
      Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
      //获取add()方法
      Method m = listClass.getMethod("add", Object.class);
      //调用add()方法
      m.invoke(strList, 100);
      
      //遍历集合
      for(Object obj : strList){
        System.out.println(obj);
      }
      • Java中的泛型是假泛型,只在编译器提供类型检查与类型强转,但是运行时泛型都会被擦除为Object类型,所以通过反射在运行时可以直接向String list中查入Object类型的数据

参考

你可能感兴趣的