设计模式之代理模式(Proxy)

代理模式

需求: 有一辆坦克 , 想要记录坦克的移动时间和移动记录

原始代码如下

package com.cyc.design.proxy.v01;

import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

interface Movable {
    void move();
}

方案一

通过在move的执行前后添加时间记录

package com.cyc.design.proxy.v02;

import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢? benchmark
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        long start = System.currentTimeMillis();

        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void main(String[] args) {
        new Tank().move();
    }
}

interface Movable {
    void move();
}

**问题:**方法虽然简单 ,但是却是要修改源码,如果不修改源码呢?

方案二

使用继承的方式,新建Tank2继承Tank类, 实现move接口, 在执行Tank类的move方法前和方法后, 添加时间记录

package com.cyc.design.proxy.v04;

import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Tank2().move();
    }
}

class Tank2 extends Tank {
    @Override
    public void move() {
        long start = System.currentTimeMillis();
        super.move();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

interface Movable {
    void move();
}

**问题:**熟悉了设计模式之后,应该记得要慎用继承,因为 继承耦合度太高,不够灵活,现在知识记录了坦克的运行时间,如果要记录坦克的行走记录,坦克发射的炮弹呢,难道还要写多个类去继承吗?

方案三

使用代理模式

新建TankTimeProxy同样实现moveAble接口,里面聚合了一个tank,记录一个起始时间和结束时间,中间调用的是tank的move方法。这种方式看起来就像是一个代理人,例如茅台酒代理人,买茅台酒,并不是去向酒厂就买,而是通过茅台酒代理人(经销商)去买,代理人再去向茅台厂购买。

package com.cyc.design.proxy.v05;

import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TankTimeProxy(new Tank()).move();
    }
}

class TankTimeProxy implements Movable {

    Tank tank;

    public TankTimeProxy(Tank tank) {
        this.tank = tank;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();
        tank.move();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

interface Movable {
    void move();
}

**问题:**这个代理只能代理坦克的移动时间。

方案四

同样新建一个TankLogProxy代理,记录坦克的行进日志

package com.cyc.design.proxy.v06;

import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TankTimeProxy().move();
    }
}
//时间代理
class TankTimeProxy implements Movable {
    Tank tank;
    @Override
    public void move() {
        long start = System.currentTimeMillis();
        tank.move();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}
//行进日志代理
class TankLogProxy implements Movable {
    Tank tank;
    @Override
    public void move() {
        System.out.println("start moving...");
        tank.move();
        System.out.println("stopped!");
    }
}

interface Movable {
    void move();
}

问题: 如何既能记录行进时间的日志又能记录行进日志的?

方法: 使用嵌套, 方向没错, 但是两个代理类里面都聚合了tank对象, 又怎么能将两个代理类聚合在一起呢?

方案五

两个代理不再代理具体的tank类,而是代理moveAble接口,这样的话,便可实现以上需求。

这也就是静态代理

在这里插入图片描述

package com.cyc.design.proxy.v07;


import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 *
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        new TankLogProxy(
                new TankTimeProxy(
                       new Tank()
                )
        ).move();
    }
}

class TankTimeProxy implements Movable {
    Movable m;

    public TankTimeProxy(Movable m) {
        this.m = m;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();
        m.move();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

class TankLogProxy implements Movable {
    Movable m;

    public TankLogProxy(Movable m) {
        this.m = m;
    }

    @Override
    public void move() {
        System.out.println("start moving...");
        m.move();
        long end = System.currentTimeMillis();
        System.out.println("stopped!");
    }
}

interface Movable {
    void move();
}

*问题: ** 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?

方案六(动态代理)

分离代理行为与被代理对象

package com.cyc.design.proxy.v08;


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 * v08:如果有stop方法需要代理...
 * 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
 * (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
 * 分离代理行为与被代理对象
 * 使用jdk的动态代理
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Tank tank = new Tank();

        //reflection 通过二进制字节码分析类的属性和方法

        Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
                new Class[]{Movable.class}, //tank.class.getInterfaces()
                new LogHander(tank)
        );

        m.move();
    }
}

class LogHander implements InvocationHandler {

    Tank tank;

