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接口的代理类. 过程简述如下:
- 调用
Proxy.newProxyInstance
获取代理类时第二个参数传入接口Person
. - JAVA会自动生成一个代理类并实现
Person
接口中的所有方法. - 每个方法的实现过程为通过反射调用第三个参数传入的
PersonProxy
中的invoke
方法 - 将生成的代理类返回. 代理类实现了
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接口编程的实现原理.
- 获取DAO(Spring注入)时, 实际获取到的是DAO的代理类.
- 代码中调用DAO中的
selectUserCount
方法, 实际上调用的是DAO代理类的方法. - 代理类的
selectUserCount
方法中通过反射调用动态代理类(PersonProxy
)的invoke
方法. - 动态代理类(
PersonProxy
)的invoke
方法可以获取DAO中被执行的方法名:selectUserCount
. - 通过
selectUserCount
在Mapper
文件中找到对应的SQL并执行
5. 总结
JAVA动态代理的优势在是Mybatis接口编程中体现的淋漓尽致. 本文介绍了代理模式并结合动态代理简述了Mybatis接口编程的实现原理, Mybatis真正的实现要复杂的多. 当明白了其原理后, 当查看Mybatis源码时可以少走很多弯路.
下一篇文章会结合项目实际应用案例介绍Mybatis拦截器的使用方式, 欢迎关注.