当前位置: 首页 > news >正文

JAVA基础之动态代理

1.概述

代理模式是结构型设计模式的一种(23种设计模式),它是底层框架使用较多的一种设计模式(Spring、Mybatis等)。代理模式指的是在某些场景下,对象会找一个代理对象,来辅助自己完成一些工作,这些工作是对原对象行为的控制或增强。举个简单的例子:有一个明星类(Star),日常的主要工作是演戏和唱歌,而她需要一个经济人(StarManager)来帮她代理一些事情,比如演戏和唱歌前的预约、结束后的收费等等。这里就体现了代理模式的简单优势,实现隐藏委托类,同时解耦客户类与委托类,在不修改委托类的基础上,修改客户类来增强功能。本文将基于JAVA语言特点,来分析JAVA语言如何实现代理(静态和动态)。

2.概念及实现方式

2.1 JAVA代理模式

在这里插入图片描述
JAVA代理模式如上图所示:

StarAction(抽象主题角色):定义代理类和真实主题对外的公共方法,也是代理类代理真实主题的方法,这里是sing()和acting()方法;
Star(真实主题角色):真实实现业务逻辑的类,这里是sing()和acting()方法的主要实现逻辑;
StarManager(代理主题角色):代理和封装真实主题的类,这里是对sing()和acting()方法的增强。

JAVA中根据字节码的创建时机将代理分为静态代理和动态代理,区别如下:

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

2.2 静态代理

根据上述案例,编写如下代码:

2.2.1 StarAction

public interface StarAction {

    void sing();

    void acting();
}

2.2.2 Star

@Data
public class Star implements StarAction {

    private String name;

    private Integer age;

    @Override
    public void sing() {
        System.out.println(name + "开始唱歌并热舞!");
        System.out.println(name + "正在激情唱歌并热舞!");
        System.out.println(name + "结束唱歌并热舞!");
    }

    @Override
    public void acting() {
        System.out.println(name + "开始演戏!");
        System.out.println(name + "正在激情演戏!");
        System.out.println(name + "结束演戏!");
    }
}

2.2.3 StarManager

@Data
public class StarManager implements StarAction {

    private Star star;

    public StarManager(Star star) {
        this.star = star;
    }

    @Override
    public void sing() {
        System.out.println("与投资方签约!");
        System.out.println("收唱歌定金!");
        //安排明星唱歌
        star.sing();
        System.out.println("收唱歌尾款!");
    }

    @Override
    public void acting() {
        System.out.println("与投资方签约!");
        System.out.println("收演戏定金!");
        //安排明星唱歌
        star.acting();
        System.out.println("收演戏尾款!");
    }
}

测试类代码如下:

public class Test {

    public static void main(String[] args) {
        Star star = new Star();
        star.setName("刘德华");
        star.setAge(60);

        StarManager starManager = new StarManager(star);
        starManager.sing();
    }
}

运行结果如下:
在这里插入图片描述
从上述代码可知,StarManager类代理了委托类Star类的sing()和acting()方法,在不修改委托类代码的情况下做了一些额外的处理,静态代理的局限在于运行前必须编写好代理类。如果在上述功能前提下,代理类还需要在明星唱歌和跳舞前将明星送过去,等表演结束再接回来,则需要修改代理类如下:

@Data
public class StarManager implements StarAction {

    private Star star;

    public StarManager(Star star) {
        this.star = star;
    }

    @Override
    public void sing() {
        System.out.println("与投资方签约!");
        System.out.println("收唱歌定金!");
        //送明星去唱歌
        before();
        //安排明星唱歌
        star.sing();
        after();
        System.out.println("收唱歌尾款!");
    }

    @Override
    public void acting() {
        System.out.println("与投资方签约!");
        System.out.println("收演戏定金!");
        //送明星去唱歌
        before();
        //安排明星演戏
        star.acting();
        after();
        System.out.println("收演戏尾款!");
    }

    private void before() {
        System.out.println("送明星" + star.getName() + "去唱歌!");
    }

    private void after() {
        System.out.println("演出结束,接明星" + star.getName() + "回来!");
    }

}

很明显在为代理对象添加一些共性行为时,必须显式修改代理类中对应的代理方法,这样做的缺点在于工作量大,而且不够灵活。如果需要统一控制多种对象的行为或共性行为,应该使用动态代理。

2.3 动态代理

动态代理是在程序运行期间由JVM根据反射等机制动态生成,这种情况下的代理类并不是一开始就在JAVA代码中定义的,而是运行时生成。相比于静态代理,动态代理的优势在于很方便地对代理类的方法进行统一处理,而不用修改每个代理类的函数。由2.2.3节可知,若对象中包含更多的行为(比如明星代言、跳舞等其他方法),这时候在每个方法内都要添加before()和after()方法,编写了大量冗余代码。为了避免这种情况出现,可以使用动态代理来实现。JAVA中实现动态代理的方式有两种:

  • JDK动态代理:利用反射机制生成了一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;
  • CGLIB动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

