行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • 【设计模式】各个击破单例模式的8种写法
  • 【设计模式】策略模式之“这不就是if-else吗”
  • 【设计模式】工厂系列-FactoryMethod,AbstractFactory,Spring IOC
  • 【设计模式】只需体验三分钟,你就会跟我一样了解Facade和Mediator模式
  • 【设计模式】代理模式那些事儿:静态代理,动态代理,JDK的动态代理,cglib,Spring AOP
    • 引言
    • 静态代理
      • 代理
    • 动态代理
    • JDK的动态代理
      • JDK动态代理原理分析
    • cglib
    • Spring AOP
    • 小结
  • 【设计模式】通俗易懂版责任链模式
  • 【设计模式】模板模式,学会它咱也写出优雅健壮的代码!
  • 【设计模式】好玩的原型模式:Prototype
  • 【设计模式】1分钟整明白什么是Builder建造者模式
  • 设计模式
行百里er
2020-08-09
目录

【设计模式】代理模式那些事儿:静态代理,动态代理,JDK的动态代理,cglib,Spring AOP

作者:行百里er

博客:https://chendapeng.cn (opens new window)

提示

这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!

吹NB不负责:这可能是你从未见过的全新版本!

# 引言

《雪地里的小画家》

下雪啦,下雪啦!

雪地里来了一群小画家。

小鸡画竹叶,小狗画梅花,

小鸭画枫叶,小马画月牙。

不用颜料不用笔,

几步就成一幅画。

青蛙为什么没参加?

他在洞里睡着啦。

还记得上小学时候的这篇课文吗?这是我记忆深刻的一篇语文课文,哈哈,在这里提出来让大家也回忆一下小学的故事。

这里面提到了小鸡,小狗,小马,小鸭,青蛙,他们都会在雪地里画画,我们以这些小动物为对象,来说明一些问题吧。

# 静态代理

这些会画画小动物我们抽象出一个 画家 Painter 接口来,让小动物实现 Painter ,完成 paint() 方法。

小画家 Painter

public interface Painter {
    void paint();
}
1
2
3

小狗 Puppy 画梅花

