代理设计模式与 AOP

本文为大家讲解代理模式,包括静态代理的作用和代码实现、动态代理的作用、使用反射实现动态代理的过程,从而理解 AOP 的原理。

代理模式分为:静态代理和动态代理。代理模式实现的功能和我们生活中的代理一样,类似于中介公司。也就是代理对象帮助被代理对象完成功能,被代理对象可以在代理对象已有的功能基础上,扩展代理对象的功能。

比如在已存在的多个具有相同接口的目标类的各个方法上增加一些系统功能,经常会使用到代理模式,例如:方法执行前后的日志打印,计算方法的运行时间、异常处理、事务等等。

一、静态代理

静态代理的介绍与代码实现

下面我们通过一个例子来认识静态代理的作用,假设我们有一个操作数据库的 Dao 服务接口 DaoService ,里面包括查询方法 query() 和更新方法 update(),程序中需要操作数据库的有订单服务 OrderService 类,它实现了 DaoService 接口。

我们思考一个问题,如果需要在查询方法 query() 和更新方法 update() 执行的前后需要加入日志打印功能,也就是说在方法执行前打印一行日志,表示方法开始执行,在方法执行后打印一行日志,表示方法执行完毕。

大家可以思考,以上的问题我们可以怎么解决?

任何人都能想到的最直接的办法就是,在每个实现了 DaoService 接口的类的 query() 方法和 update() 方法的前后加上两行日志就可以了。这种方法其实产生了很多的重复代码,不是一个好的解决办法。如果实现了 DaoService 接口的类有很多,那么我们就会在这些类的 query() 方法和 update() 方法加很多日志代码。

另外一个解决办法就是用静态代理来实现,用一个代理类 DaoServiceProxy 实现 DaoService 接口,让它来对实现了 DaoService 接口的类进行代理,在代理类的query() 方法和 update() 方法的前后加上两行日志就可以了。这样做的好处是不管有多少 DaoService 的实现类,只需要加一个代理类,在代理类的具体方法上加两行日志就可以了,重复代码得到了极大的减少。

示例代码如下:

// DaoService接口
interface DaoService {
  void query();
  void update();
}

// 被代理类OrderService
class OrderService implements DaoService {

  @Override
  public void query() {
    System.out.println("OrderService.query()");
  }

  @Override
  public void update() {
    System.out.println("OrderService.update()");
  }

}

// 代理类
class DaoServiceProxy implements DaoService {
  DaoService dao;

  // 创建代理类对象的时候,实际上传入一个被代理类的对象
  public DaoServiceProxy(DaoService dao) {
    this.dao = dao;
  }

  @Override
  public void query() {
    System.out.println("query()开始执行!");
    dao.query();
    System.out.println("query()执行完毕!");
  }

  @Override
  public void update() {
    System.out.println("update()开始执行!");
    dao.update();
    System.out.println("update()执行完毕!");
  }
}
// 测试
public class TestStaticProxy1 {
  public static void main(String[] args) {
    // 创建被代理类的对象
    OrderService orderService = new OrderService();
    // 创建代理类的对象
    DaoServiceProxy orderServiceProxy = new DaoServiceProxy(orderService);
    orderServiceProxy.query();
    System.out.println("==================");
    orderServiceProxy.update();
  }
}

运行结果:

query()开始执行!
OrderService.query()
query()执行完毕!
==================
update()开始执行!
OrderService.update()
update()执行完毕!

假如我们现在新建了一个用户表,需要开发用户的查询和更新方法的代码,同时要求在方法执行前后输出日志,那么我们就可以非常方便的完成。

只需要写一个用户服务类 UserService 实现 DaoService 接口,在调用 UserService 方法的时候把 UserService 类的对象传递给代理类 DaoServiceProxy 类,然后调用代理类的 query() 方法和 update() 方法,就可以完美的打印出方法调用前后的日志。

// 用户服务类UserService
class UserService implements DaoService {

  @Override
  public void query() {
    System.out.println("UserService.query()");
  }

  @Override
  public void update() {
    System.out.println("UserService.update()");
  }

}

