Mybatis之接口编程--JAVA动态代理的最佳展现

0. 前言

Mybatis是非常流行的ORM开发框架. 由最初的IBatis到今天的MyBatis, 功能越来越强大, 开发越来越便捷. 尤其是基于接口的开发, 使得开发者在编写DAO时可以节省了很多时间. JAVA中接口不能被实例化, 因此不能直接调用接口. 那么Mybatis是如何实现接口调用的呢?

本文将带领大家了解Mybatis实现接口编程的关键 -- JAVA动态代理

1. Mybatis2.x

很早以前使用Mybatis2.x版本编写DAO时, 我们需要这么写:

public interface UserDAO {
    Integer selectUserCount();
}
复制代码
public class IbatisUserDAO extends SqlMapClientDaoSupport implements UserDAO {

    public Integer selectUserCount() {
        return (Integer) this.getSqlMapClientTemplate().queryForObject("selectUserCount");
    }
    
}
复制代码
<sqlMap namespace="User">

    <select id="selectUserCount" resultClass="int">
        select count(*) from t_user
    </select>

</sqlMap> 
复制代码

每个DAO接口都需要实现类并手动调用Mybatis的组件, Mybatis通过传入的ID找到SQL并执行.

2. Mybatis3.x

Mybatis3.x开始支持基于接口编程, 开发者只需要定义DAO中的数据接口, 在Mapper中编写对应的SQL即可. 从此开发者不在需要手动调用Mybatis的组件, 这不仅节约了开发时间(无需提供DAO实现类), 更使得开发者不需要关心Mybatis的细节, 可以把更多精力放在业务数据接口上.

使用Mybatis3.x版本时编写DAO时, 我们需要这么写:

public interface UserDAO {
    Integer selectUserCount();
}
复制代码
<sqlMap namespace="User">

    <select id="selectUserCount" resultClass="int">
        select count(*) from t_user
    </select>

</sqlMap> 
复制代码

3. 动态代理

Mybatis中的DAO定义的是接口, 没有实现类. 但是在JAVA中是无法直接调用接口, 那Mybatis是如何实现的基于接口编程的呢? 首先我们先熟悉一种设计模式:

3.1 代理模式

设计模式中有一种模式叫做代理模式: 假如有人欠你钱, 那么你会找一个律师帮你打官司. 需要谈判时会由你的律师帮你和对方进行谈判. 那么律师就相当于代理类, 你相当于委托类, 代理类全权代理你. 双方谈判时由代理类出面, 谈好了需要签字画押时才需要你出手. 这就叫做代理模式.

为什么需要代理类呢? 很简单: 你不会谈判, 律师比你更专业.

3.2 静态代理

JAVA中有我们下面会讲到的动态代理, 相比之下才有静态代理一说, 静态代理指的就是代理模式. 我们以上述的律师代理谈判为例, 编写一段代码:

你和律师都能谈判和签名, 因此定义一个含有谈判和签名方法的接口, 定义你和律师两个类实现接口:

// 接口: 人
public interface Person {

    // 谈判
    void talk();

    // 签名
    void sign();

}

复制代码
// 你
public class You implements Person{

    // 你的谈判
    @Override
    public void talk() {
        System.out.println("你开始谈判");
    }

    // 你的签名
    @Override
    public void sign() {
        System.out.println("你开始签名");
    }

}
复制代码
// 律师
public class Lawer implements Person {

    // 委托人(被代理人)
    private Person client;

    // 创建律师时必须指定委托人
    public Lawer(Person client) {
        this.client = client;
    }

    // 律师的谈判
    @Override
    public void talk() {

        // 谈判不需要委托人出面, 由律师完成
        // 因此不需要调用委托人的talk方法
        System.out.println("律师开始谈判");

    }

    // 律师的签名
    @Override
    public void sign() {

        // 律师不能代替委托人签名
        // 需要调用委托人的sign方法
        client.sign();

    }

}
复制代码
  • 律师是代理别人去谈判, 因此律师类中必须有委托人. 构造时传入委托人.
  • 律师谈判时不需要委托人出面, 因此不需要调用委托人的talk方法.
  • 签名时必须由委托人签名, 因此需要调用委托人的sign方法.

上述场景构建完成, 新建测试类:

public class Test {

    public static void main(String[] args) {

        // 需要谈判
        // 你找个律师代理你
        Person lawer = new Lawer(new You());

        // 开始谈判
        lawer.talk();

        // 需要签名
        lawer.sign();

    }

}
复制代码

当你需要谈判时, 创建的是代理类(律师)并将委托类(你)传入代理类中, 所有的操作都是对代理类(代理你的律师)进行操作. 总结一下代理模式的实现方式:

  • 代理类与委托类实现同一接口.
  • 代理类中需要指定委托类(即代理类代理了谁).
  • 需要委托类时不直接返回委托类, 构建代理类返回.

使用代理模式有如下好处:

  • 你不需要学习谈判的技巧, 这些都是律师去做的, 你只需要做自己做的事.
  • 你的朋友也需要谈判时, 只需让律师代理你的朋友即可, 他同样不需要学习谈判技巧.
  • 你的很多朋友都委托律师时, 如果有新的谈判技巧, 只需要律师一人学习就可以.
  • 如果你觉得律师不好时, 只需要换一个律师. 你什么都不需要改变.
  • 当有很多人找你谈判, 都会先找到律师, 由律师和你对接, 你是很安全的.