两者的主要区别在于:
JDK代理只能对实现接口的类生成代理;CGLIB是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。

2.3.1 JDK动态代理实现

使用JDK动态代理时,需要定义一个位于代理类与委托类之间的中介类,这个中介类需要实现InvocationHandler接口,接口定义如下:

public interface InvocationHandler {

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

从方法名可知,实现这个接口的类是“调用处理器”。当调用代理类的方法时,此次调用会传送到invoke方法中,代理类对象作为proxy传入,参数method标识了调用了代理类的哪个方法,args为这个方法的调用参数。此次调用过程对代理类的调用就变成了对invoke的调用,这样我们可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。针对2.2.3节需要在表演前送过去和表演后接回来,定义before()和after()方法,在调用代理方法sing()和acting()方法后调用,具体实现如下:

public class StarManagerProxy implements InvocationHandler {

    private Star star;

    public StarAction getInstance(Star star) {
        this.star = star;
        //newProxyInstance包含三个参数:
        //1.loader:代理类的ClassLoder;
        //2.interfaces: 代理类实现接口列表;
        //3.h: 调用处理器,这里也就是当前实现了InvocationHandler接口的类实例
        return (StarAction) Proxy.newProxyInstance(star.getClass().getClassLoader(), star.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(star, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("送明星" + star.getName() + "去唱歌!");
    }

    private void after() {
        System.out.println("演出结束,接明星" + star.getName() + "回来!");
    }
}

测试代码如下:

public class Test {

    public static void main(String[] args) {
        Star star = new Star();
        star.setName("刘德华");
        star.setAge(60);
        StarManagerProxy starManagerProxy = new StarManagerProxy();
        StarAction instance = starManagerProxy.getInstance(star);
        instance.sing();
    }
}

运行结果如下:
在这里插入图片描述
上述代码的主要流程如下:首先通过newProxyInstance()方法获取代理类的实例,拿到实例后便可以调用代理类的方法,对代理类方法的调用主要通过代理类invoke方法进行,在invoke方法中调用代理类的相应方法,并添加自己的处理逻辑。

2.3.2 CGLIB动态代理

由于JDK动态代理只针对实现接口的类,而对于没有实现接口的情况,JDK无法实现动态代理。针对这种情况,CGLIB做了弥补。CGLIB采用了非常底层的字节码技术,通过字节码技术为一个类创建子类,并在子类中使用方法拦截技术拦截所有父类方法的调用,并加入横切逻辑,来实现动态代理。具体实现方式如下:代理类实现MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。同等情况下,CGLIB创建代理对象所花时间远多于JDK创建模式,而且对于final修饰的父类,无法创建其代理对象(final修饰的类无法创建子类)。本文利用CGLIB实现的代理类如下:

public class StarManagerCGLIBProxy implements MethodInterceptor {

    private Star star;

    public Star createProxyInstance(Star star) {
        this.star = star;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(star.getClass());
        enhancer.setCallback(this);
        Star o = (Star) enhancer.create();
        return o;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = method.invoke(star, objects);
        after();
        return result;
    }

    private void before() {
        System.out.println("送明星" + star.getName() + "去唱歌!");
    }

    private void after() {
        System.out.println("演出结束,接明星" + star.getName() + "回来!");
    }


}

测试代码如下:
在这里插入图片描述
CGLIB也可以对接口实现代理(与类实现方式基本一致),这里不再赘述。

2.4 使用场景

动态代理技术在底层框架中使用较多,日常开发中也可根据场景使用。比如对Feign接口请求后获取到的结果进行解析,获取数据(前提下接口返回格式确定)。具体如下:

public class DataServiceHandler implements InvocationHandler {

    // logger
    private Logger logger = LoggerFactory.getLogger(DataServiceHandler.class);

    private Object target;

    private String serviceName;

    public Object getInstance(Object target, String serviceName) {
        this.target = target;
        this.serviceName = serviceName;
        Class<?> clazz = target.getClass();
        Object obj = Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
        return obj;
    }

    @Override
    public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
        logger.debug("代理feign客户端调用服务接口");
        // 第一个参数是target,也就是被代理类的对象;第二个参数是方法中的参数
        String result = null;
        JSONObject resultMap = null;
        try {
            result = (String) method.invoke(target, args);
            resultMap = JSON.parseObject(result);
        } catch (Exception e) {
            StringBuilder detail = new StringBuilder("调用微服务").append(serviceName).append("--业务接口").append(method.getName()).append("异常");
            logger.error(detail.toString(), e);
            throw new VideoCloudException(ErrorContents.ERROR_NUM_600, detail.toString(), e);
        }
        if (resultMap == null) {
            logger.error("返回结果为null");
            throw new VideoCloudException("返回结果为null");
        }
        String dataErrorCode = resultMap.getString("errorCode");
        if (dataErrorCode != null && !dataErrorCode.equals("0")) {
            String dataErrorMsg = resultMap.getString("errorMsg");
            StringBuilder detail = new StringBuilder("调用微服务").append(serviceName).append("--业务接口").append(method.getName()).append("异常:");
            logger.error("调用feign服务{}--业务接口{}, params:{}, error result:{}", serviceName, method.toString(), JSON.toJSON(args), result);
            if (dataErrorMsg != null) {
                throw new InterfaceException(dataErrorCode, dataErrorMsg, detail.toString());
            } else {
                throw new InterfaceException(null, null, detail.toString());
            }
        }
        return resultMap.getString("data");
    }

}

返回类格式为:

@Data
public class ResultData<T> implements Serializable {

