Java笔记··By/蜜汁炒酸奶

漫谈代理模式

定义与目的

代理模式为某对象提供一种代理(一个替身或者占位符),从而控制对这个对象的访问。

原文:Provide a surrogate or placeholder for another object to control access to it.

多数写设计模式的书中都如此直译描述代理模式的定义与作用,而在《设计模式之美》中做了另一番阐述:

在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

感觉后者站在开发者角度看问题,相对来说更容易理解。
代理模式主要目的是控制访问,而非加强功能。在代理类中附加的是与原始类无关的功能,将非功能性附加需求与业务功能解耦,放到代理中统一处理,方便开发人员只关注业务功能方面的开发。而装饰器模式是对功能的增强,装饰器附加的是根原始类相关的增强功能。

应用场景

常用的使用场景有:

  • 非功能性需求开发:监控、统计、限流、事务、日志等需求。
  • 远程代理:RPC框架也可以看作是一种远程代理模式。Java本身提供了RMI相关jar包用于远程调用,WebService是RPC的一种实现。
  • 缓存代理:为开销大的结果提供暂时存储。如通过请求参数,当存在缓存标识则从缓存中直接获取数据返回,反之从数据库等处获取数据并处理后返回。如Spring中的 @Cacheable 方法。

在《Head First 设计模式》中,基于访问控制,做了另一种细分,在此列出来可作了解,大致包含如下,:

  • 远程代理:控制访问远程对象。可以作为另一个JVM上对象的本地代表,调用代理方法,利用网络转发到远程执行,并且结果通过网络返回给代理,再由代理转给调用的客户。在Java中有RMI用于实现远程调用,WebService是RPC框架的一种实现。
  • 虚拟代理:控制访问创建开销大的资源。在真正需要时才创建,创建过程中由虚拟代理代替对象,创建后将请求直接委托给对象。
  • 保护代理:基于权限控制对资源的访问,如鉴权,也可以归到上面的非功能性需求开发。
  • 防火墙代理:控制网络资源的访问,用于保护主题免受侵害。常出现在公司的防火墙系统。
  • 写入时复制代理:虚拟代理的变体,通过延迟对象的复制,直到客户端真的需要时的方式用来控制对象的复制。实现可参考Java的 CopyOnWriteArrayList 相关。
  • 智能引用代理:当主题被引用时,进行额外的动作。类似上面的非功能性需求开发。如通过Spinrg AOP拦截添加相关功能。
  • 同步代理:多线程中为主题提供安全的访问。常出没于 JavaSpaces , 为分布式环境内的潜在对象集合提供同步访问控制。(这块接触不多,不是特别了解,有需要的自己查找相关资料)
  • 复杂隐藏代理:用来隐藏一个类的复杂度,并进行访问控制。有时还也称为外观代理。与外观模式的区别是,外观模式只提供另一组接口。(这块接触不多,不是特别了解,有需要的自己查找相关资料)

代理模式的UML类图

代理模式的UML类图如下:
umlproxybasea.png
由上图可见,代理模式一般包括3个角色:

  1. 抽象主题角色(ISubject):负责声明真实主题与代理的共同接口方法。可以是接口或者抽象类。
  2. 真实主题角色(RealSubject):也称被代理类,负责执行系统的真实业务逻辑。
  3. 代理主题角色(SubjectProxy):也称代理类,由于内部持有真实主题角色的引用,所以可以完全代理真实主题角色,同时可以在调用真实对象的方法前后增加一些新的处理代码。

某些情况下一个对象不适合或不能直接引用另一个类,而代理对象(代理主题角色)可以在客户端与目标对象(真实主题对象)之间起到中介作用。

通用写法

下面是代理模式的通用写法:

