码猿技术专栏

微信公众号:码猿技术专栏

设计模式之代理模式

结构型模式之代理模式(静态代理)

  • 由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称之为“代理”的第三者来实现间接访问,该方案对应的设计模式被称为代理模式。

  • 代理其实是实现简介访问的媒介,当然在代理类中还可以在执行代理操作之前,之后,之中,环绕等执行相关动作。Spring 中面向切面编程就是这个原理

  • 代理模式是一种应用很广泛的结构型设计模式,而且变化形式非常多,常见的代理形式包括远程代理、保护代理、虚拟代理、缓冲代理、智能引用代理等,后面将学习这些不同的代理形式

  • 当使用代理类的时候, 真实类中的信息对用户来说是透明的(不可见的)

  • 主要就是用于对象的间接访问提供了一个方案,可以对对象的访问进行控制

定义

代理模式

  1. Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
  2. Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
  3. RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

实例

第一个例子

  • 需求: 我们知道mac笔记本是在美国生产的,那么如果中国供销商想要卖mac笔记本,那么必须从美国供销商那里先进货,然后中国的顾客才可以在中国供销商买mac。这里的中国供销商就相当于代理,美国供销商就相当于真实主题角色
  • Mac笔记本抽象接口(相当于其中的抽象主题)
1
2
3
4
5
6
/*
* 苹果笔记本的接口,其中有一个方法实现了买笔记本的动作
*/
public interface MacBook {
public void buy(); //购买笔记本的行为
}
  • 美国供销商(相当于这里RealSubject)
1
2
3
4
5
6
7
8
9
10
11
12
/*
* 美国的笔记本,实现了MacBook接口,表示在美国买笔记本
*/
public class USAMac implements MacBook {

@Override
public void buy() {
System.out.println("在美国买笔记本");

}
}

  • 中国供销商(相当于这里的代理角色)

    • 我们可以看到我们在使用代理模式的时候可以在之前和之后进行操作
      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
      /*
      * 中国的笔记本,实现了MacBook 表示在中国买笔记本
      * 但是中国想要买到苹果笔记本,那么还是需要先从美国进货,因此中国只是一个中间的代理作用而已
      * 当然代理的最大作用就是在代理之前、之后、之中执行相关的操作,这就是面向切面编程的原理
      */
      public class ChinaMac implements MacBook {

      private MacBook mcBook=new USAMac(); //创建USAMac的对象

      /**
      * 在购买之前执行的操作
      */
      public void preBuy(){
      System.out.println("购买之前执行的操作");
      }

      /**
      * 在购买之后执行的操作
      */
      public void afterBuy(){
      System.out.println("购买之后执行的操作");
      }

      @Override
      public void buy() {
      this.preBuy(); //之前执行的操作
      mcBook.buy(); //在美国买笔记本
      System.out.println("在中国买笔记本");
      this.afterBuy(); //之后执行的操作
      }
      }
  • 测试类

    • 我们在使用的时候直接使用代理类即可,我们根本不知道在真实类的使用,完全是代理类为我们提供了
1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
MacBook macBook=new ChinaMac(); //创建ChinaMac对象,在中国买笔记本
macBook.buy(); //直接在中国买笔记本

}
}

第二个例子

  • 我们登录一个网站的服务器端的验证步骤:

    • 读取用户名和密码
    • 验证用户名和密码
    • 记录到日志中
  • 这里的验证密码和记录到日志中可以在代理类中实现,在用户执行操作之前需要读取用户名和密码,并且验证,在操作之后需要将用户的一些操作记录到日志中。其实这里的真实用户需要做的只是执行自己的操作,而验证和记录都是交给代理类实现的。

实现

  • 用户接口(User)
1
2
3
4
5
6
7
/*
* 用户的抽象类
*/
public interface User {
public void DoAction(); //执行动作

}
  • 真实的用户类(实现了用户接口)
    • 主要的做的就是执行自己的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RealUser implements User {
public String name;
public String password;
public RealUser(String name, String password) {
this.name = name;
this.password = password;
}
public RealUser() {}
/*
* 执行一些操作
*/
@Override
public void DoAction() {
System.out.println("开始执行操作......");
}
}
  • 代理类(实现了User接口)
    • 在执行操作之前验证密码和用户名是否正确
    • 在执行操作之后记录到日志中
    • 实际上这里就是面向切面编程
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
public class ProxUser implements User {
private RealUser user; // 真实用户的对象

/**
* 创建对象
* @param name 姓名
* @param password 密码
*/
public ProxUser(String name, String password) {
user = new RealUser(name, password);
}

@Override
public void DoAction() {
//验证用户名和密码
if (Validate()) {
user.DoAction(); //调用真实用户的DoAction方法执行相关操作
logger(); //调用日志记录信息
} else {
System.out.println("请重新登录.......");
}
}

/*
* 验证用户的用户名和密码
*/
public Boolean Validate() {
if ("陈加兵".equals(user.name) && "123456".equals(user.password)) {
return true;
}
return false;
}

/**
* 添加日志记录信息
*/
public void logger() {
System.out.println(user.name + "登录成功......");
}
}
  • 测试类
    • 实际上执行了验证用户名和密码,记录日志的操作,但是对于客户端来说只能看到自己执行的操作