    public LogHander(Tank tank) {
        this.tank = tank;
    }
    //getClass.getMethods[]
    /**
     * @param proxy 生成的代理对象, 也就是那个m
     * @param method 要代理的方法
     * @param args 方法需要的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method " + method.getName() + " start..");
        Object o = method.invoke(tank, args);
        System.out.println("method " + method.getName() + " end!");
        return o;
    }
}


interface Movable {
    void move();
}
package com.cyc.design.proxy.v08;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;

/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 * v08:如果有stop方法需要代理...
 * 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型 Object
 * (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
 * 分离代理行为与被代理对象
 * 使用jdk的动态代理
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Tank tank = new Tank();

        //reflection 通过二进制字节码分析类的属性和方法

        Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
                new Class[]{Movable.class}, //tank.class.getInterfaces()
                new LogHander(tank)
        );

        m.move();
    }
}

class LogHander implements InvocationHandler {

    Tank tank;

    public LogHander(Tank tank) {
        this.tank = tank;
    }
    //getClass.getMethods[]
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method " + method.getName() + " start..");
        Object o = method.invoke(tank, args);
        System.out.println("method " + method.getName() + " end!");
        return o;
    }
}


interface Movable {
    void move();
}

查看输出结果:

method move start..
Tank moving claclacla...
method move end!

这里使用到了JDK的动态代理, 点进去newProxyInstance

在这里插入图片描述

第一个参数表示, 需要哪一个ClassLoader将new 出来的代理对象加载到内存,第二个参数,表示这个代理对象需要实现哪些接口。第三个参数,Invocationhandler,调用处理器,指的是,被代理对象的方法被调用的时候, 需要做什么处理。

**问题: ** 根据代码, 观察发现, 并没有地方调用LogHander的invoke方法, method move start…和method move end!是如何输出的呢? 要弄清楚这个问题, 继续往下研究

方案七(动态代理)



/**
 * 问题:我想记录坦克的移动时间
 * 最简单的办法:修改代码,记录时间
 * 问题2:如果无法改变方法源码呢?
 * 用继承?
 * v05:使用代理
 * v06:代理有各种类型
 * 问题:如何实现代理的各种组合?继承?Decorator?
 * v07:代理的对象改成Movable类型-越来越像decorator了
 * v08:如果有stop方法需要代理...
 * 如果想让LogProxy可以重用,不仅可以代理Tank,还可以代理任何其他可以代理的类型
 * (毕竟日志记录,时间计算是很多方法都需要的东西),这时该怎么做呢?
 * 分离代理行为与被代理对象
 * 使用jdk的动态代理
 *
 * v09: 横切代码与业务逻辑代码分离 AOP
 * v10: 通过反射观察生成的代理对象
 * jdk反射生成代理必须面向接口,这是由Proxy的内部实现决定的
 */
public class Tank implements Movable {

    /**
     * 模拟坦克移动了一段儿时间
     */
    @Override
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Tank tank = new Tank();

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");//jdk 1.8
//        System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles","true"); jdk 1.8以上

        Movable m = (Movable)Proxy.newProxyInstance(Tank.class.getClassLoader(),
                new Class[]{Movable.class}, //tank.class.getInterfaces()
                new TimeProxy(tank)
        );

        m.move();

    }
}

class TimeProxy implements InvocationHandler {
    Movable m;

    public TimeProxy(Movable m) {
        this.m = m;
    }

    public void before() {
        System.out.println("method start..");
    }

    public void after() {
        System.out.println("method stop..");
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //Arrays.stream(proxy.getClass().getMethods()).map(Method::getName).forEach(System.out::println);

        before();
        Object o = method.invoke(m, args);
        after();
        return o;
    }

}

interface Movable {
    void move();
}

在这里插入图片描述

在当前项目目录下 , 会生成该代理对象的类, idea自动将其反编译,

查看

在这里插入图片描述

在这里插入图片描述

观察代码 , 发现他生成了move方法, 以为他调用动态代理时, 指定了被代理对象实现movable接口, 所以, 这里生成了move方法, 点击去Proxy类, 发现InvocationHandler就是上图中指定的当前LogProxyHandler

所以原理为: 当我们new代理类对象的时候, InvocationHandler传进去, 调用super.h就被指定为, 当m.move方法执行时 , 首选调用LogProxyHandler

JDK动态代理逻辑图

在这里插入图片描述

源码分析

  • 从 Proxy.newProxyInstance点进去

在这里插入图片描述

  • 重点是下面的getProxyConstructor这个方法,

在这里插入图片描述

  • 点击进入getProxyConstructor

在这里插入图片描述