// 1.创建抽象主题角色
public interface ISubject {
    public void doSomeThing();
}
// 2. 创建真实主题角色
public class RealSubject implements ISubject{
    @Override
    public void doSomeThing() {
        PrintUtill.println("真正的对象做一些事情");
    }
}
// 3.创建代理主题角色
public class SubjectProxy implements ISubject{
    private ISubject subject;
    // 初始化时传入真实主题角色的引用
    public SubjectProxy(ISubject subject) {
        this.subject = subject;
    }

    @Override
    public void doSomeThing() {
        before();
        this.subject.doSomeThing();
        after();
    }

    private void before() {
        PrintUtill.println("真正对象执行操作之前的附加功能>>>>>>>>>");
    }

    private void after() {
        PrintUtill.println("真正对象执行操作之后的附加功能>>>>>>>>>");
    }

}
// 4. 客户端--用于调用测试
public class SubejctClient {
    public static void main(String[] args) {
        SubjectProxy subjectProxy = new SubjectProxy(new RealSubject());
        subjectProxy.doSomeThing();
    }
}
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
39
40
41
42

通过代码可见这是基于接口的实现方式,也是静态代理的通用实现方式。
代理模式除了基于接口实现,还可以基于继承实现:

  1. 基于接口的实现:参照基于接口而非实现编程的设计思想,将原始类对象替换为代理类对象的时候,为了让代码改动尽量少,故代理类和原始类需要实现相同的接口。
  2. 基于继承的实现:如果原始类(即被代理类)没有实现接口,且不是我们开发的维护的(如来自第三方类库),无法直接修改原始类,给它重新定义一个接口。针对这种无法直接修改原始类的外部类的修改,一般采用继承的方式。

动态代理

在通用实现中,我们看到了静态代理的写法。同时可以看出这种直接创建业务代码的代理类还是存在一些问题的:

  1. 需要在代理类中将被代理类中的所有方法都重新实现一遍,并且为每个方法添加类似的代码逻辑。
  2. 若需要添加附加功能不止一个,需要对每个被代理类都重新创建一个代理类。

浙江导致项目中类型数量成倍增加,从而增加了代码维护成本,且每个代理类中的代码都是些模板式的“重复”代码,也增加了不必要的开发成本。
基于上述问题,我们可以引入动态代理(Dynamic Proxy):
不事先为被代理类创建代理类,而是在运行时动态创建对应的代理类,然后在系统中用代理类代替被代理类。
Java中存在两种动态代理实现,JDk动态代理与CGLIB动态代理。

  1. JDk动态代理,可变相当作是一种基于接口的实现,被代理类必需实现相应接口,因为最终代理类需要实现相应接口。具体实现依赖Java的反射语法。
  2. CGLIB动态代理,可变相看作基于继承的实现,最终实现的虚拟类会继承被代理类。实现上利用ASM开源包,通过修改被代理类的字节码生成子类。

在《深入理解JAVA虚拟机》(第三版)中关于动态代理的“动态”的描述如下:

动态代理中所说的“动态”,是针对使用Java代码实现编写了代理类的“静态”代理而言的,它的优势是不在于省去了编写代理类那一点编码工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
–9.2.3

JDK动态代理

关键涉及两个:Proxy(可理解为调度器)和 InvocationHandler(调度处理器)。
Proxy 通过反射创建动态代理类,其代理类会继承Proxy类。
InvocationHandler 是一个接口,里面只有一个invoke方法。该接口用于实现代理的行为,即用于提供被代理类方法调用发生时所需附加的功能。
当代理的方法被调用时,便会被转发给 InvocationHandler 接口的具体实现类,调用 invoke 方法。此方法中既有附加的新功能,又有被代理类的方法调用。
这里以在支付服务中新增日志为例,具体代码实例如下:
(1) 基础支付类

// 支付接口,
public interface PayService {
    void pay();
    void pay(int a);
}
// 微信支付实现
public class WXPayService implements PayService{
    @Override
    public void pay() {
        PrintUtill.println("微信支付>>>>>>WXPayService>>>>>>>>>pay>>>>>>>>>>>");
    }

