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

在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。

在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。

代理模式的定义与特点

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

代理模式的主要优点有:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性


其主要缺点是:

  • 代理模式会造成系统设计中类的数量增加
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度;

那么如何解决以上提到的缺点呢?答案是可以使用动态代理方式

代理模式的结构与实现

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构和实现方法。

1. 模式的结构

代理模式的主要角色如下。

  1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。


其结构图如图 1 所示。
 

代理模式的结构图
图1 代理模式的结构图


在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态:在程序运行时,运用反射机制动态创建而成

2. 模式的实现

代理模式的实现代码如下:

package proxy;
public class ProxyTest {
    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.Request();
    }
}
//抽象主题
interface Subject {
    void Request();
}
//真实主题
class RealSubject implements Subject {
    public void Request() {
        System.out.println("访问真实主题方法...");
    }
}
//代理
class Proxy implements Subject {
    private RealSubject realSubject;
    public void Request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        preRequest();
        realSubject.Request();
        postRequest();
    }
    public void preRequest() {
        System.out.println("访问真实主题之前的预处理。");
    }
    public void postRequest() {
        System.out.println("访问真实主题之后的后续处理。");
    }
}

程序运行的结果如下:

访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

代理模式的应用实例

【例1】韶关“天街e角”公司是一家婺源特产公司的代理公司,用代理模式实现。

分析:本实例中的“婺源特产公司”经营许多婺源特产,它是真实主题,提供了显示特产的 display() 方法,可以用窗体程序实现(点此下载该实例所要显示的图片)。而韶关“天街e角”公司是婺源特产公司特产的代理,通过调用婺源特产公司的 display() 方法显示代理产品,当然它可以增加一些额外的处理,如包裝或加价等。客户可通过“天街e角”代理公司间接访问“婺源特产公司”的产品,图 2 所示是公司的结构图。
 

韶关“天街e角”公园的结构图
图2 韶关“天街e角”公司的结构图


程序代码如下:

package proxy;
import java.awt.*;
import javax.swing.*;
public class WySpecialtyProxy {
    public static void main(String[] args) {
        SgProxy proxy = new SgProxy();
        proxy.display();
    }
}
//抽象主题:特产
interface Specialty {
    void display();
}
//真实主题:婺源特产
class WySpecialty extends JFrame implements Specialty {
    private static final long serialVersionUID = 1L;
    public WySpecialty() {
        super("韶关代理婺源特产测试");
        this.setLayout(new GridLayout(1, 1));
        JLabel l1 = new JLabel(new ImageIcon("src/proxy/WuyuanSpecialty.jpg"));
        this.add(l1);
        this.pack();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public void display() {
        this.setVisible(true);
    }
}
//代理:韶关代理
class SgProxy implements Specialty {
    private WySpecialty realSubject = new WySpecialty();
    public void display() {
        preRequest();
        realSubject.display();
        postRequest();
    }
    public void preRequest() {
        System.out.println("韶关代理婺源特产开始。");
    }
    public void postRequest() {
        System.out.println("韶关代理婺源特产结束。");
    }
}
程序运行结果如图 3 所示。


韶关“天街e角”公司的代理产品
图3 韶关“天街e角”公司的代理产品

代理模式的应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

前面分析了代理模式的结构与特点,现在来分析以下的应用场景。

  • 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
  • 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
  • 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
  • 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
  • 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。

代理模式的扩展

在前面介绍的代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点。

  1. 真实主题与代理主题一一对应,增加真实主题也要增加代理。
  2. 设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题,如 SpringAOP,其结构图如图 4 所示。

 

动态代理模式的结构图
图4 动态代理模式的结构图

静态代理和动态代理

静态代理就是按照代理模式书写的代码,如《代理模式》一节中的示例,其特点是代理类和目标类在代码中是确定的,因此称为静态。静态代理可以在不修改目标对象功能的前提下,对目标功能进行扩展。

但是静态代理显然不够灵活,这时就需要动态代理。

动态代理也叫 JDK 代理或接口代理,有以下特点:

