JAVA反射机制及动态代理

news2024/11/16 18:32:31

反射机制

反射机制是什么

1、Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后, 再通过class对象进行反编译,从而获取对象的各种信息。
2、Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性, 不需要提前在编译期知道运行的对象是谁。

白话理解一下反射:

  • 我们编译时知道类或对象的具体信息,此时直接对类和对象进行操作即可,无需使用反射(reflection)
  • 如果编译不知道类或对象的具体信息,此时应该如何做呢?这时就要用到 反射 来实现。比如类的名称放在XML文件中,属性和属性值放在XML文件中,需要在运行时读取XML文件,动态获取类的信息
public class Test {
    public static void main(String[] args) throws Exception {
//编码/编译的时候,已经知道要创建哪个类的对象,此时和反射没关系
        //创建对象
        //Animal an = new Dog();
        Animal an = new Cat();
        //操作属性
        an.nickName ="旺财";        an.color = "黑色";
        //执行方法
        an.shout();        an.shout("门口");
        an.run();        System.out.println(an);
        //编码/编译的时候,不知道要创建哪个类的对象,只有根据运//行时动态获取的内容来创建对象
   //使用Properties类读取属性文件,最终得到了类的完整路径字符串
        String className = "com.bjsxt.why.Cat";
        //创建对象
        //Animal an2 = new "com.bjsxt.why.Cat"();
        Class clazz = Class.forName(className);
        Object an2 = clazz.newInstance();
        //操作属性
        //执行方法
    }
}

反射的应用场合

  • 在编译时根本无法知道该对象或类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息
  • 比如:log4j,Servlet、SSM框架技术都用到了反射机制
比如:log4j
   log4j.appender.stdout=org.apache.log4j.ConsoleAppender
   log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
 比如:Servlet
  <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>servlet.HelloServlet</servlet-class>
    </servlet>
 比如 SSM
  <bean id="tm"  
  class="org..jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
   </bean>

反射的原理

下图是类的正常加载过程、反射原理与class对象:

Class对象的由来是将.class文件读入内存,并为之创建一个Class对象。

反射机制的优缺点

1、优点:

在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

2、缺点:

