初探Mybatis源码——Mybatis的动态代理

目录

动态代理

Mybatis传统实现

Mybatis动态代理

MyBatis动态代理体现在哪里?


最近在整理大半年来所学内容,准备实习面试,之前更多的是在学怎么用,现在想深入地了解下为什么要这么用。在研究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 源码分析:

怒肝一夜 | Mybatis源码深度解析怒肝一夜 | Mybatis源码深度解析http://www.360doc.com/content/21/0104/15/64417993_955151966.shtml


更新于2022年6月18日

最近进一步阅读了mybatis源码和动态代理的内容,补充一点新的理解:

动态代理之投鞭断流!(全部拦截,拦截后你的东西我也不要了,全部用我的)

动态代理的目的是对目标方法进行增强、变动,而Mybatis的JDK动态代理,只代理了接口,没有实现类,可以认为是通过动态代理将目标实现方法全盘推倒,实现方法内容由invoke方法全部重写(那此时跟有没有实现类就没有关系了)。

有点类似CGLib动态代理,在代理接口时的操作:CGLib不是通过反射,而是通过继承来实现代理,因此如果代理的是接口,是无法获取到它的实现类的,只能在拦截方法intercept(等价于JDK动态代理的invoke方法)中直接写要实现的功能。

代理接口时,将实现类方法的代码全部覆盖,相当于动态代理做拦截,拦截后由拦截方法的内容直接替换实现类中的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码与咖啡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值