proxyCache.sub(intf).computeIfAbsent 这句话的意思是 , 每次生成代理的时候 , 如果这个代理已经在代理缓存池中存在 , 则直接从缓存池中拿。 computeIfAbsent , 意为 : 如果没有,则通过计算得出该代理。再往下, 有proxyBuild的build方法,这才是在去生成代理,点击进入build方法,

在这里插入图片描述

  • 点击进入defineProxyClass

在这里插入图片描述

  • 点击进入generateProxyClass

在这里插入图片描述

  • gen.generateClassFile() 是直接操作编译生成的字节码文件,点击进入

在这里插入图片描述

  • 在经过一系列的操作后,在methods.add(pm.generateMethod());将方法加入进去。

在这里插入图片描述

点击进入method.add方法

在这里插入图片描述

在这里插入图片描述

最终进入到asm, 归根结底 , JDK动态代理是又asm实现

ASM是一个通用的Java字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。 ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。 ASM提供与其他Java字节码框架类似的功能,但专注于性能。 因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用

JDK动态代理缺陷在于被代理的类必须实现某个接口

CGLIB动态代理

cglib 全称Code Generation Library ,是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。

代码实现

package com.cyc.design.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

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

/**
 * CGLIB实现动态代理不需要接口
 */
public class Main {
    public static void main(String[] args) {
        //增强器
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(Tank.class);
        //设置回调
        enhancer.setCallback(new TimeMethodInterceptor());
        Tank tank = (Tank)enhancer.create();
        tank.move();
    }
}

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());
        //输出 com.cyc.design.cglib.Tank, 说明cglib动态代理, 生成的是被代理对象的一个子类
        //这么看来 ,cglib也有缺陷 如果被代理对象是final类型,那么cglib就无法动态代理该类
        //cglib底层, 使用的也是asm
        System.out.println("before");
        Object result = null;
        result = methodProxy.invokeSuper(o, objects);
        System.out.println("after");
        return result;
    }
}

class Tank {
    public void move() {
        System.out.println("Tank moving claclacla...");
        try {
            Thread.sleep(new Random().nextInt(10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}



输出 com.cyc.design.cglib.Tank, 说明cglib动态代理, 生成的是被代理对象的一个子类,这么看来 ,cglib也有缺陷 如果被代理对象是final类型,那么cglib就无法动态代理该类
同样,和JDK动态代理一样,cglib底层, 使用的也是asm。

面对final类型的类 , asm就可以实现器动态代理,因为asm在读取完成器二进制码文件后, 可以对其进行修改,此时,可以删除器final关键字,便可对其实现动态代理。

spring aop

代码演示:

package com.cyc.design.spring.v1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * spring aop test
 */

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
        Tank t = (Tank)context.getBean("tank");
        t.move();
    }
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <bean id="tank" class="com.cyc.design.spring.v1.Tank"/>
    <bean id="timeProxy" class="com.cyc.design.spring.v1.TimeProxy"/>

    <aop:config>
        <!--ref="timeProxy" 表示要切入的类是id为timeProxy指定的TimeProxy-->
        <aop:aspect id="time" ref="timeProxy">
            <!--切点位置-->
            <aop:pointcut id="onmove" expression="execution(void com.cyc.design.spring.v1.Tank.move())"/>
            <!--执行前,调用before方法-->
            <aop:before method="before" pointcut-ref="onmove"/>
            <!--执行后,调用after方法-->
            <aop:after method="after" pointcut-ref="onmove"/>
        </aop:aspect>
    </aop:config>

</beans>

实现方式: 当Tank.move执行前 , spring 发现在配置文件中有该方法的切点, 并且有执行前和执行后要执行的方法。

这种配置文件的方式,如果需要进行切面的类很多,那么维护配置文件就是一件很麻烦的事。由此引出了下面的基于注解的方式。

基础注解的AOP

package com.cyc.design.spring.v2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * spring aop test
 */

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app_auto.xml");
        Tank t = (Tank)context.getBean("tank");
        t.move();
    }
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

    <aop:aspectj-autoproxy/>

    <bean id="tank" class="com.cyc.design.spring.v2.Tank"/>
    <bean id="timeProxy" class="com.cyc.design.spring.v2.TimeProxy"/>


</beans>

Spring

  • AOP+IOC
  • bean工厂+灵活装配+动态行为拼接(织入), 成就了spring在java框架中的一哥地位
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

意田天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值