(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;

(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

反射机制常用的类文件:

  • Java.lang.Class;
  • Java.lang.reflect.Constructor;
  • Java.lang.reflect.Field;
  • Java.lang.reflect.Method;
  • Java.lang.reflect.Modifier;

反射的作用

    • 动态创建对象
    • 动态操作属性
    • 动态调用方法

在JDK中,主要由以下类来实现Java反射机制,这些类都位于java.lang.reflect包中

    • Class类:代表一个类
    • Constructor 类:代表类的构造方法
    • Field 类:代表类的成员变量(属性)
    • Method类:代表类的成员方法

反射的入口—Class类

-Class类是Java 反射机制的起源和入口

  • 用于获取与类相关的各种信息
  • 提供了获取类信息的相关方法
  • Class类继承自Object类

-Class类是所有类的共同的图纸

  • 每个类有自己的对象,好比图纸和实物的关系
  • 每个类也可看做是一个对象,有共同的图纸Class,存放类的结构信息,比如类的名字、属性、方法、构造方法、父类和接口,能够通过相应方法取出相应信息

-Class类的对象称为类对象

代码演示:认知Class类

public class TestClass1 {
    public static void main(String[] args) throws Exception {
        //1.获取一个类的结构信息(类对象 Class对象)
        Class clazz = Class.forName("com.bjsxt.why.Dog");
        //2.从类对象中获取类的各种结构信息
        //2.1 获取基本结构信息
        System.out.println(clazz.getName());
        System.out.println(clazz.getSimpleName());
        System.out.println(clazz.getSuperclass());
        System.out.println(Arrays.toString(clazz.getInterfaces()));
        //2.2 获取构造方法
        //只能得到public修饰的构造方法
        //Constructor[] constructors = clazz.getConstructors();
        //可以得到所有的构造方法
        Constructor[] constructors = clazz.getDeclaredConstructors(); 
        System.out.println(constructors.length);
        for(Constructor con :constructors){
            //System.out.println(con.toString());
            System.out.println(con.getName() + "||" + 
                    Modifier.toString(con.getModifiers())+"  ||"
                    + Arrays.toString(con.getParameterTypes()));
        }
        //Constructor con = clazz.getConstructor();//获取无参数构造方法
      //Constructor con = clazz.getConstructor(String.class,String.class);
        Constructor con = 
clazz.getDeclaredConstructor(String.class,String.class);
        System.out.println(con);
        //2.3 获取属性
       //Field[] fields = clazz.getFields();
        Field [] fields = clazz.getDeclaredFields();
        System.out.println(fields.length);
        for(Field f :fields){
            System.out.println(f);
        }
        //Field f = clazz.getField("color");
        //private 默认 protecte public都可以获取,但不包括父类的
        Field f = clazz.getDeclaredField("age");
        System.out.println(f);
        //2.3 获取方法
       //Method[] methods = clazz.getMethods();
        Method [] methods = clazz.getDeclaredMethods();
        for(Method m : methods){
            System.out.println(m);
        }
        //Method m = clazz.getMethod("shout",String.class);
        //Method m = clazz.getMethod("run");//public
        Method m = clazz.getDeclaredMethod("run");
        System.out.println(m);
    }
}

Class类的常用方法:

getFields()—— 获得类的public类型的属性。

getDeclaredFields()—— 获得类的所有属性

getField(String name)—— 获得类的指定属性

getMethods()—— 获得类的public类型的方法

getMethod (String name,Class [] args)—— 获得类的指定方法

getConstrutors()—— 获得类的public类型的构造方法

getConstrutor(Class[] args)—— 获得类的特定构造方法

newInstance()—— 通过类的无参构造方法创建对象

getName()—— 获得类的完整名字

getPackage()—— 获取此类所属的包

getSuperclass()—— 获得此类的父类对应的Class对象

获取一个类的类对象的三种方式:

public class TestClass2 {
    public static void main(String[] args) throws  Exception {
        //1.获取一个类的结构信息(类对象 Class对象)
        // 1.1Class.forName(类的完整路径字符串);
        //Class clazz = Class.forName("java.lang.String");
        //1.2 类名.class
       // Class clazz = String.class;
        //1.3 对象名.getClass()
        String str = "bjsxt";
        Class clazz = str.getClass();
        //Integer in = new Integer(20);
        //2.从类对象中获取类的各种结构信息
        System.out.println(clazz.getName());
        System.out.println(clazz.getSimpleName());
        System.out.println(clazz.getSuperclass());
        System.out.println(Arrays.toString(clazz.getInterfaces()));
    }
}

其中类名.class、对象名.getClass()方式在编码时已经知道了要操作的类,而Class.forName()方式在操作的时候,可以知道,也可以不知道要操作的类。所以当编码时还不知道要操作的具体类,就只能使用Class.forName()方式了。

使用反射创建对象

调用无参数构造方法创建对象

方法1:通过Class的newInstance()方法

  • 该方法要求该Class对象的对应类有无参构造方法
  • 执行newInstance()实际上就是执行无参构造方法来创建该类的实例

方法2:通过Constructor的newInstance()方法

  • 先使用Class对象获取指定的Constructor对象
  • 再调用Constructor对象的newInstance()创建Class对象对应类的对象
  • 通过该方法可选择使用指定构造方法来创建对象

代码示例:通过Class的newInstance()方法

public class TestConstructor1 {
    public static void main(String[] args) throws Exception{
        //不使用反射创建对象
        //Dog dog = new Dog();
        //使用反射创建对象
        //1.获取类的完整路径字符串
        String className = "com.bjsxt.why.Dog";
        //2.根据完整路径字符串获取Class对象信息
        Class clazz = Class.forName(className);
        //3.直接使用Class的方法创建对象
        Object obj = clazz.newInstance();
        System.out.println(obj.toString());
    }
}

代码示例:通过Constructor的newInstance()方法创建对象

public class TestConstructor2  {
    public static void main(String[] args) throws Exception{
        //不使用反射创建对象
        //Dog dog = new Dog();
        //使用反射创建对象
        //1.获取类的完整路径字符串
        String className = "com.bjsxt.why.Dog";
        //2.根据完整路径字符串获取Class对象信息
        Class clazz = Class.forName(className);
        //3.获取无参数构造方法
        Constructor con = clazz.getConstructor();
        //4.使用无参数构造方法来创建对象
        Object obj = con.newInstance();
        System.out.println(obj);
    }
}

使用反射操作属性

通过Class对象的getFields()或者getField()方法可以获得该类所包括的全部Field属性或指定Field属性。Field类提供了以下方法来访问属性

  • getXxx(Object obj):获取obj对象该Field的属性值。此处的Xxx对应8个基本数据类型,如果该属性类型是引用类型则直接使用get(Object obj)
  • setXxx(Object obj,Xxx val):将obj对象的该Field赋值val。此处的Xxx对应8个基本数据类型,如果该属性类型是引用类型则直接使用set(Object obj, Object val)
  • setAccessible(Boolean flag):若flag为true,则取消属性的访问权限控制,即使private属性也可以进行访问

代码示例:使用反射操作属性

public class TestField {
    public static void main(String[] args) throws Exception{
        //不使用反射操作属
//        Dog dog = new Dog();
//        dog.nickName = "旺财";
//        dog.age ="黑色";
//        System.out.println(dog.nickName);
//        System.out.println(dog.color);
        //使用反射操作属性  实际操作中使用反射直接操作属性也不多
        //1.获取类的完整路径字符串
        String className = "com.bjsxt.why.Dog";
        //2.得到类对象
        Class clazz = Class.forName(className);
        //3.使用反射创建对象
        //Object dog = clazz.newInstance();
        Object dog = clazz.getConstructor().newInstance();
        //4.获取属性
        Field f1 =  clazz.getField("color");
       //Field f2 = clazz.getField("age");
        Field f2 = clazz.getDeclaredField("age");
        //5.给属性赋值
        f1.set(dog,"黑色1"); //  dog.color ="黑色";
        f2.setAccessible(true);//突破权限的控制
        f2.set(dog,10);
        //6.输出给属性
        System.out.println(f1.get(dog)); //dog.color
        System.out.println(f2.get(dog)); //dog.age
        System.out.println(dog);
    }
}

使用反射执行方法

  • 通过Class对象的getMethods() 方法可以获得该类所包括的全部方法, 返回值是Method[]
  • 通过Class对象的getMethod()方法可以获得该类所包括的指定方法, 返回值是Method
  • 每个Method对象对应一个方法,获得Method对象后,可以调用其invoke() 来调用对应方法
  • Object invoke(Object obj,Object [] args):obj代表当前方法所属的对象的名字,args代表当前方法的参数列表,返回值Object是当前方法的返回值,即执行当前方法的结果。

代码示例:使用反射执行方法

public class TestMethod {
    public static void main(String[] args) throws Exception{
        //不使用反射执行方法
//        Dog dog = new Dog();
//        dog.shout();
//        int result = dog.add(10,20);
//        System.out.println(result);
        //使用反射执行方法
        //1.获取类的完整路径字符串
        String className = "com.bjsxt.why.Dog";
        //2.得到类对象
        Class clazz = Class.forName(className);
        //3.使用反射创建对象
        //Object dog = clazz.newInstance();
        Object dog = clazz.getConstructor().newInstance();
        //4.获取方法
        Method m1 = clazz.getMethod("shout");
        Method m2 = clazz.getMethod("add",int.class,int.class);
        //5.使用反射执行方法
        m1.invoke(dog);//dog.shout();
        Object result = m2.invoke(dog,10,20);   
        System.out.println(result);
    }
}

参考连接  Java反射机制-十分钟搞懂 - 知乎 (zhihu.com)

 动态代理

动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。

本文主要介绍Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。

由于Java动态代理与java反射机制关系紧密,请读者确保已经了解了Java反射机制。

代理模式

本文将介绍的Java动态代理与设计模式中的代理模式有关,什么是代理模式呢?

代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。

代理模式角色分为 3 种:

Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
RealSubject(真实主题角色):真正实现业务逻辑的类;
Proxy(代理主题角色):用来代理和封装真实主题;

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。

代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理等等。

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
  • 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件

静态代理

我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理

编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl

public interface UserService {
    public void select();   
    public void update();
}

public class UserServiceImpl implements UserService {  
    public void select() {  
        System.out.println("查询 selectById");
    }
    public void update() {
        System.out.println("更新 update");
    }
}

我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用selectupdate之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService

public class UserServiceProxy implements UserService {
    private UserService target; // 被代理的对象

    public UserServiceProxy(UserService target) {
        this.target = target;
    }
    public void select() {
        before();
        target.select();    // 这里才实际调用真实主题角色的方法
        after();
    }
    public void update() {
        before();
        target.update();    // 这里才实际调用真实主题角色的方法
        after();
    }

    private void before() {     // 在执行方法之前执行
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {      // 在执行方法之后执行
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

客户端测试

public class Client1 {
    public static void main(String[] args) {
        UserService userServiceImpl = new UserServiceImpl();
        UserService proxy = new UserServiceProxy(userServiceImpl);

        proxy.select();
        proxy.update();
    }
}

输出

log start time [Thu Dec 20 14:13:25 CST 2018] 
查询 selectById
log end time [Thu Dec 20 14:13:25 CST 2018] 
log start time [Thu Dec 20 14:13:25 CST 2018] 
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018] 

通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。

静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。

如何改进?

当然是让代理类动态的生成啦,也就是动态代理。

为什么类可以动态的生成?

这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

  • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
  • 从网络中获取,典型的应用是 Applet
  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
  • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
  • 从数据库中获取等等

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。

常见的字节码操作类库

这里有一些介绍: java-source.net/open-source…
  • Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
  • ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
  • CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
  • Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
  • ...

实现动态代理的思考方向

为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:

  1. 通过实现接口的方式 -> JDK动态代理
  2. 通过继承类的方式 -> CGLIB动态代理

注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦。

JDK动态代理

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler,我们仍然通过案例来学习编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 invoke 方法中编写方法调用的逻辑处理。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
    Object target;  // 被代理的对象,实际的方法执行者

    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }
    // 调用invoke方法之前执行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // 调用invoke方法之后执行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法,具体步骤可见代码和注释

import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Client2 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        // 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名
        // System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 1. 创建被代理的对象,UserService接口的实现类
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        // 2. 获取对应的 ClassLoader
        ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
        // 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
        Class[] interfaces = userServiceImpl.getClass().getInterfaces();
        // 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
        //     这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
        InvocationHandler logHandler = new LogHandler(userServiceImpl);
        /*
		   5.根据上面提供的信息,创建代理对象 在这个过程中,
               a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
               b.然后根据相应的字节码转换成对应的class,
               c.然后调用newInstance()创建代理实例
		 */
        UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
        // 调用代理的方法
        proxy.select();
        proxy.update();
        
        // 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
        // ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");
    }
}

运行结果

log start time [Thu Dec 20 16:55:19 CST 2018] 
查询 selectById
log end time [Thu Dec 20 16:55:19 CST 2018] 
log start time [Thu Dec 20 16:55:19 CST 2018] 
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018] 