    private Integer errorCode;

    private String errorMsg;

    private T data;

    public ResultData() {
    }

    public ResultData(Integer errorCode, String errorMsg) {
        this.errorCode= code;
        this.errorMsg= errorMsg;
    }

    public ResultData(Integer errorCode, String errorMsg, T data) {
        this.errorCode= errorCode;
        this.errorMsg= errorMsg;
        this.data = data;
    }

    public static <T> ResultData<T> success(String message, T data) {
        return new ResultData<>(0, message, data);
    }

    public static <T> ResultData<T> success(T data) {
        return new ResultData<>(0, "success", data);
    }

    public static <T> ResultData<T> success() {
        return new ResultData<>(0, "success", null);
    }

    public static <T> ResultData<T> fail(T data) {
        return new ResultData<>(111, "fail", data);
    }

    public static <T> ResultData<T> fail() {
        return new ResultData<>(111, "fail", null);
    }

}

3.小结

1.JAVA动态代理能帮助实现对客户类的增强,且能够帮助解耦客户类与委托类;
2.JDK动态代理只针对实现接口的类,而对于没有实现接口的情况,JDK无法实现动态代理
3.CGLIB是基于字节码创建代理类子类的方式进行代理,若该类被final修饰,则无法进行代理。
4.JDK代理方式的性能要优于CGLIB代理方式。

4.参考文献

1.https://juejin.cn/post/6844903591501627405
2.https://www.php.cn/java-article-487165.html
3.https://www.bilibili.com/video/BV1Cv411372m?p=193&vd_source=c0ee3c78abb8202795797d12feb40feb

相关文章:

  • 轻量级神经网络算法系列文章-MobileNet v3
  • 聚苯乙烯负载酸性离子液体(P[Vim-PS][HSO4])|活性炭(AC)负载酸性离子液体[Hmim-BS][HSO4]齐岳
  • 视频流PS打包方式详解
  • BIM从业者的焦虑和困惑,你遇到了吗?
  • 携职教育:2022年初级会计考试证书领取流程及所需材料
  • iOS App怎么上架到苹果TestFlight?
  • 自动控制原理6.2---常用校正装置及其特性
  • Android——常用定时器
  • 堆排序-Head Sort
  • 【C++】wav文件解析(兼容性强)
  • 免费查题接口搭建
  • 多目标优化算法|用于全局和工程设计优化的多目标原子轨道搜索 (MOAOS)算法(Matlab代码实现)
  • [C++]:for循环for(int num : nums)
  • 3年测试经验,去面试连25K都拿不到了吗?现在测试这么坑?
  • 网课查题公众号 免费授权搜题接口
  • 【剑指offer】让抽象问题具体化
  • CSS3 变换
  • css系列之关于字体的事
  • docker-consul
  • Docker下部署自己的LNMP工作环境
  • JS创建对象模式及其对象原型链探究(一):Object模式
  • Python 基础起步 (十) 什么叫函数?
  • WePY 在小程序性能调优上做出的探究
  • 记录:CentOS7.2配置LNMP环境记录
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 码农张的Bug人生 - 初来乍到
  • 如何设计一个比特币钱包服务
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 收藏好这篇,别再只说“数据劫持”了
  • 新书推荐|Windows黑客编程技术详解
  • 找一份好的前端工作,起点很重要
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • $L^p$ 调和函数恒为零
  • (3)(3.5) 遥测无线电区域条例
  • (3)nginx 配置(nginx.conf)
  • (39)STM32——FLASH闪存
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (转)LINQ之路
  • (转载)(官方)UE4--图像编程----着色器开发
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • .a文件和.so文件
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .NET CF命令行调试器MDbg入门(一)
  • .NET/C# 使窗口永不获得焦点
  • .net项目IIS、VS 附加进程调试
  • .NET项目中存在多个web.config文件时的加载顺序
  • // an array of int
  • /proc/stat文件详解(翻译)
  • ??myeclipse+tomcat
  • [ MSF使用实例 ] 利用永恒之蓝(MS17-010)漏洞导致windows靶机蓝屏并获取靶机权限
  • [20150321]索引空块的问题.txt
  • [Android]创建TabBar