  • 代理对象不需要实现接口
  • 代理对象的生成是利用 JDK 的 API 动态的在内存中构建代理对象
  • 能在代码运行时动态地改变某个对象的代理,并且能为代理对象动态地增加方法、增加行为


一般情况下,动态代理的底层不用我们亲自去实现,可以使用线程提供的 API 。例如,在 Java 生态中,目前普遍使用的是 JDK 自带的代理和 GGLib 提供的类库。

JDK 实现代理只需要使用 newProxyInstance 方法,该方法需要接收三个参数,语法格式如下:

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

注意该方法在 Proxy 类中是静态方法,且接收的三个参数说明依次为:

  • ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
  • Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
  • InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,把当前执行目标对象的方法作为参数传入


下面根据实例介绍静态代理和动态代理。

众所周知,编程非常培养孩子的逻辑思维能力。很多父母为了不让孩子输在起跑线上,就开始到处为孩子找辅导老师。下面来看代码实现。

创建顶层接口 IPerson,代码如下:

 
public interface IPerson {
    void findTeacher(); //找老师
}

儿子张三要找老师,实现 IPerson 接口,ZhangSan 类代码如下:

 
public class ZhangSan implements IPerson {
    @Override
    public void findTeacher() {
        System.out.println("儿子张三提出要求");
    }
}

父亲张老三要帮儿子张三找老师,实现 IPerson 接口,ZhangLaoSan 类代码如下:

 
public class ZhangLaoSan implements IPerson {
    private ZhangSan zhangsan;
    public ZhangLaoSan(ZhangSan zhangsan) {
        this.zhangsan = zhangsan;
    }
    @Override
    public void findTeacher() {
        System.out.println("张老三开始找老师");
        zhangsan.findTeacher();
        System.out.println("开始学习");
    }
}

新建 Test 类测试代码。

 
public class Test {
    public static void main(String[] args) {
        ZhangLaoSan zhanglaosan = new ZhangLaoSan(new ZhangSan());
        zhanglaosan.findTeacher();
    }
}

运行结果如下所示:

张老三开始找老师
儿子张三提出要求
开始学习

上面的场景有个弊端,就是自己的父亲只会帮自己的子女去挑选辅导老师,别人家的孩子是不会管的。于是社会上这样业务发展成了一个产业,出现了答疑、辅导班、培训机构等,还有各种各样的定制套餐。

这样如果还是用静态代理成本就太高了,需要一个更加通用的解决方案,满足任何想学习编程找老师的需求。这就由静态代理升级到了动态代理。采用动态代理基本上只要是人(IPerson)就可以提供找老师服务。

下面基于 JDK 动态代理支持来升级一下代码。

首先创建辅导班类 JdkFuDao。

public class JdkFuDao implements InvocationHandler {
    private IPerson target;
    public IPerson getInstance(IPerson target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        return (IPerson) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target, args);
        after();
        return result;
    }
    private void after() {
        System.out.println("双方同意,开始辅导");
    }
    private void before() {
        System.out.println("已经收集到您的需求,开始挑选老师");
    }
}

然后创建一个类 ZhaoLiu。

public class ZhaoLiu implements IPerson {
    @Override
    public void findTeacher() {
        System.out.println("符合赵六的要求");
    }
    public void buyInsure() {
    }
}

需要注意的是,代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。

最后客户端测试代码如下:

public class Test {
    public static void main(String[] args) {
        JdkFuDao jdkFuDao = new JdkFuDao();
        IPerson zhaoliu = jdkFuDao.getInstance(new ZhaoLiu());
        zhaoliu.findTeacher();
    }
}

运行结果如下所示:

已经收集到您的需求,开始挑选老师
符合赵六的要求
双方同意,开始辅导

静态代理和动态代理的区别

静态代理和动态代理主要有以下几点区别:

  • 静态代理只能通过手动完成代理操作,如果被代理类增加了新的方法,则代理类需要同步增加,违背开闭原则。
  • 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
  • 若动态代理要对目标类的增强逻辑进行扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。

 

使用代理模式切换数据源

学了代理模式后,小伙伴们可能还是不知道如何将代理模式应用到实际业务场景中,下面来看一个实际的业务场景。

在分布式业务场景中,通常会对数据库进行分库分表,这样使用 Java 操作时就可能需要配置多个数据源,可以通过设置数据源路由来动态切换数据源。本节分别使用静态代理和动态代理来切换数据源。