InvocationHandler 和 Proxy 的主要方法介绍如下:

java.lang.reflect.InvocationHandler

Object invoke(Object proxy, Method method, Object[] args)定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

java.lang.reflect.Proxy

static InvocationHandler getInvocationHandler(Object proxy)用于获取指定代理对象所关联的调用处理器

static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)返回指定接口的代理类

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

static boolean isProxyClass(Class<?> cl)返回 cl 是否为一个代理类

代理类的调用过程

生成的代理类到底长什么样子呢?借助下面的工具类,把代理类保存下来再探个究竟(通过设置环境变量sun.misc.ProxyGenerator.saveGeneratedFiles=true也可以保存代理类)

import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;

public class ProxyUtils {
    /**
     * 将根据类信息动态生成的二进制字节码保存到硬盘中,默认的是clazz目录下
     * params: clazz 需要生成动态代理类的类
     * proxyName: 为动态生成的代理类的名称
     */
    public static void generateClassFile(Class clazz, String proxyName) {
        // 根据类信息和提供的代理类名称,生成字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;
        try {
            //保留到硬盘中
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

然后在 Client2 测试类的main的最后面加入一行代码

// 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy");

IDEA 再次运行之后就可以在 target 的类路径下找到 UserServiceProxy.class,双击后IDEA的反编译插件会将该二进制class文件

UserServiceProxy 的代码如下所示:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;

public final class UserServiceProxy extends Proxy implements UserService {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    public UserServiceProxy(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        // 省略...
    }

    public final String toString() throws  {
        // 省略...
    }

    public final void select() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        // 省略...
    }

    public final void update() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("proxy.UserService").getMethod("select");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("proxy.UserService").getMethod("update");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

从 UserServiceProxy 的代码中我们可以发现:

  • UserServiceProxy 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
  • 由于 UserServiceProxy 继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器
  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承
  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m + 数字 的格式命名
  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null); 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstance 的 LogHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

而 LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);       // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }

JDK动态代理执行方法调用的过程简图如下:

代理类的调用过程相信大家都明了了,而关于Proxy的源码解析,还请大家另外查阅其他文章或者直接看源码

CGLIB动态代理

maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select() 和 update()

public class UserDao {
    public void select() {
        System.out.println("UserDao 查询 selectById");
    }
    public void update() {
        System.out.println("UserDao 更新 update");
    }
}

编写一个 LogInterceptor ,继承了 MethodInterceptor,用于方法的拦截回调

import java.lang.reflect.Method;
import java.util.Date;

public class LogInterceptor implements MethodInterceptor {
    /**
     * @param object 表示要进行增强的对象
     * @param method 表示拦截的方法
     * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
     * @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
     * @return 执行结果
     * @throws Throwable
     */
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(object, objects);   // 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

测试

import net.sf.cglib.proxy.Enhancer;

public class CglibTest {
    public static void main(String[] args) {
        DaoProxy daoProxy = new DaoProxy(); 
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Dao.class);  // 设置超类,cglib是通过继承来实现的
        enhancer.setCallback(daoProxy);