public class Puppy implements Painter {
    @Override
    public void paint() {
        System.out.println("小狗画梅花");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

小马 Pony 画月牙

public class Pony implements Painter {
    @Override
    public void paint() {
        System.out.println("小马画月牙");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

两个就够了,其他几个小画家就不模拟了,手动捂脸~

老师 Teacher 想要看Pony画画:

public class Teacher {
    public static void main(String[] args) {
        new Pony().paint();
    }
}
1
2
3
4
5

运行结果:

小马画月牙

Process finished with exit code 0
1
2
3

因为画的方法里有 随机睡x秒 的业务处理逻辑, Teacher 现在想知道具体睡了多少秒,怎么办呢?

这还不简单,在 paint() 方法中加开始、结束时间,然后相减就可以了:

public class Pony implements Painter {
    @Override
    public void paint() {
        //加上时间记录,计算业务处理运行的时间
        long start = System.currentTimeMillis();
        System.out.println("小马画月牙");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("paint 画画耗时:" + (end - start) + "毫秒");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当然,小狗 Puppy 的 paint() 方法也要加这一段。

老师 Teacher 的问题又来了,他还想让画画的时候记录下日志,那么可以做如下修改:

public class Pony implements Painter {
    @Override
    public void paint() {
        //加上日志记录
        System.out.println("日志:开始作画");
        //加上时间记录,计算业务处理运行的时间
        long start = System.currentTimeMillis();
        System.out.println("小马画月牙");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("日志:画完了");
        System.out.println("paint 画画耗时:" + (end - start) + "毫秒");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看出,如果想要Pony在画画的时候添加一些诸如 记录执行时间 , 记录日志 这样的动作的时候,就要在实现paint()方法的时候添加时间、日志这些东西。

但是,这不合理呀,我Pony明明只需要处理画画的逻辑就行了啊!也简单,把时间处理、日志处理这些东西交给别人去做,可以把别人看成代理,这些代理分别持有paint()方法,在代理内部实现画画之外的事情。

# 代理

Teacher将来只和代理打交道,所以代理必须也“会画画”的业务,除此之外,才是代理处理特殊的业务。

so,代理可以看成是 具有额外功能的Painter ,那就也让他实现Painter接口,并且持有具体小画家(比如Pony)对象(因为代理需要会画画)

处理时间的代理 TimeProxy

public class TimeProxy implements Painter {

    private Pony pony;
    
    public TimeProxy(Pony pony) {
        this.pony = pony;
    }

    @Override
    public void paint() {
        long start = System.currentTimeMillis();
        //调用小马画画
        pony.paint();
        long end = System.currentTimeMillis();
        System.out.println("paint 画画耗时:" + (end - start) + "毫秒");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这时的 Pony 开心了,只处理自己的逻辑即可,去掉时间、日志:

public class Pony implements Painter {
    @Override
    public void paint() {
        System.out.println("小马画月牙");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

来,Teacher走一遍,让代理给我办事:

public class Teacher {
    public static void main(String[] args) {
        new TimeProxy(new Pony()).paint();
    }
}
1
2
3
4
5

运行结果:

小马画月牙
paint 画画耗时:3221毫秒

Process finished with exit code 0
1
2
3
4

大家想想,这样写有什么缺陷没有?

有的,这里只是持有了Pony的对象,也就是说这个代理只能代理Pony画画的时间处理,事实上,这个TimeProxy也能代理Puppy等其他小画家的,那如何做呢?

把具体的Pony对象换成抽象的Painter,Teacher想看谁画画就给代理传哪个画家就行了!

改一下TimeProxy:

public class TimeProxy implements Painter {
    private Painter painter;

    public TimeProxy(Painter painter) {
        this.painter = painter;
    }

    @Override
    public void paint() {
        long start = System.currentTimeMillis();
        painter.paint();
        long end = System.currentTimeMillis();
        System.out.println("paint 画画耗时:" + (end - start) + "毫秒");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这次调用小狗Puppy来画:

new TimeProxy(new Puppy()).paint();
1
小狗画梅花
paint 画画耗时:2152毫秒

Process finished with exit code 0
1
2
3
4

very ok 了!别急,来把日志的代理也加进去。

public class LogProxy implements Painter {
    private Painter painter;

    public LogProxy(Painter painter) {
        this.painter = painter;
    }

    @Override
    public void paint() {
        System.out.println("日志:开始作画");
        painter.paint();
        System.out.println("日志:画完了");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

现在想一下,Teacher该怎么调用这两个代理,既能打印运行时间,又能打印处理日志,还能画画?

我们看一下代理的构造方法,他里面传的是抽象的画家,并不是具体的,而代理本身也是一种特殊的画家-代理本身是实现Painter这个接口的,所以调用的时候可以把代理作为参数传递到另一个代理!!!

public class Teacher {
    public static void main(String[] args) {
        //new TimeProxy(new Puppy()).paint();
        new TimeProxy(new LogProxy(new Puppy())).paint();
    }
}
1
2
3
4
5
6

运行:

日志:开始作画
小狗画梅花
日志:画完了
paint 画画耗时:8489毫秒

Process finished with exit code 0
1
2
3
4
5
6

既有日志处理,又有时间处理,还有画画本身的逻辑处理,大功告成!

静态代理

上面的例子诠释了一种设计模式-代理模式,这是一种静态代理模式。

# 动态代理

从前面的例子我们可以看到,静态代理只能作为某一特定的接口的代理,比如前面的TimeProxy只能代理Painter。

像这种记录执行时间的操作,应该可以应用于所有对象的方法上,具有普遍性,如果要实现把TimeProxy使用到别的地方,其他Object,该怎么做呢?

分离代理行为与被代理对象,使用jdk的动态代理。

# JDK的动态代理

jdk-proxy

jdk的 Proxy 类来自于 java.lang.reflect 包,没错,就是大名鼎鼎的 反射机制 ,反射是根据已经编译好的二进制字节码来分析类的属性和方法,只要给我一个 .class 我就能分析出他的内容。

上代码:

public class Teacher {
    public static void main(String[] args) {
        Pony pony = new Pony();
        Painter painter = (Painter) Proxy.newProxyInstance(
                Pony.class.getClassLoader(),
                Pony.class.getInterfaces(),//new Class[]{Painter.class}
                new TimeProxyHandler(pony));
        painter.paint();
    }
}
1
2
3
4
5
6
7
8
9
10

Proxy.newProxyInstance有三个参数,第一个是被代理类的类加载器,第二个是实现的接口数组,也可以写成:

new Class[]{Painter.class}
1

重点是第三个参数,该参数是一个InvocationHandler,动态代理方法在执行时,会调用InvocationHandler类里面的invoke方法去执行。

类TimeProxyHandler的具体实现:

public class TimeProxyHandler implements InvocationHandler {
    private Pony pony;

    public TimeProxyHandler(Pony pony) {
        this.pony = pony;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object o = method.invoke(pony, args);
        long end = System.currentTimeMillis();
        System.out.println("执行耗时:" + (end - start) + "毫秒");
        return o;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行Teacher.main()运行结果:

小马画月牙
执行耗时:7881毫秒

Process finished with exit code 0
1
2
3
4

以上我们是用JDK的动态代理可以分离代理行为和被代理的对象,这里的Pony可以换成其他对象。

我的main方法里只调用了painter.paint();啊,怎么连执行耗时:7881毫秒这句话也打印出来了呢?

# JDK动态代理原理分析

运行结果打印了执行耗时:7881毫秒,说明程序必然运行了TimeProxyHandler的invoke方法,我们来分析一下下面这句

Painter painter = (Painter) Proxy.newProxyInstance(
                Pony.class.getClassLoader(),
                //new Class[]{Painter.class}
                Pony.class.getInterfaces(),
                new TimeProxyHandler(pony));
1
2
3
4
5

Proxy.newProxyInstance这一句创建了一个中间类,我们通过如下手段System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")把它弄出来看看:

public class Teacher {
    public static void main(String[] args) {
        Pony pony = new Pony();
        //将proxy内部调用invoke方法 生成的中间类 保存下来
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        Painter painter = (Painter) Proxy.newProxyInstance(
                Pony.class.getClassLoader(),
                new Class[]{Painter.class},
                new TimeProxyHandler(pony));
        painter.paint();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

再次运行,发现项目目录多了这个:

打开 $Proxy0 看看,就能明白个差不多了

public final class $Proxy0 extends Proxy implements Painter {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
1
2
3
4
5

jdk帮我们生成的 $Proxy0 继承 Proxy 实现 Painter

static {
    try {
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m3 = Class.forName("com.xblzer.dp.proxy.dynamicproxy.Painter").getMethod("paint");
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    } catch (NoSuchMethodException var2) {
        throw new NoSuchMethodError(var2.getMessage());
    } catch (ClassNotFoundException var3) {
        throw new NoClassDefFoundError(var3.getMessage());
    }
}

...

public final void paint() throws  {
    try {
        super.h.invoke(this, m3, (Object[])null);
    } catch (RuntimeException | Error var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当我们 main 里面调用 painter.paint() 时,实际上执行了 super.h.invoke(this, m3, (Object[])null) ,这里的 m3 :

m3 = Class.forName("com.xblzer.dp.proxy.staticproxy.Painter").getMethod("paint");
1

jdk动态代理

# cglib

引入Spring相关依赖包,org.springframework.cglib

cglib底层也是基于asm实现的,并且它不需要实现任何接口。

来看效果:

/**
 * cglib-code generate library
 * cglib实现动态代理不需要实现接口
 * 底层用的也是asm
 * @author 行百里者
 */
public class Main {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Pony.class);
        enhancer.setCallback(new TimeMethodInterceptor());
        Pony pony = (Pony) enhancer.create();
        pony.paint();
    }
}

class TimeMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println(o.getClass().getSuperclass().getName());
        System.out.println("before...");
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("after");
        return result;
    }
}

class Pony {
    public void paint() {
        System.out.println("小马画月牙");
        //随机睡10s以内,假装这是处理业务逻辑
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

程序运行结果:

# Spring AOP

现在我们知道了,动态代理可以对任何方法的任何地方切入代理所执行的逻辑,比如执行时间,记录日志,处理事务等。

我们可以在Pony的paint()方法执行前切入before(),在执行后切入after(),也就是说可以在指定的点切入代理所要做的事情,这就是简单的面向切面了。

Spring AOP就是面向切面,AOP是Spring的核心之一。

下面用代码演示一下,AOP是怎么切入代理处理逻辑的。

Spring配置文件app_aop.xml:

<bean id="pony" class="com.xblzer.dp.proxy.springaop.Pony"></bean>
<bean id="logProxy" class="com.xblzer.dp.proxy.springaop.LogProxy"></bean>

<aop:config>
    <aop:aspect id="log" ref="logProxy">
        <aop:pointcut id="onpaint" expression="execution(void com.xblzer.dp.proxy.springaop.Pony.paint())" />
        <!-- 在Pony.paint()之前执行logProxy的before()方法 -->
        <aop:before method="before" pointcut-ref="onpaint"/>
        <!-- 在Pony.paint()之后执行logProxy的after()方法 -->
        <aop:before method="after" pointcut-ref="onpaint"/>
    </aop:aspect>
</aop:config>
1
2
3
4
5
6
7
8
9
10
11
12

LogProxy

public class LogProxy {
    public void before() {
        System.out.println("日志:开始作画");
    }

    public void after() {
        System.out.println("日志:画完了");
    }
}
1
2
3
4
5
6
7
8
9

Pony还是那个Pony,不赘述。

使用:

public class Teacher {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("app_aop.xml");
        Pony pony = (Pony) ctx.getBean("pony");
        pony.paint();
    }
}
1
2
3
4
5
6
7

运行结果:

日志:开始作画
日志:画完了
小马画月牙

Process finished with exit code 0
1
2
3
4
5

Spring AOP就是这么方便!!!

# 小结

代理模式应用得非常广泛,大到一个系统框架、企业平台,小到代码片段、事务处理,用到代理模式的概率是非常大的。

有了AOP大家写代理就更加简单了,有类似Spring AOP这样非常优秀的工具,拿来主义即可!

另外,我们看源代码,特别是调试时,只要看到类似 $Proxy0 这样的结构,我们不妨打开它看看,这样能够帮助我们更容易理解动态代理。


首发公众号 行百里er ,欢迎老铁们关注阅读指正。

#设计模式
上次更新: 2022/10/06, 18:00:06
【设计模式】只需体验三分钟,你就会跟我一样了解Facade和Mediator模式
【设计模式】通俗易懂版责任链模式

← 【设计模式】只需体验三分钟,你就会跟我一样了解Facade和Mediator模式 【设计模式】通俗易懂版责任链模式→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式