public class TestStaticProxy2 {
  public static void main(String[] args) {
    // 创建被代理类的对象 userService
    UserService userService = new UserService();
    // 创建代理类的对象
    DaoServiceProxy orderServiceProxy = new DaoServiceProxy(userService);
    orderServiceProxy.query();
    System.out.println("==================");
    orderServiceProxy.update();
  }
}

运行结果:

query()开始执行!
UserService.query()
query()执行完毕!
==================
update()开始执行!
UserService.update()
update()执行完毕!

从以上的代码我们可以看出,使用了代理设计模式会使我们实际开发过程中的代码量大幅减少,这就是使用设计模式的威力!

二、动态代理

动态代理介绍与代码实现

静态代理的特点是一个代理类只能为一个接口服务,这样的话程序开发中必然会产生过多的代理类。

比如程序中除了有数据库操作 DaoService 接口,还有支付操作 PayService 接口,PayService 接口里定义了支付方法 pay(),要求在 pay() 方法的执行前后也要加两行日志的输出。

这样的话,不得不再建立一个实现了 PayService 接口的代理类,用来代理实现了 PayService 接口的被代理类的操作。如果还有其他的接口,则需要继续创建代理类,因此最好的解决办法是通过一个动态代理类完成所有接口的方法执行前后的日志输出功能。

在 Java 中实现动态代理机制,需要使用 java.lang.reflect.InvocationHandler 接口以及 java.lang.reflect.Proxy 类。

InvocationHandler 接口的定义如下:

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

在 InvocationHandler 接口中只定义了一个 invoke() 方法,此方法中有 3 个参数:

1、Object proxy:被代理的对象
2、Method method:要调用的方法
3、Object[] args:方法调用时需要传入的参数

Proxy 类是用来创建代理类的,它通过 newProxyInstance() 方法为一个或多个接口动态的生成实现类。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException

1、ClassLoader loader:类加载器
2、Class<?>[] interfaces:获取被代理类的全部接口
3、InvocationHandler h:InvocationHandler 接口的子类的实例

下面我们用动态代理来实现刚才提到的需求场景的日志打印

//PayService接口
interface PayService {
  void pay();
}

//被代理类WeChatPayService
class WeChatPayService implements PayService {

  @Override
  public void pay() {
    System.out.println("WeChatPayService.pay()");
  }
}

// 动态代理类DynamicProxy
class DynamicProxy implements InvocationHandler {

  Object obj; // 实现了接口的被代理类的对象的声明

  // 1、给被代理类的对象实例化,并且返回一个代理类对象,体会一下反射是动态语言的关键
  public Object getInstance(Object obj) {
    this.obj = obj;
    Class<?> clazz = obj.getClass();
    return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
  }

  // 当通过代理类的对象发起对被重写的方法的调用时,都会转换为对如下的invoke方法的调用
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println(method.getName() + "开始执行!");
    // returnVal就是调用被重写的方法的返回值
    Object returnVal = method.invoke(obj, args);
    System.out.println(method.getName() + "执行完毕!");
    return returnVal;
  }
}

// 测试
public class TestDynamicProxy {
  public static void main(String[] args) {
    System.out.println("1、动态代理orderService");
    // 被代理类的对象orderService
    OrderService orderService = new OrderService();
    // 创建一个实现了InvocationHandler接口的类的对象
    DynamicProxy proxy = new DynamicProxy();
    // 调用getInstance()方法,动态的返回一个同样实现了被代理类OrderService类实现的接口DaoService的代理类的对象
    // dao就是代理类对象
    DaoService order = (DaoService) proxy.getInstance(orderService);
    order.query(); // 调用代理类对象的方法就会转换为调用handler类里invoke方法的调用
    System.out.println("==================");
    order.update();

    System.out.println("2、动态代理UserService");
    UserService userService = new UserService();
    DaoService user = (DaoService) proxy.getInstance(userService);
    user.query();
    System.out.println("==================");
    user.update();

    System.out.println("3、动态代理WeChatPayService");
    WeChatPayService weChatPayService = new WeChatPayService();
    PayService weChat = (PayService) proxy.getInstance(weChatPayService);
    weChat.pay();
  }
}