        Dao dao = (Dao)enhancer.create();   // 创建代理类
        dao.update();
        dao.select();
    }
}

运行结果

log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao 查询 selectById
log end time [Fri Dec 21 00:06:40 CST 2018] 
log start time [Fri Dec 21 00:06:40 CST 2018] 
UserDao 更新 update
log end time [Fri Dec 21 00:06:40 CST 2018] 

还可以进一步多个 MethodInterceptor 进行过滤筛选

public class LogInterceptor2 implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(object, objects);
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("log2 start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log2 end time [%s] ", new Date()));
    }
}

// 回调过滤器: 在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。
public class DaoFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
        if ("select".equals(method.getName())) {
            return 0;   // Callback 列表第1个拦截器
        }
        return 1;   // Callback 列表第2个拦截器,return 2 则为第3个,以此类推
    }
}

再次测试

public class CglibTest2 {
    public static void main(String[] args) {
        LogInterceptor logInterceptor = new LogInterceptor();
        LogInterceptor2 logInterceptor2 = new LogInterceptor2();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserDao.class);   // 设置超类,cglib是通过继承来实现的
        enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2, NoOp.INSTANCE});   // 设置多个拦截器,NoOp.INSTANCE是一个空拦截器,不做任何处理
        enhancer.setCallbackFilter(new DaoFilter());

        UserDao proxy = (UserDao) enhancer.create();   // 创建代理类
        proxy.select();
        proxy.update();
    }
}