首先创建 Order 订单类。

public class Order {
    private Object orderInfo;
    private Long createTime;
    private String id;
    public Object getOrderInfo() {
        return orderInfo;
    }
    public void setOrderInfo(Object orderInfo) {
        this.orderInfo = orderInfo;
    }
    public Long getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Long createTime) {
        this.createTime = createTime;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
}

创建 OrderDao 持久层操作类。

public class OrderDao {
    public int insert(Order order) {
        System.out.println("OrderDao创建Order成功");
        return 1;
    }
}

创建 IOrderService 接口。

public interface IOrderService {
    int createOrder(Order order);
}

创建 OrderService 实现类。

public class OrderService implements IOrderService {
    private OrderDao orderDao;
    public OrderService() {
        //如果使用Spring,这里应该是自动注入的
        //为了使用方便,我们在构造方法中直接将orderDao初始化
        orderDao = new OrderDao();
    }
    @Override
    public int createOrder(Order order) {
        System.out.println("OrderService调用orderDao创建订单");
        return orderDao.insert(order);
    }
}

使用静态代理切换数据源

以下主要实现根据订单创建时间自动按年来进行分库的功能。根据开闭原则,通过代理对象来完成数据源路由对象的创建。

创建 DynamicDataSourceEntry 类,使用 ThreadLocal 的单例实现。

//动态切换数据源
public class DynamicDataSourceEntry {
    //默认数据源
    public final static String DEFAULT_SOURCE = null;
    private final static ThreadLocal<String> local = new ThreadLocal<String>();
    private DynamicDataSourceEntry() {
    }
    //清空数据源
    public static void clear() {
        local.remove();
    }
    //获取当前正在使用的数据源名字
    public static String get() {
        return local.get();
    }
    //还原当前切换的数据源
    public static void restore() {
        local.set(DEFAULT_SOURCE);
    }
    //设置已知名字的数据源
    public static void set(String source) {
        local.set(source);
    }
    //根据年份动态设置数据源
    public static void set(int year) {
        local.set("DB_" + year);
    }
}

创建切换数据源的代理类 OrderServiceStaticProxy。

import java.text.SimpleDateFormat;
import java.util.Date;
public class OrderServiceStaticProxy implements IOrderService {
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    private IOrderService orderService;
    public OrderServiceStaticProxy(IOrderService orderService) {
        this.orderService = orderService;
    }
    public int createOrder(Order order) {
        before();
        Long time = order.getCreateTime();
        Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
        System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
        DynamicDataSourceEntry.set(dbRouter);
        orderService.createOrder(order);
        after();
        return 0;
    }
    public void before() {
        System.out.println("代理模式之前的方法");
    }
    public void after() {
        System.out.println("代理模式之后的方法");
    }
}

DbRouteProxyTest 类测试代码如下。