运行结果:

1、动态代理orderService
query开始执行!
OrderService.query()
query执行完毕!
==================
update开始执行!
OrderService.update()
update执行完毕!
2、动态代理UserService
query开始执行!
UserService.query()
query执行完毕!
==================
update开始执行!
UserService.update()
update执行完毕!
3、动态代理WeChatPayService
pay开始执行!
WeChatPayService.pay()
pay执行完毕!

从以上的代码以及运行结果可以看出,动态代理可以对任意的接口实现类进行代理,避免了静态代理需要针对不同的接口开发对应的代理类。

动态代理可以对任何接口的所有方法实现前后增加相同功能的目的,Spring 框架中的 AOP 就是采用了动态代理的设计模式。AOP 切面编程对系统的设计与编码具有非常重要的作用,对于日志、事务、权限校验等可以在系统设计的阶段不予考虑,在设计后通过 AOP 的方式实现。

三、动态代理的实现模式

动态代理的实现模式有两种:

1、用 JDK 实现:代理对象必须实现一个接口,否则无法使用 JDK 自带的动态代理。上面的例子就是用 JDK 实现的动态代理。

2、用 CGLIB 实现:代理对象可以不实现接口,但是代理方法不能用 final 修饰。

用 CGLIB 实现动态代理

JDK 动态代理中提供一个 Proxy 类来创建代理类,而在 CGLib 动态代理中也提供了一个类 Enhancer 来创建代理类。使用 CGLib 创建动态代理类需要在项目中引入 CGLib 的 jar 包。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.5</version>
</dependency>

使用CGLib 实现动态代理,首先需要动态代理类实现 MethodInterceptor 方法拦截器接口,然后通过构造函数传递被代理对象,然后利用 Enhancer 来实例化被代理对象,通过 Enhancer 设置被代理对象的字节码文件、设置回调函数、创建被代理对象,最后覆写 intercept() 方法。

使用CGLib 实现动态代理的示例代码如下:

// CGLIB动态代理类
class CglibProxy implements MethodInterceptor {

  private Object obj; // 被代理对象

  public CglibProxy(Object obj) {
    this.obj = obj;
  }

  // 给目标对象创建一个被代理对象的示例
  public Object getInstance() {
    // 创建Enhancer对象,类似于JDK动态代理的Proxy类
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(obj.getClass()); // 设置被代理对象的字节码文件
    enhancer.setCallback(this);// 设置回调函数
    return enhancer.create();// 创建被代理对象(子类)
  }

  @Override
  public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proyx) throws Throwable {
    System.out.println("cglib动态代理开始");
    Object object = proyx.invokeSuper(obj, objects); // 关键代码
    System.out.println("cglib动态代理结束");
    return object;
  }
}

// 测试
public class TestCglib {
  public static void main(String[] args) {
    System.out.println("1、动态代理orderService");
    DaoService orderService = new OrderService();
    CglibProxy orderProxy = new CglibProxy(orderService);
    OrderService order = (OrderService) orderProxy.getInstance();
    order.query();
    System.out.println("==================");
    order.update();

    System.out.println("2、动态代理UserService");
    DaoService userService = new UserService();
    CglibProxy userProxy = new CglibProxy(userService);
    UserService user = (UserService) userProxy.getInstance();
    user.query();
    System.out.println("==================");
    user.update();

    System.out.println("3、动态代理WeChatPayService");
    PayService payService = new WeChatPayService();
    CglibProxy payProxy = new CglibProxy(payService);
    WeChatPayService weChat = (WeChatPayService) payProxy.getInstance();
    weChat.pay();
  }
}

本文详细介绍了静态代理和动态代理的作用和实现方式,并介绍了动态代理实现的两种方式,在一般的开发中很少会使用到动态代理,但是在编写一些底层代码或者框架代码的时候动态代理模式就比较常用了,掌握动态代理的实现模式是一个程序员走向高级的必备技能。

下一篇文章将会为大家介绍如何使用动态代理模拟 Spring 框架的 AOP 切面编程,实现统计方法执行前后共花费的时间功能,敬请关注.....

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342