1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
ProxUser proxUser=new ProxUser("陈加兵", "123456"); //创建代理对象
proxUser.DoAction(); //执行操作,实际执行了验证信息,doaction(),日志记录这个三个动作
}
}

缺点

  • 如果增加一个接口就需要增加一个代理类,如果是要增加很多,那么就要增加很多代理类,代码将会重复

解决方法

  • 下面我们将会讲解到动态代理,仅仅需要一个代理类即可

结构型模式之动态代理模式

  • 前面我们说的代理模式其实是属于静态代理模式,就是说在程序执行之前已经写好了代理类,但是缺点也是说过,必须为每个接口都实现一个代理类,如果有多个接口需要代理,那么代码肯定是要重复的,因此就需要动态代理了。

  • 动态代理可以实现多个接口共用一个代理类,只需要改变初始化的参数即可,可以省去很多的重复的代码。

  • JDK的动态代理需要一个类一个接口,分别为Proxy和InvocationHandler

  • 主要原理就是利用了反射的原理

InvocationHandler

  • 这个是代理类必须实现的接口,其中有一个方法public Object invoke(Object proxy,Method method,Object[] args)
    • Object proxy:指被代理的对象。 
    • Method method:要调用的方法 
    • Object[] args:方法调用时所需要的参数 

Proxy

  • Proxy类是专门完成代理的操作类,可以通过此类为一个或多个接口动态地生成实现类,此类提供了如下的操作方法:
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
    • ClassLoader loader:类加载器 
    • Class<?>[] interfaces:得到全部的接口 
    • InvocationHandler h:得到InvocationHandler接口的子类实例 

实例

  • 肯德基的接口
1
2
3
4
5
6
/*
* 肯德基的接口,其中一个eat方法
*/
public interface IKFC {
public void eat();
}
  • 肯德基的实现类(RealSubject)
1
2
3
4
5
6
7
8
9
10
11
/*
* IKFC的实现类
*/
public class KFC implements IKFC {

@Override
public void eat() {
System.out.println("我在肯德基吃了饭......");

}
}
  • 苹果笔记本的接口
1
2
3
4
5
6
7
/*
* 苹果笔记本的接口
*/
public interface MacBook {
public void buy();
}

  • 美国供销商的类(RealSubject)
1
2
3
4
5
6
7
8
9
10
/*
* 美国笔记本的类,实现了MacBook接口
*/
public class USAMacBook implements MacBook {
@Override
public void buy() {
System.out.println("在美国买了一个苹果电脑......");
}

}
  • 动态代理的类(实现了InvocationHandler接口)
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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
* 这个是代理类,实现了InvocationHandler接口
*
*/
public class ProxyHandler implements InvocationHandler {
private Object Realobject; //被代理的对象

//构造方法,用来初始化被代理的对象
public ProxyHandler(Object obj){
this.Realobject=obj; //初始化真实类的对象
}

/**
* @param proxy 表示被代理的对象的,就是真实类的对象
* @param method 表示要调用真实类的方法
* @param args 表示方法调用的时候所需要的参数
* @return 方法调用之后的返回值
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
prefunction(); //执行之前调用的方法
Object res=method.invoke(Realobject, args); //Method类中的执行方法的函数,在反射中常用
afterFunction(); //执行之后调用的方法
return res;
}


/**
* 执行方法之前调用的方法
*/
public void prefunction(){
System.out.println("执行方法之前......");
}

/**
* 执行方法之后调用的方法
*/
public void afterFunction(){
System.out.println("执行方法之后......");
}
}
  • 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.Proxy;
import com.sun.org.apache.bcel.internal.generic.NEW;
import com.sun.org.apache.bcel.internal.util.Class2HTML;
public class Client {
public static void main(String[] args) {

Class[] cls1={IKFC.class}; //第一个代理的所有接口数组,直接用接口的反射即可

Class[] cls2=USAMacBook.class.getInterfaces(); //直接具体的实现类的反射调用getInterfaces即可返回所有的接口数组

// 返回KFC的代理对象
IKFC kfc = (IKFC) Proxy.newProxyInstance(Client.class.getClassLoader(),
cls1, new ProxyHandler(new KFC()));
kfc.eat(); //执行方法

MacBook macBook = (MacBook) Proxy.newProxyInstance(Client.class.getClassLoader(),
cls2, new ProxyHandler(
new USAMacBook()));

macBook.buy(); //执行方法

}
}

总结

  • 动态代理的好处
    • 即使有多个接口,也仅仅只有一个动态代理类

笔者有话说

  • 最近建了一个微信交流群,提供给大家一个交流的平台,扫描下方笔者的微信二维码,备注【交流】,我会把大家拉进群

欢迎关注我的其它发布渠道