目录
最近在整理大半年来所学内容,准备实习面试,之前更多的是在学怎么用,现在想深入地了解下为什么要这么用。在研究MyBatis动态代理时发现,MyBatis的动态代理与传统的JDK动态代理不一样,它是直接代理了接口,而非某个目标类。于是整理了下动态代理的相关内容。
动态代理
java中动态代理通常分为 jdk动态代理 和 cglib动态代理,这里主要整理的是jdk动态代理,概念就不赘述了,直接上代码。
什么是jdk动态代理?以我在学习是的一个案例为例。
接口(行为):
// 出售U盘的接口
public interface UsbSell {
Object sell(int amount);
}
目标类(厂家):
// 某个U盘厂家实现U盘出售接口
public class Factory implements UsbSell {
@Override
public Double sell(int amount) {
return amount * 25.0;
}
}
代理类(中间商):
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
// 代理类,可以理解为就是中间商
public class MySellHandler implements InvocationHandler {
private Object factory = null;
public MySellHandler(Object factory) {
this.factory = factory;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object res;
// 通过反射机制获取厂家的方法
// 具体方法,具体参数需要在测试类中决定
res = method.invoke(factory, args);
// 代理类可以理解是中间商,在使用厂家方法后,再加上额外的操作(赚差价)
if (res != null) {
res = (Double)res + 10;
System.out.println(res);
return proxy;
}
return res;
}
}
测试类(顾客):
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
// 用接口多态创建厂家对象
UsbSell factory = new Factory();
// 创建InvocationHandler对象,用于创建代理对象
InvocationHandler handler = new MySellHandler(factory);
// 创建代理对象(中间商)
UsbSell proxy = (UsbSell) Proxy.newProxyInstance(
factory.getClass().getClassLoader(),
factory.getClass().getInterfaces(),
handler);
// 买家通过中间商购买U盘
proxyy.sell(3);
}
}
在这个例子中,动态代理可以理解为,买家无法直接接触厂家,只能通过代理商(中间商)间接的接触到厂家,代理商可以代理厂家的出售方案,并且从中赚取差价(功能增强)。
JDK动态代理的三个核心角色:
1、接口
2、目标类(接口的实现类、被代理类)
3、实现了InvocationHandler的代理类
动态代理最重要的作用是,能够在不改变目标类源码的情况下,增加功能。
Mybatis传统实现
Mybatis工具类,用于创建SqlSession:
package com.example.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory factory = null;
static {
String config="mybatis.xml"; // 与项目中配置文件名一致
try {
InputStream in = Resources.getResourceAsStream(config);
//创建SqlSessionFactory对象,使用SqlSessionFactoryBuild
factory = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
e.printStackTrace();
}
}
//获取SqlSession的方法
public static SqlSession getSqlSession() {
SqlSession sqlSession = null;
if( factory != null){
sqlSession = factory.openSession();// 非自动提交事务
}
return sqlSession;
}
}
Dao接口:
package com.example.dao;
import com.example.domain.Student;
import java.util.List;
public interface StudentDao {
List<Student> selectStudents();
int insertStudent(Student student);
}
Dao.xml(mapper文件):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.StudentDao">
<select id="selectStudents" resultType="com.example.domain.Student">
select id,name,email,age from student order by id
</select>
<insert id="insertStudent">
insert into student values(#{id},#{name},#{email},#{age})
</insert>
</mapper>
Dao接口实现类:
package com.example.dao.impl;
import com.example.dao.StudentDao;
import com.example.domain.Student;
import com.example.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import java.util.List;
public class StudentDaoImpl implements StudentDao {
@Override
public List<Student> selectStudents() {
//获取SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
String sqlId="com.example.dao.StudentDao.selectStudents";
//执行sql语句, 使用SqlSession类的方法
List<Student> students = sqlSession.selectList(sqlId);
//关闭
sqlSession.close();
return students;
}
@Override
public int insertStudent(Student student) {
//获取SqlSession对象
SqlSession sqlSession = MyBatisUtils.getSqlSession();
String sqlId="com.example.dao.StudentDao.insertStudent";
//执行sql语句, 使用SqlSession类的方法
int nums = sqlSession.insert(sqlId,student);
//提交事务
sqlSession.commit();
//关闭
sqlSession.close();
return nums;
}
}
几个概念:
MappedStatement类:每一个<select>、<insert>、<update>、<delete>标签均会被解析为MappedStatement对象,包含了标签中设定的sqlCommandType、resultSetType等属性;
BoundSql类:标签内具体sql语句会被解析为BoundSql对象,该对象有一个成员变量String sql,就是存放sql语句的;
SqlSession接口:定义了数据库操作方法,如selectList(),insert(),update(),delete()等。源码部分截图如下:
这些方法的第一个参数一般为String类型,用于传递sqlID,即xml映射文件中的namespace+id,用于定位xml中的具体sql语句。
在Mybatis传统实现中,完成一次数据库操作的流程是:
1、Dao接口声明方法
2、手动创建Dao接口实现类
3、在实现类中手动指定待执行的sql语句id和sqlSession的数据库操作方法
4、测试类中调用Dao接口实现类的方法,完成一次数据库操作
Mybatis动态代理
采用动态代理的方式实现,Dao接口和xml映射文件保持不变,不用手动写实现类,直接在测试类中创建Dao接口的代理对象即可。
package com.example;
import com.example.dao.StudentDao;
import com.example.domain.Student;
import com.example.utils.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class TestMyBatis {
@Test
public void testSelectStudents(){
/**
* 使用mybatis的动态代理机制, 使用SqlSession.getMapper(dao接口)
* getMapper能获取dao接口对应的实现类对象(代理类)。
*/
SqlSession sqlSession = MyBatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
//com.sun.proxy.$Proxy2 : jdk的动态代理
System.out.println("dao="+dao.getClass().getName());
//调用dao的方法, 执行数据库的操作
List<Student> students = dao.selectStudents();
for(Student stu: students){
System.out.println("学生="+stu);
}
}
@Test
public void testInsertStudent(){
SqlSession sqlSession = MyBatisUtils.getSqlSession();
StudentDao dao = sqlSession.getMapper(StudentDao.class);
Student student = new Student();
student.setId(1007);
student.setName("李飞");
student.setEmail("dunshan@qq.com");
student.setAge(28);
int nums = dao.insertStudent(student);
sqlSession.commit();
System.out.println("添加对象的数量:"+nums);
}
}
原理分析:
xml文件中,namespace与Dao接口全限定名称一致,id与Dao接口中的方法名一致,因此namespace + id 可以全局唯一地定位到 Dao 接口中的方法、BoundSql 对象和 MappedStatement 对象,因此,理论上如果已知 namespace 和 id 则通过某种机制(是反射机制吗?)能够让 sqlSession 知道应该用何种数据库操作方法执行何种 sql 语句。
数据库操作方法:从 MappedStatement 对象的成员变量 sqlCommandType 取;
sql 语句:从 BoundSql 对象的成员变量 sql 取。
MyBatis动态代理体现在哪里?
StudentDao dao = sqlSession.getMapper(StudentDao.class);
将 getMapper 层层深入,观察源码,最终能找到一个 MapperProxy 类,这个类继承了 InvocationHandler,并且重写了 invoke() 方法。
返回 MapperProxy 的上面一层,源码如下,调用 newInstance 方法时候创建了 MapperProxy 对象,并且是当 newProxyInstance 的第三个参数,所以 MapperProxy 类必须要实现 InvocationHandler,并且 newProxyInstance 前两个参数中的 mapperInterface 是指 getMapper 传入的 Dao.class。
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
回顾一下JDK动态代理的三个核心角色:
1、接口
2、目标类(接口的实现类、被代理类)
3、实现了 InvocationHandler 的代理类
显然,MyBatis 的动态代理与传统的 jdk 动态代理相比缺少了目标类,传统的是代理目标类,而MyBatis 是直接代理接口。动态代理的目的是不变的,传统的是从代理类的 invoke() 方法中去实现目标类中的方法,如果 MyBatis 能够从 invoke() 方法中,根据接口中方法的声明直接实现调用对应的 sql 语句,也就达到了动态代理的目的。经过上面的分析,如果知道了接口中的方法,其实已经能够让 sqlSession 知道执行什么内容了。
在 sqlSession.getMapper(StudentDao.class) 时,不光光传入了 Dao.class,还传入了 sqlSession,贴出 MapperProxy 类中的 invoke() 方法,mapperMethod.execute(this.sqlSession, args) 就是在调用 sqlSession 中的方法。接口已知,方法已知,就能定位到具体要执行的 sql 语句,此时的 sqlSession 完全能够完成数据库的对应操作。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
// 注意这里,是让sqlSession调用对应的方法
return mapperMethod.execute(this.sqlSession, args);
}
以上就是我对 MyBatis 动态代理机制的一点点理解,仅仅只是对源码一些浅层的分析,如有分析不足的地方欢迎指正~
贴一份大佬的 MyBatis 源码分析:
更新于2022年6月18日
最近进一步阅读了mybatis源码和动态代理的内容,补充一点新的理解:
动态代理之投鞭断流!(全部拦截,拦截后你的东西我也不要了,全部用我的)
动态代理的目的是对目标方法进行增强、变动,而Mybatis的JDK动态代理,只代理了接口,没有实现类,可以认为是通过动态代理将目标实现方法全盘推倒,实现方法内容由invoke方法全部重写(那此时跟有没有实现类就没有关系了)。
有点类似CGLib动态代理,在代理接口时的操作:CGLib不是通过反射,而是通过继承来实现代理,因此如果代理的是接口,是无法获取到它的实现类的,只能在拦截方法intercept(等价于JDK动态代理的invoke方法)中直接写要实现的功能。
代理接口时,将实现类方法的代码全部覆盖,相当于动态代理做拦截,拦截后由拦截方法的内容直接替换实现类中的方法。