    @Override
    public void pay(int a) {
        PrintUtill.println("微信支付>>>>>>WXPayService>>>>>>>>>pay>>>>>>>>>>>"+a);
    }
}
// 客户端
public class LoggerDynamicClient {
    public static void main(String[] args) {
      	// 用于生成在运行时产生的代理类--非必需
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        // 创建日志调度处理器
        LoggerDynamicProxyHandler proxy =  new LoggerDynamicProxyHandler();
      	// 为 微信支付 添加日志
        PayService pay = (PayService) proxy.getInstance(new WXPayService());
        pay.pay();
        PrintUtill.printlnRule();
        // 为 用户服务 添加日志 -实现与支付服务类似。
        BaseService userService = (BaseService) proxy.getInstance(new UserService());
        userService.add();
	// showProxyClass();
    }
    // 代理类文件生成方式一,可指定要生成的代理类以及代理类名称
    public static void showProxyClass() {
        String path = "./$Proxy0.class";
        // 生成字节码的方法,在运行时产生一个描述代理类的字节码byte[]数组。
        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",
                WXPayService.class.getInterfaces());
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(path);
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

(2) 代理实现

public class LoggerDynamicProxyHandler implements InvocationHandler {
    private SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-DD hh:mm:ss");
    private Object target;
		
		// 返回一个实现了接口,并且代理了真实对象实例行为的对象
    public Object getInstance(Object target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        // newProxyInstance 初始化代理,基于被代理者的类加载器、实现的接口,以及当前代理类
        // 返回一个实现了PayService接口的,并且代理了new WXPayService()实例行为的对象
        return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
    }
		// 用于实现代理的行为
		// proxy:动态生成的匿名代理类
		// method:调用的方法
		// args:真实主题类method的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object obj = method.invoke(target,args);
        after();
        return obj;
    }

    private void before() {
        PrintUtill.println("日志动态代理开始>>>>>>>>>>>>>" + sdf.format(System.currentTimeMillis()) + ">>>>>>>>>>>");
    }

    private void after() {
        PrintUtill.println("日志动态代理完成>>>>>>>>>>>>>" + sdf.format(System.currentTimeMillis()) + ">>>>>>>>>>>");
    }
}
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

通过在上述mian函数中添加如下一句话,可查看在运行时产生的代理类。

// 用于生成在运行时产生的代理类--非必需
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
1
2

实现原理

JDK动态代理采用字节重组,重新生成对象来代替被代理对象,以达到动态代理的目的。JDK动态代理生成对象的方式如下:
(1) 通过反射获取被代理对象的引用以及它的所有接口。
(2) 通过Proxy类重新生成一个新类,同时新的类实现被代理类实现的所有接口。
(3) 动态生成Java代码,新加的业务逻辑方法由一定的逻辑代码调用(如InvocationHandler的实现类)。
(4) 编译新生成的Java类代码.class文件
(5) 重新加载到JVM中运行。
以上过程就是字节码重组。其中(2)-(4)并不是先生成.java文件再生成.class文件,大致的生成过程是根据Class文件的格式规范去拼装字节码。

CGLIB动态代理

CGLIB动态代理需要引入单独Jar包,具体版本根据自己需要引入,这里仅作参考:

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.7</version>
        </dependency>
1
2
3
4
5

CGLIB动态代理的实现类似JDK的动态代理,都需要实现相应的调度处理器接口。
这里依旧以上述支付服务添加日志为例,原则上不需要抽象主题角色,仅需创建真实主题角色即可。这里仅展示CGLIB的关键实现部分。