运行结果

log start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao 查询 selectById
log end time [Fri Dec 21 00:22:39 CST 2018] 
log2 start time [Fri Dec 21 00:22:39 CST 2018] 
UserDao 更新 update
log2 end time [Fri Dec 21 00:22:39 CST 2018] 

CGLIB 创建动态代理类的模式是:

  1. 查找目标类上的所有非final 的public类型的方法定义;
  2. 将这些方法的定义转换成字节码;
  3. 将组成的字节码转换成相应的代理的class对象;
  4. 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求

JDK动态代理与CGLIB动态代理对比

JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。

cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK Proxy 的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
  • 代码实现简单。

基于类似 cglib 框架的优势:

  • 无需实现接口,达到代理类无侵入
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能

面试题

描述动态代理的几种实现方式?分别说出相应的优缺点

代理可以分为 "静态代理" 和 "动态代理",动态代理又分为 "JDK动态代理" 和 "CGLIB动态代理" 实现。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object

  • 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
  • 缺点:不同的接口要有不同的代理类实现,会很冗余

JDK 动态代理

为了解决静态代理中,生成大量的代理类造成的冗余;JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象 jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口。

优点:解决了静态代理中冗余的代理实现类问题。

缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。

CGLIB 代理

由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。

缺点:技术实现相对难理解些。

CGlib 对接口实现代理?

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import proxy.UserService;
import java.lang.reflect.Method;

/**
 * 创建代理类的工厂 该类要实现 MethodInterceptor 接口。
 * 该类中完成三样工作:
 * (1)声明目标类的成员变量,并创建以目标类对象为参数的构造器。用于接收目标对象
 * (2)定义代理的生成方法,用于创建代理对象。方法名是任意的。代理对象即目标类的子类
 * (3)定义回调接口方法。对目标类的增强这在这里完成
 */