public class DbRouteProxyTest {
    public static void main(String[] args) {
        try {
            Order order = new Order();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
            Date date = sdf.parse("2018/01/01");
            order.setCreateTime(date.getTime());
            IOrderService orderService = new OrderServiceStaticProxy(new OrderService());
            orderService.createOrder(order);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

运行结果如下所示:

代理模式之前的方法
静态代理类自动分配到【DB_2018】数据源处理数据
OrderService调用orderDao创建订单
OrderDao创建Order成功
代理模式之后的方法

运行结果符合预期,类图如下所示:
 


动态代理和静态代理的基本思路是一致的,只不过动态代理的功能更强大,随着业务的扩展,适应性更强。

使用动态代理实现无感知切换数据源

在学习了上面的案例后,下面我们使用动态代理实现无感知切换数据源,可以加深小伙伴们对动态代理的印象。

创建动态代理的类 OrderServiceDynamicProxy,代码如下:

public class OrderServiceDynamicProxy implements InvocationHandler {
    private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
    private Object target;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before(args[0]);
        Object object = method.invoke(target, args);
        after();
        return object;
    }
    private void before(Object target) {
        System.out.println("代理模式之前的方法");
        Long time = null;
        try {
            time = (Long) target.getClass().getMethod("getCreatTime").invoke(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
        Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
        System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
        DynamicDataSourceEntry.set(dbRouter);
    }
    private void after() {
        System.out.println("代理模式之后的方法");
    }
}

客户端测试代码如下:

public static void main(String[] args) {
    try {
        Order order = new Order();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        Date date = sdf.parse("2018/01/01");
        order.setCreateTime(date.getTime());
        IOrderService orderService = new OrderServiceStaticProxy(new OrderService());
        orderService.createOrder(order);
    } catch (ParseException e) {
        e.printStackTrace();
    }
}

运行结果如下:

代理模式之前的方法
静态代理类自动分配到【DB_2018】数据源处理数据
OrderService调用orderDao创建订单
OderDao创建Order成功
代理模式之后的方法

由运行结果可以看出,使用动态代理修改后的代码也能够达到相同的运行效果。但是,使用动态代理实现之后,不仅能实现 Order 的数据源动态路由,还可以实现其他类的数据源路由。

需要注意的是,这里有一个比较重要的约定,即(实体类)必须实现 getCreateTime() 方法,因为路由规则是根据时间来运算的,可以通过接口规范达到约束的目的。

 

彻底搞懂JDK动态代理核心原理

 通过前面的学习,我们知道 JDK 动态代理是代理模式的一种实现方式,那么它是如何实现的呢?俗话说:不仅知其然,还得知其所以然。下面主要探究一下 JDK 动态代理的原理,并模仿 JDK 动态代理编写一个属于自己的动态代理。

JDK 动态代理采用字节重组,重新生成对象来替代原始对象,以达到动态代理的目的。JDK 动态代理生成对象的步骤大致如下。

  1. 获取被代理对象的引用,并且获取它的所有接口。
  2. JDK 动态代理类重新生成一个新的类,同时新的类要实现被代理类实现的所有接口。
  3. 动态生成 Java 代码,新加的业务逻辑方法由一定的逻辑代码调用(在代码中体现),拿到被代理对象的引用。
  4. 编译新生成的 Java 代码 .class 字节码文件。
  5. 重新加载到 JVM 中运行。


以上过程就叫作字节码重组。

JDK 中有一个规范,在 ClassPath 目录下只要是 $ 开头的 .class 文件,一般都是自动生成的。

下面我们查看以 $ 开头的 .class 文件的内容。方法为:首先将内存中的对象字节码通过文件流输出到一个新的 .class 文件,然后使用反编译工具查看源码。

public static void main(String[] args) {
    try {
        IPerson obj = (IPerson) new JdkFuDao().getInstance(new ZhangSan());
        obj.findTeacher();
        //通过反编译工具查看源代码
        byte bytes[] = ProxyGenerator.generateProxyClass("$Proxy0", new Class[]{IPerson.class
        });
        FileOutputStream os = new FileOutputStream("D://$Proxy0.class");
        os.write(bytes);
        os.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

运行以上代码,成功后就可以在 D 盘找到 $Proxy0.class 文件了。

这里我们使用 Jad 工具进行反编译,当然您也可以使用其他反编译工具。反编译后得到 $Proxy0.jad 文件,文件内容如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
import java.lang.reflect.*;
import proxy.IPerson;
public final class $Proxy0 extends Proxy
    implements IPerson
{
    public $Proxy0(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }
    public final boolean equals(Object obj)
    {
        try
        {
            return ((Boolean)super.h.invoke(this, m1, new Object[] {
                obj
            })).booleanValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }
    public final int hashCode()
    {
        try
        {
            return ((Integer)super.h.invoke(this, m0, null)).intValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }
    public final void findTeacher()
    {
        try
        {
            super.h.invoke(this, m3, null);
            return;
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }
    public final String toString()
    {
        try
        {
            return (String)super.h.invoke(this, m2, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }
    private static Method m1;
    private static Method m0;
    private static Method m3;
    private static Method m2;
    static
    {
        try
        {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m3 = Class.forName("proxy.IPerson").getMethod("findTeacher", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
        }
        catch(NoSuchMethodException nosuchmethodexception)
        {
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());
        }
        catch(ClassNotFoundException classnotfoundexception)
        {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }
}

我们发现,$Proxy0 继承了 Proxy 类,并且实现了 IPerson 的接口,而且重写了 equals、hashCode、toString、findTeacher() 等方法。其中,在静态代码块中,通过反射获取了代理类的所有方法,而且保存了所有方法的引用,重写的方法用反射调用目标对象的方法。通过 invoke 执行代理类中的目标方法 findTeacher。

学到这里,大家一定会好奇,$Proxy0.jad 中的代码都是从哪里来的?这些都是 JDK 自动生成的。

手动模拟实现动态代理

下面我们不依赖 JDK,自己来动态生成源码、动态完成编译,然后替代目标对象并执行。

JDK 代理需要实现 java.lang.reflect.InvocationHandler 接口,并使用 java.lang.reflect.Proxy.newProxyInstance() 方法生成代理对象。

下面我们使用 JDK 代理的类名和方法名定义,由于篇幅原因,没有展示其源代码,大家可以自行查看。

仿照 InvocationHandler 接口,创建 MyInvocationHandler 接口并定义 invoke 方法,代码如下:

public interface MyInvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
}

仿照 Proxy 类,创建 MyProxy 类,代码如下:

/**
* 自己实现的代理类,用来生成字节码文件,并动态加载到JVM中
*/
public class MyProxy {
    public static final String ln = "\r\n";
    public static Object newProxyInstance(MyClassLoader  classLoader, Class<?>[] interfaces, MyInvocationHandler h) {
        try {
            //1、动态生成源代码.java文件
            String src = generateSrc(interfaces);
            //2、Java文件输出磁盘
            String filePath = MyProxy.class.getResource("").getPath();
            File f = new File(filePath + "$Proxy0.java");
            FileWriter fw = new FileWriter(f);
            fw.write(src);
            fw.flush();
            fw.close();
            //3、把生成的.java文件编译成.class文件
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager manage = compiler.getStandardFileManager(null, null, null);
            Iterable iterable = manage.getJavaFileObjects(f);
            JavaCompiler.CompilationTask task = compiler.getTask(null, manage, null, null, null, iterable);
            task.call();
            manage.close();
            //4、编译生成的.class文件加载到JVM中来
            Class proxyClass = classLoader.findClass("$Proxy0");
            Constructor c = proxyClass.getConstructor(MyInvocationHandler.class);
            f.delete();
            //5、返回字节码重组以后的新的代理对象
            return c.newInstance(h);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    private static String generateSrc(Class<?>[] interfaces) {
        StringBuffer sb = new StringBuffer();
        sb.append(MyProxy.class.getPackage() + ";" + ln);
        sb.append("import " + interfaces[0].getName() + ";" + ln);
        sb.append("import java.lang.reflect.*;" + ln);
        sb.append("public class $Proxy0 implements " + interfaces[0].getName() + "{" + ln);
        sb.append("MyInvocationHandler h;" + ln);
        sb.append("public $Proxy0(MyInvocationHandler h) { " + ln);
        sb.append("this.h = h;");
        sb.append("}" + ln);
        for (Method m : interfaces[0].getMethods()) {
            Class<?>[] params = m.getParameterTypes();
            StringBuffer paramNames = new StringBuffer();
            StringBuffer paramValues = new StringBuffer();
            StringBuffer paramClasses = new StringBuffer();
            for (int i = 0; i < params.length; i++) {
                Class clazz = params[i];
                String type = clazz.getName();
                String paramName = toLowerFirstCase(clazz.getSimpleName());
                paramNames.append(type + " " + paramName);
                paramValues.append(paramName);
                paramClasses.append(clazz.getName() + ".class");
                if (i > 0 && i < params.length - 1) {
                    paramNames.append(",");
                    paramClasses.append(",");
                    paramValues.append(",");
                }
            }
            sb.append("public " + m.getReturnType().getName() + " " + m.getName() + "(" + paramNames.toString() + ") {" + ln);
            sb.append("try{" + ln);
            sb.append("Method m = " + interfaces[0].getName() + ".class.getMethod(\"" + m.getName() + "\",new Class[]{" + paramClasses.toString() + "});" + ln);
            sb.append((hasReturnValue(m.getReturnType()) ? "return " : "") + getCaseCode("this.h.invoke(this,m,new Object[]{" + paramValues + "})", m.getReturnType()) + ";" + ln);
            sb.append("}catch(Error _ex) { }");
            sb.append("catch(Throwable e){" + ln);
            sb.append("throw new UndeclaredThrowableException(e);" + ln);
            sb.append("}");
            sb.append(getReturnEmptyCode(m.getReturnType()));
            sb.append("}");
        }
        sb.append("}" + ln);
        return sb.toString();
    }
    private static Map<Class, Class> mappings = new HashMap<Class, Class>();
    static {
        mappings.put(int.class, Integer.class);
    }
    private static String getReturnEmptyCode(Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "return 0;";
        } else if (returnClass == void.class) {
            return "";
        } else {
            return "return null;";
        }
    }
    private static String getCaseCode(String code, Class<?> returnClass) {
        if (mappings.containsKey(returnClass)) {
            return "((" + mappings.get(returnClass).getName() + ")" + code + ")." + returnClass.getSimpleName() + "Value()";
        }
        return code;
    }
    private static boolean hasReturnValue(Class<?> clazz) {
        return clazz != void.class;
    }
    private static String toLowerFirstCase(String src) {
        char[] chars = src.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }
}

创建 MyClassLoader 类,代码如下:

public class MyClassLoader  extends ClassLoader {
    private File classPathFile;
    public MyClassLoader () {
        String classPath = MyClassLoader .class.getResource("").getPath();
        this.classPathFile = new File(classPath);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String className = MyClassLoader .class.getPackage().getName() + "." + name;
        if (classPathFile != null) {
            File classFile = new File(classPathFile, name.replaceAll("\\.", "/") + ".class");
            if (classFile.exists()) {
                FileInputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte[] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1) {
                        out.write(buff, 0, len);
                    }
                    return defineClass(className, out.toByteArray(), 0, out.size());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

创建 MyFuDao 类,代码如下:

public class MyFuDao implements MyInvocationHandler {
    private IPerson target;
    public IPerson getInstance(IPerson target) {
        this.target = target;
        Class<?> clazz = target.getClass();
        return (IPerson) MyProxy.newProxyInstance(new MyClassLoader (), clazz.getInterfaces(), this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(this.target, args);
        after();
        return result;
    }
    private void after() {
        System.out.println("双方同意,开始辅导");
    }
    private void before() {
        System.out.println("已经收集到你的需求,开始挑选");
    }
}

客户端测试代码如下:

public class Test {
    public static void main(String[] args) {
        MyFuDao MyFuDao = new MyFuDao();
        IPerson zhangsan = MyFuDao.getInstance(new ZhangSan());
        zhangsan.findTeacher();
    }
}

运行结果如下:

已经收集到你的需求,开始挑选
儿子张三提出要求
双方同意,开始辅导

 

到这里,手写JDK动态代理就完成了。

 

代理模式在Spring源码中的应用

我们在使用 Spring AOP 时,需要先配置好 ProxyFactoryBean,然后通过 ac.getBean(bean id) 来获取 ProxyFactoryBean 代理的对象。而 ProxyFactoryBean 类使用 ProxyFactoryBean.getObject() 方法获取返回的对象,即代理对象。

下面先看 ProxyFactoryBean 类中的核心方法 getObject(),源码如下:

public Object getObject() throws BeansException {
    initializeAdvisorChain();
    if (isSingleton()) {
        return getSingletonInstance();
    }
    else {
        if (this.targetName == null) {
            logger.info("Using non-singleton proxies with singleton targets is often undesirable. " +
                    "Enable prototype proxies by setting the 'targetName' property.");
        }
        return newPrototypeInstance();
    }
}

在 getObject() 方法中,主要调用 getSingletonInstance() 和 newPrototypeInstance() 方法。

在 Spring 的配置中,如果不做任何设置,则 Spring 代理生成的 Bean 都是单例对象。如果修改 scope,则每次都创建一个新的原型对象。newPrototypeInstance() 里的逻辑比较复杂,教程后面会详细讲解,这里简单了解即可。

Spring 使用动态代理实现 AOP 时有两个非常重要的类,即 JdkDynamicAopProxy 类和 CglibAopProxy 类,其类图如下:
 


Spring 中的代理选择如下:

  • 当 Bean 有实现接口时,Spring 会用 JDK 动态代理方式
  • 当 Bean 没有实现接口时,Spring 会选择 CGLib 动态代理方式


Spring 可以通过配置强制使用 CGLib 动态代理,只需在 Spring 的配置文件中加入如下代码即可。

<aop:aspectj-autoproxy proxy-target-class="true"/>

代理模式在MyBatis源码中的应用

MyBatis 是一个应用非常广泛的优秀持久层框架,它几乎避免了所有 JDBC 代码、手动设置参数和获取结果集等工作。MyBatis 可以使用简单的 XML 配置文件或注解来映射类、接口和 POJO 与数据库记录的对应关系。

如果您使用过 MyBatis,则会发现 MyBatis 的使用非常简单。首先需要定义一个 Dao 接口,然后编写一个与 Dao 接口对应的 Mapper 配置文件。Java 对象与数据库字段的映射关系和 Dao 接口对应的 SQL 语句都写在配置文件中,非常简单清晰。

那么 Dao 接口是怎么和 Mapper 文件映射的呢?只有一个 Dao 接口,又是怎么以对象的形式来实现数据库读写操作的呢?

在了解代理模式后,我们应该很容易猜到,可以通过动态代理来创建 Dao 接口的代理对象,并通过这个代理对象实现数据库的操作。

在 MyBatis 中,MapperProxyFactory、MapperProxy、MapperMethod 是三个很重要的类。我们只需要了解这 3 个类,就能明白 Dao 接口与 SQL 的映射原理。

1. MapperProxyFactory

调用 addMapper() 方法时,跟踪源码就会发现 MapperRegistry 类的 addMapper() 方法中有如下语句:

knownMappers.put(type, new MapperProxyFactory<T>(type));

type 就是 Dao 接口,下面来看 MapperProxyFactory 类。

public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    public Class<T> getMapperInterface() {
        return mapperInterface;
    }
    public Map<Method, MapperMethod> getMethodCache() {
        return methodCache;
    }
    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}

MapperProxyFactory 看名字就知道这是一个工厂类,目的就是为了生成 MapperProxy。下面我们主要看它的构造方法和 newInstance() 方法。

构造方法中传入了一个 Class,通过参数名 mapperInterface 可以很容易猜到这个类就是个 Dao 接口。

MapperProxyFactory 中有 2 个 newInstance() 方法,权限修饰符不同,一个是 protected,一个是 public。

  1. protected 的 newInstance() 方法中首先创建了一个 MapperProxy 类的对象,然后通过 Proxy.newProxyInstance() 方法创建了一个对象并返回,也是通过同样的方法返回了 mapperInterface 接口的代理对象。
  2. public 的 newInstance() 方法中有一个参数 SqlSession,SqlSession 处理的就是执行一次 SQL 的过程。


上面提到的 MapperProxy 类显然是 InvocationHandler 接口的实现。因此,可以说 MapperProxyFactory 类是一个创建代理对象的工厂类,它通过构造函数传入自定义的 Dao 接口,并通过 newInstance 方法返回 Dao 接口的代理对象。

2. MapperProxy

看到这里,是不是有了一种豁然开朗的感觉,但新的疑问又来了,我们定义的 Dao 接口的方法并没有被实现,那这个代理对象又是如何实现增删改查的呢?带着这个疑问,我们来看一下 MapperProxy 类,源码如下:

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            try {
                return method.invoke(this, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        return mapperMethod.execute(sqlSession, args);
    }
    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
            methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
    }
}

很明显因为 Java 动态代理,MapperProxy 实现了 InvocationHandler 接口。下面先看 invoke() 方法。

在 invoke() 方法中,首先检查了如果是 Object 的方法就直接调用方法本身;如果不是就把方法 Method 包装成 MapperMethod。我们前面已经提到了 MapperMethod 主要就是处理方法的注解,参数,返回值,以及参数与 SQL 语句中参数的对应关系。因为将 Method 处理为 MapperMethod 是一个较频繁的操作,所以这里做了缓存处理。

MapperProxy中的成员变量

下面我们讲解 MapperProxy 类的 3 个成员变量,分别如下:

1)SqlSession

通过名字就知道 SqlSession 变量是一个执行 SQL 的接口,简单看一下它的接口定义。

public interface SqlSession extends Closeable {
    <T> T selectOne(String statement);
    <T> T selectOne(String statement, Object parameter);
   
    // 下面省略
}

这个接口方法的入参是 statement 和参数(parameter),返回值是数据对象。statement 可能会被误解为 SQL 语句,但其实这里的 statement 是指 Dao 接口方法的名称,自定义的 SQL 语句都被缓存在 Configuration 对象中。

在 SqlSession 中,可以通过 Dao 接口的方法名称找到对应的 SQL 语句。因此,可以想到代理对象本质上就是要将执行的方法名称和参数传入 SqlSession 的对应方法中,根据方法名称找到对应的 SQL 语句并替换参数,最后得到返回的结果。

2)mapperInterface

mapperInterface 的作用要结合第 3 个成员变量来说明。

3)methodCache

methodCache 其实就是一个 Map 键值对结构,键是 Method,值是 MapperMethod。

下面再回到 invoke() 方法的最后两行,它首先通过 cachedMapperMethod() 方法找到与将要执行的 Dao 接口方法对应的 MapperMethod,然后调用 MapperMethod 的 execute() 方法来实现数据库的操作。这里显然是将 SqlSession 传入 MapperMethod 内部,并在 MapperMethod 内部将要执行的方法名和参数再传入到 SqlSession 对应的方法中去执行。

3. MapperMethod

下面来看 MapperMethod 类的内部,看它如何完成 SQL 的执行。

public class MapperMethod{
    private final SqlCommand command;
    private final MethodSignature method;
}
两个成员变量,分别是 SqlCommand 和 MethodSignature。虽然这两个类的代码看起来很多,但实际上这两个内部类非常简单。
  • SqlCommand 主要解析了接口的方法名称和方法类型,定义了诸如 INSERT、SELECT、DELETE 等数据库操作的枚举类型。
  • MethodSignature 则解析了接口方法的签名,即接口方法的参数名称和参数值的映射关系,即通过 MethodSignature 类可以将入参的值转换成参数名称和参数值的映射。


最后来看 MapperMethod 类中最重要的 execute() 方法。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object param;
    Object result;
    switch(this.command.getType()) {
    case INSERT:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
        break;
    case UPDATE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
        break;
    case DELETE:
        param = this.method.convertArgsToSqlCommandParam(args);
        result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
        break;
    case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) { // 返回类型为void
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) { // 返回类型为集合或数组
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {// 由@MapKey控制返回
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {// 返回类型为Cursor<T>,采用游标
            result = this.executeForCursor(sqlSession, args);
        } else {
            // 其他类型
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
        }
        break;
    case FLUSH:
        result = sqlSession.flushStatements();
        break;
    default:
        throw new BindingException("Unknown execution method for: " + this.command.getName());
    }
    if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
        throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
    } else {
        return result;
    }
}

通过上面对 SqlCommand 和 MethodSignature 的简单分析,我们很容易理解这段代码。

  1. 首先,根据 SqlCommand 中解析出来的方法类型选择对应 SqlSession 中的方法,即如果是 INSERT 类型,则选择 SqlSession.insert() 方法来执行数据库操作。
  2. 其次,通过 MethodSignature 将参数值转换为 Map<Key, Value> 的映射,Key 是方法的参数名称,Value 是参数值。
  3. 最后,将方法名称和参数传入对应的 SqlSession 的方法中执行。至于在配置文件中定义的 SQL 语句,则被缓存在 SqlSession 的成员变量 中。


Configuration 中有非常多参数,其中一个是 mappedStatements,它保存了我们在配置文件中定义的所有方法。还有一个是 mappedStatement,它保存了我们在配置文件中定义的各种参数,包括 SQL 语句。

到这里,我们应该对 MyBatis 中如何通过将配置与 Dao 接口映射起来、如何通过代理模式生成代理对象来执行数据库读写操作有了较为宏观的认识,至于 SqlSession 中如果将参数与 SQL 语句结合组装成完整的 SQL 语句,以及如何将数据库字段与 Java 对象映射,感兴趣的小伙伴可以自行分析相关的源码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值