上述代理模式的优势可以归纳为以下几点:

  • 权限控制: 代理类负责控制委托类的访问权限. 即需要先和律师谈判, 由律师决定找不找你.
  • 业务解耦: 降低委托类的业务复杂度. 即你不需要学习很多知识, 比如谈判技巧.
  • 便于维护: 代理类实现通用业务, 委托类实现差异化. 通用业务变化时只修改代理类即可.

但如果你没有使用代理模式, 你会发现你需要不停的学习各种谈判技巧, 你的朋友需要谈判时你需要教给他怎么谈判, 很多人直接和你谈判时你会很累...

3.3 JAVA动态代理

代理模式虽然优势很多, 但有这样一个问题: 由于代理类与委托类都是实现同一接口, 当接口变化时, 所有代理类和委托类都需要相应的修改(随接口增加或删除方法). 比如上例接口增加了一个谈判必备的喝酒功能, 你和律师都需要在各自类中增加相应方法.

Mybatis2.x时很多项目都定义了DAO接口和实现类, 当业务频繁变化时需要同步修改接口与实现类.

代理模式核心就是返回委托类的代理类, 如果能够做到当调用委托类的方法时自动执行代理类的方法, 代理模式就会完美很多. JAVA通过反射机制提供了这种方式, 叫做动态代理.

动态代理可以实现: 当调用Person接口中的任一方法时, 自动调用代理类中的方法.

3.3.1 创建动态代理类

动态代理类必须实现java.lang.reflect.InvocationHandler接口的invoke方法, 当调用Person接口中任一方法时都将执行动态代理类的invoke方法, 可以将代理类的业务逻辑写在invoke方法中.

// 动态代理类
// 需要实现InvocationHandler接口的invoke方法
public class PersonProxy implements InvocationHandler {

    // 当调用被代理的接口的任何方法时都会执行到该方法
    // proxy: 被调用的委托者对象(Person)
    // method: 委托者被调用的方法对象(Person中被调用的方法)
    // args: 委托者被调用的的方法的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        return null;
    }

}

复制代码

3.3.2 获取动态代理类

调用java.lang.reflect.Proxy类中的newProxyInstance方法获取动态代理类, 此时获取到的代理类是Person接口的代理类. 可以调用Person中的方法.

public static void main(String[] args) {

    // 动态代理类
    PersonProxy proxy = new PersonProxy();

    // 调用Proxy.newProxyInstance可获取, newProxyInstance参数如下:
    // 参数1: 生成代理类的类加载器
    // 参数2: 代理的接口, JAVA根据接口在生成的代理类中添加接口的实现方法(执行参数3的invoke)
    // 参数3: 实现InvocationHandler的动态代理类,当调用参数2接口中的方法时会执行该类的invoke方法
    Person person = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), 
                                                    new Class[] { Person.class }, 
                                                    proxy);

    // 调用接口的方法, 会自动执行到动态代理类的invoke中
    person.talk();
    person.sign();

}
复制代码

获取到的代理类为调用newProxyInstance时传入的第二个参数的代理类, 本例中传入Person接口, 获取的到代理类为Person的代理类, Person为委托类.

3.3.3 动态代理原理

从程序的结构分析: 动态代理和静态代理相比, 少定义了一个实现Person接口的代理类. 实际上JAVA动态代理会自动生成代理模式所需的实现Person接口的代理类. 过程简述如下:

  1. 调用Proxy.newProxyInstance获取代理类时第二个参数传入接口Person.
  2. JAVA会自动生成一个代理类并实现Person接口中的所有方法.
  3. 每个方法的实现过程为通过反射调用第三个参数传入的PersonProxy中的invoke方法
  4. 将生成的代理类返回. 代理类实现了Person接口, 因此代理类中有Person接口的所有方法.

JAVA动态代理自动生成的代理类, 示例如下:

// JAVA动态代理生成的代理类
public final class $Proxy0 extends Proxy implements Person {

    // 实现Person的方法
    @Override
    public void talk() {
        // 通过反射调用PersonProxy的invoke方法
        // this.h为创建代理类时传入的PersonProxy对象(第三个参数)
        // m为PersonProxy的invoke方法
        this.h.invoke(this, m, null);

    }

    // 实现Person的方法
    @Override
    public void sign() {
        this.h.invoke(this, m, null);
    }

}
复制代码

当JAVA动态代理生成完代理类后, 是不是和静态代理的实现方式完全一样了呢.

4. Mybatis实现原理

熟悉了JAVA动态代理, 我们来看一下Mybatis接口编程的实现原理.

  1. 获取DAO(Spring注入)时, 实际获取到的是DAO的代理类.
  2. 代码中调用DAO中的selectUserCount方法, 实际上调用的是DAO代理类的方法.
  3. 代理类的selectUserCount方法中通过反射调用动态代理类(PersonProxy)的invoke方法.
  4. 动态代理类(PersonProxy)的invoke方法可以获取DAO中被执行的方法名: selectUserCount.
  5. 通过selectUserCountMapper文件中找到对应的SQL并执行

5. 总结

JAVA动态代理的优势在是Mybatis接口编程中体现的淋漓尽致. 本文介绍了代理模式并结合动态代理简述了Mybatis接口编程的实现原理, Mybatis真正的实现要复杂的多. 当明白了其原理后, 当查看Mybatis源码时可以少走很多弯路.

下一篇文章会结合项目实际应用案例介绍Mybatis拦截器的使用方式, 欢迎关注.

猜你喜欢

转载自juejin.im/post/5b8a61886fb9a01a0d74c83a