public class CGLibFactory implements MethodInterceptor {
    // 声明目标类的成员变量
    private UserService target;

    public CGLibFactory(UserService target) {
        this.target = target;
    }
    // 定义代理的生成方法,用于创建代理对象
    public UserService myCGLibCreator() {
        Enhancer enhancer = new Enhancer();
        // 为代理对象设置父类,即指定目标类
        enhancer.setSuperclass(UserService.class);
        /**
         * 设置回调接口对象 注意,只所以在setCallback()方法中可以写上this,
         * 是因为MethodIntecepter接口继承自Callback,是其子接口
         */
        enhancer.setCallback(this);
        return (UserService) enhancer.create();// create用以生成CGLib代理对象
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("start invoke " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("end invoke " + method.getName());
        return result;
    }
}

参考连接 深入理解Java动态代理 - 知乎 (zhihu.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1151020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter 使用 GetX 中遇到的问题

创建了控制器&#xff0c;但是在别的页面中&#xff0c;无法引用控制器里面的某些变量 如下图&#xff1a;后来发现&#xff0c;是命名的问题&#xff0c; 如果是以 _ 下划线开头的变量&#xff0c;那么就无法被引用

(三)docker:Dockerfile构建容器运行jar包

目录结构以及准备的文件 ├── dockerfile │ ├── Dockerfile │ ├── application.properties │ ├── demo.jar │ └── jdk-17.0.9-linux-x64.tar.gz2.Dockerfile内容 FROM ubuntu:latest # JDK存放处 ENV JAVA_DIR/home # 拷贝本地jdk到容器home目录下…

京东数据分析:2023年9月京东洗地机行业品牌销售排行榜

鲸参谋监测的京东平台9月份洗地机市场销售数据已出炉&#xff01; 9月份&#xff0c;洗地机市场的销售额增长。根据鲸参谋电商数据分析平台的相关数据显示&#xff0c;9月京东平台上洗地机的销量为9.2万&#xff0c;销售额将近2.2亿&#xff0c;同比增长约9%。从价格上看&#…

如何使用htmltab库

htmltab是一个用于从HTML表格中提取数据的Python库。它可以将HTML表格转换为Pandas数据框&#xff0c;方便进行数据处理和分析。 要使用htmltab库&#xff0c;首先需要安装htmltab。可以使用pip命令来安装htmltab&#xff0c;命令如下&#xff1a; pip install htmltab 安装完…

[SHCTF 2023 校外赛道] pwn

有19道题这么多,不过基本是入门题,都是在骗新生,看这么容易快来PWN吧! week1 四则计算器 这里用危险函数gets读入有个溢出.而且PIE也没开,地址是固定的.而且有后门.直接溢出到ret写上后门即可. from pwn import *p remote(112.6.51.212, 31473) context(archamd64, log_lev…

【手机端远程连接服务器】安装和配置cpolar+JuiceSSH:实现手机端远程连接服务器

文章目录 1. Linux安装cpolar2. 创建公网SSH连接地址3. JuiceSSH公网远程连接4. 固定连接SSH公网地址5. SSH固定地址连接测试 处于内网的虚拟机如何被外网访问呢?如何手机就能访问虚拟机呢? cpolarJuiceSSH 实现手机端远程连接Linux虚拟机(内网穿透,手机端连接Linux虚拟机) …

Redis(10)| I/O多路复用(mutiplexing)

上文select/epoll 在上文《Redis&#xff08;09&#xff09;| Reactor模式》 思考问题可以使用I/O多路复用技术解决多多客户端TCP连接问题&#xff0c;同时也提到为了解决最早期的UNIX系统select调用存在的四个问题。 select(int nfds, fd_set *r, fd_set *w, fd_set *e, stru…

数据库拓展语句,约束方式和用户管理

拓展语句 删除表内的所有数据 delete truncate drop 1.delete from test&#xff1b; delete删除是一行一行删除&#xff0c;如果表中有自增长列&#xff0c;清空所有记录之后&#xff0c;再次添加内容&#xff0c;会从原来的记录之后继续自增写入 2.truncate table test;…

docker 安装minio,访问地址进不去

文章目录 黑马头条P37docker安装minio文图一、启动后页面一直是加载状态进不去 黑马头条P37docker安装minio文图 一、启动后页面一直是加载状态进不去 通过docker logs -f (容器id)查看日志 通过这个报错信息&#xff0c;得知最近minio 升级&#xff0c;一些启动信息和之前不…

全球运营的游戏公司,实现存储就近访问、提升访问安全和效率

在上一篇文章&#xff08;永远在线的网游公司&#xff0c;如何在线替换开源存储&#xff1f; &#xff09;中&#xff0c;我们介绍了 XSKY星辰天合如何协助游戏公司解决在存储系统建设中遇到的挑战&#xff0c;这家游戏公司需要一直在线&#xff0c;以便为客户提供服务。 通过…

sass相关

1、代码简化 1.1、简化媒介查询 mixin flex{display: flex;justify-content: center;align-items: center; } .header{width: 100%;include flex;//可以这样引用 }//加入参数 mixin flex($layout){display: flex;justify-content: $layout;align-items: $layout; } .header{w…

行情分析——加密货币市场大盘走势(10.30)

目前大饼开始了震荡盘整&#xff0c;可以在这个位置33000-35000短线逢低做多。大饼依然以多头为主&#xff0c;少做空单。目前震荡行情&#xff0c;一直也跌不下去&#xff0c;等待行情到来即可。 目前开始震荡&#xff0c;也是修复指标&#xff0c;现在大饼的价格远离EMA21均线…

【斗破年番】暗杀行动开始,萧炎斩杀负伤,彩鳞心疼霸气回击

【侵权联系删除】【文/郑尔巴金】 深度爆料&#xff0c;《斗破苍穹》年番第69集刚刚更新了&#xff01;在这集剧情中&#xff0c;萧炎和美杜莎筹划了一场暗杀行动&#xff0c;以保障炎盟的安全。他们根据小医仙提供的地图&#xff0c;分别负责击杀慕兰三老和雁落天这两位敌方强…

云服务器安装Hbase

文章目录 1. HBase安装部署2.HBase服务的启动3.HBase部署高可用&#xff08;可选&#xff09;4. HBase整合Phoenix4.1 安装Phoenix4.2 **Phoenix Shell** 操作4.3 表的映射4.4 Phoenix二级索引4.4.1 全局索引&#xff08;global index&#xff09;4.4.2 包含索引(covered index…

SEW MOVIPRO应用模块AMA0801

应用模块AMA0801特点 1)、点动模式2)、试教模式3)、寻参模式4)、定位模式5)、同步模式 其他额外的功能(只要通过端子控制时有效) 1)、平滑过渡功能2)、位置开关功能3)、自动调整功能4)、位置修正功能 基本模式&#xff1a; 点动模式 点动控制电机正、反转运行。 如果…