// 客户端
public class LoggerCGlibProxyClient {
    public static void main(String[] args) {
        LoggerCGlibProxyInterceptor proxy = new LoggerCGlibProxyInterceptor();
        PayService pay = (PayService) proxy.getInstance(WXPayService.class);
        pay.pay();
    }
}
// 调度处理器接口实现
public class LoggerCGlibProxyInterceptor implements MethodInterceptor {
    public Object getInstance(Class target) {
      	// 创建一个字节码增强器,可以用来为无接口的类创建代理
        Enhancer enhancer = new Enhancer();
      	// 设置要代理的业务类(即:为下面生成的代理类指定父类)
        enhancer.setSuperclass(target);
      	// 设置回调方法实现类:调用被代理类的方法时,会通过调用该实现类的intercept的方法,从而实现代理的行为。
        enhancer.setCallback(this);
      	// create方法生成Target的代理类,并返回代理类的实例
        return enhancer.create();
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, 
      MethodProxy proxy) throws Throwable {
        System.out.println("windcoder.com日志开始...");
        //代理类调用父类的方法
        proxy.invokeSuper(obj, args);
        System.out.println("windcoder.com日志结束...");
        return null;
    }
}
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

CGLIB之所以比JDK执行代理方法的效率高。是因为其是用了FastClass机制。该原理简单来说是:

  1. 为代理类和被代理类个生成一个类,该类会为代理类或者被代理类的方法分配一个index(int类型,类似索引)。
  2. 将index当作入参,FastClass可以直接定位到相应方法并直接调用执行,省去了反射调用。

查看CGLIB生成代理类的方式,可在main函数中添加如下代码

// 设置输出目录,方便之后查看CGLIB生成的class---第二个参数是路径,可自行修改
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./tmp/");
1
2

JDk与CGLIB两种动态代理比较

  1. JDK 实现了被代理对象的接口,CGLIB 继承了被代理对象。
  2. JDK 和 CGLIB 均在运行时生成字节码。JDK 直接榭 Class 字节码,CGLIB 通过 ASM 框架生成 Class 字节码。CGLIB 实现更复杂,生成代理类比JDk效率低。
  3. JDk 通过反射机制调用代理方法,CGLIB 通过 FastClass 机制直接调用方法,CGLIB执行效率比JDK高。
  4. CGLIB 无法代理 final 修饰的类。

总结

静态代理与动态代理区别

  1. 静态代理需要手动完成代理操作,若被代理类新增方法,则代理类需同步增加,违背开闭原则。
  2. 动态代理采用在运行时动态生成代码字节码的方式,没有了对被代理类扩展的限制,遵循开闭原则。
  3. 若动态代理要对目标类的增强逻辑进行扩展,结合策略模式,只需新增策略类即可,无需修改代理类的代码。

代理类优点

  1. 代理模式能将代理对象与被代理对象分离。在一定程度上降低了系统的耦合性,扩展性好。
  2. 可以起到保护目标对象的作用(即控制访问)。
  3. 可以增强被代理类的功能(如增加一些非功能性需求的功能)。

代理模式的缺点

  1. 代理模式会造成系统设计中类的数量增加。
  2. 在客户端增加一个代理对像,会导致处理请求的速度变慢。
  3. 增加了系统的复杂性(适用于多数设计模式)。

代理模式暂时写这些,相关具体的实现原理并没深入,可自行查找相关资料了解。代理模式在框架中使用率较高,比如Spring的AOP底层就是基于动态代理实现的,Spring 中代理的选择原则如下:

  1. 当 Bean 有实现接口时,使用 JDk 动态代理。
  2. 当 Bean 没有实现接口时, 选择CGLIB 动态代理。
  3. 可通过配置强制使用 CGLIB 。

关于代理模式之前也写过几次,有兴趣可以参考看一下:

  1. Java代理1 代理和动态代理的基础与使用
  2. Java代理2 动态代理的实现原理分析
  3. Java代理3:二刷代理

若想深入了解Spring AOP的内容,若自己阅读源码难度较大,推荐看一下《小马哥讲Spring AOP编程思想》

JK_SpringAop.png

参考资料:

  1. 设计模式之美
  2. 《Head First 设计模式》
  3. 《设计模式就该这么学》
  4. 《Java设计模式及实践》

JK_DesignPattern.png

预览
Loading comments...
0 条评论

暂无数据

example
预览