udp协议/tcp协议

udp和tcp作为传输层的两大重要协议&#xff0c;是众多学习网络编程者不可错过的学习内容&#xff0c;协议的概念想必不用再过多解释&#xff0c;即程序员和程序员之间进行网络通讯时的标准&#xff0c;那么经历了应用层&#xff0c;也就是肉眼能看到、用户能直接操作的层&#…

Spring Cloud之ElasticSearch的学习【详细】

目录 ElasticSearch 正向索引与倒排索引 数据库与elasticsearch概念对比 安装ES、Kibana与分词器 分词器作用 自定义字典 拓展词库 禁用词库 索引库操作 Mapping属性 创建索引库 查询索引库 删除索引库 修改索引库 文档操作 新增文档 查找文档 修改文档 全量…

安卓平板-学习平板、三防工业平板安卓主板方案

近年来&#xff0c; 生活和工业产品的需求呈爆发式增长&#xff0c;学习平板、工业平板和智能设备的出货量正处于快速增长的阶段。尤其是安卓平板智能设备&#xff0c;其增长势头依然迅猛。根据预测&#xff0c;到2024年&#xff0c;中国平板设备的总出货量将会进一步增长。 安…

顺序表(1)

目录 线性表 顺序表Sequential List 静态顺序表 动态顺序表 主函数Test.c test1 test2 test3 test4 头文件&函数声明SeqList.h 头文件 函数声明 函数实现SeqList.c 初始化SLInit 释放销毁SLDestroy 扩容SLCheckCapacity 打印SLPrint 尾插SLPushBack …