【MyBatis框架】动态代理-MyBatis框架的代理模式

本文详细介绍了MyBatis框架中的动态代理机制,包括原生方法调用方式、JDK动态代理和CGLib动态代理的原理及实现。重点讨论了MyBatis如何使用JDK动态代理创建Mapper接口的代理对象,以及Mapper接口的注册和代理对象的获取过程。

MyBatis框架的代理模式

通过前面的了解,我们大致知道了MyBatis框架的基本使用方式。但是,我们可能会有一个疑问,为什么Mapper接口没有实现类却能被正常调用呢?其实这是因为MyBatis在Mapper接口上使用了动态代理的一种非常规的用法。在了解这种用法之前,我们先来看看原生方法的使用。

原生方法的调用方式
  • 以查询操作为例,原生方法的调用会直接通过sqlSession会话调用相应的方法来达到查询的目的。代码如下:

  • 接口文件:

public interface StudentMapper {
    /**
     * 模糊查询
     */
    public List<Student> getStudentByName(String str);
}
  • XML文件:
<?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.tulun.dao.StudentMapper">
    <!--直接在参数上拼接通配符,进行模糊查询-->
    <select id="getStudentByName" resultType="com.tulun.pojo.Student">
        select * from Student where SName like #{SName}
    </select>
mapper>
  • 测试文件:
public class StudentMapper3Test {
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() {
        //mybatis配置文件
        String resource = "mybatis-config.xml";
        //通过mybatis提供的Resources类来得到配置文件流
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //创建会话工厂,传输mybatis配置文件信息
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }
    
    @Test
    public void getStudentByName(){
        //通过工厂得到SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        //原生方法调用形式
        //通过方法接口进行调用
        List<Object> objects = sqlSession.selectList("com.tulun.dao.StudentMapper3.getStudentByName", "%L%");
        System.out.println(objects);
    }
}
  • 注意测试方式的不同:

    • 返回多个结果时,使用selectList方法。
    • 返回的结果不管是单个还是多个在resultType属性都是返回的Java对象全路径。
    • 返回单个结果对象使用selectOne
    • 对于增删改操作,同样可以使用此操作,其sqlSession对象也同样提供了其对应的方法。
  • 执行结果:

在这里插入图片描述

而在MyBatis的使用过程中,我们经常使用的是通过getMapper方法获取代理对象,形如:StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);方式,从而进行对接口的动态代理,接下来,我们看看什么是动态代理。

动态代理

动态代理其实是代理模式的一种,而代理模式其实是Java当中的一种设计模式。其结构如图所示:

在这里插入图片描述

这种模式就相当于一个公司生产了一个产品,但并不是由其开发人员去销售,而是由销售人员去销售。

代理模式的特点:

  • 代理模式中代理类和委托类是具有相同的接口。
  • 代理类的主要职责就是为委托类预处理消息,过滤消息等功能的实现。
  • 代理类的对象本身并不是真正的实现服务,而是通过委托类的对象的相关方法,来提供特定的一些服务。
  • 代理类和委托类之间存在关联关系,一个代理类的对象和一个委托类的对象相关联。
  • 访问实际对象,是通过代理对象来访问的。

代理模式的分类: 静态代理和动态代理。

  • 静态代理是在程序编译阶段就确定了代理对象。
  • 而动态代理是在程序运行阶段才确定代理对象。
  • 而在这里,我们仅仅来分析一下动态代理:

其中动态代理是在运行时根据Java代码指示动态生成的,相比较静态代理,优势在于方便对代理类的函数进行统一的处理,而不用修改每个代理类的方法。

Java中提供的动态代理方式有两种:JDK自带的动态代理CGLib实现的代理。下面来看看这两种方式的区别。

JDK自带的动态代理

JDK自带的代理方式是需要实现invocationHandler接口,并且实现invoke的方法来进行的。下面我们来自己实现一下这种代理方式:

  • 先提供一个共有的接口和委托类的实现。
  • 接口文件:
/**
 * 接口类
 * 	定义委托类和代理类共工的方法
 */
public interface IUser {
    void talk();
}
  • 委托实现类:
/**
 * 委托类
 * 	实现了接口Iuser中的talk方法
 */
public class User implements IUser {
    @Override
    public void talk() {
        System.out.println("doing User.talk");
    }
}
  • 要实现动态代理,需要首先创建一个实现了InvocationHandler接口的辅助类。
/**
 * 代理的辅助类
 */
public class UserProxy implements InvocationHandler {
    private Object object;
    public UserProxy(Object o) {
        this.object = o;
    }


    /**
     * 实现动态代理,就需要实现InvocationHandler接口中的invoke方法,该方法有三个参数
     * @param proxy :就是动态代理生成的代理对象
     * @param method:就是调用的方法
     * @param args:表示该方法的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理类提供委托类提供的功能,也可以提供自己特有的功能
        System.out.println("donging UserProxy.invoke");
        //调用委托类提供的方法
        method.invoke(object,args);
        System.out.println("donging UserProxy.invoke end");
        return null;
    }
}
  • 使用产生代理对象时,需要调用代理辅助类来调用委托方法。
public static void main(String[] args) {
    //产生代理对象
    IUser iUser = (IUser) Proxy.newProxyInstance(UserProxy.class.getClassLoader(), new Class[]{IUser.class}, new UserProxy(new User()));
    iUser.talk();
}
  • 日志打印:

在这里插入图片描述

  • 通过对日志的观察可以发现,当前的代理类在调用talk方法时,会调用到代理辅助类UserProxy中的invoke方法,并且还会调用到委托类中的talk实现。
  • 那么JVM是如何自动实现invoke方法的调用呢?
JDK自带代理方式的原理探究

在探究时,需要进行参数的配置。在代理类的主方法调用中添加一个监控操作。

public static void main(String[] args) {
    //看看JDK生成的自动代理类
    System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
    
    //产生代理对象
    IUser iUser = (IUser) Proxy.newProxyInstance(UserProxy.class.getClassLoader(), new Class[]{IUser.class}, new UserProxy(new User()));
    iUser.talk();
}
  • 通过执行后查看,发现JVM生成了一个$Proxy0.class文件,源码如下:

在这里插入图片描述

  • $Proxy0的定义中可知,它确实是实现了IUser接口,和代理模式下的代理是完全一致的。

在这里插入图片描述

  • 通过分析可得:当代理类实例中调用方法talk时,根据Java中的多态,调用的是$Proxy0的talk方法,而$Proxy0是重写接口中的talk方法。
CGLib实现的动态代理

这种代理方式提供了一种可扩展机制来控制被代理对象的访问操作,就是对对象的访问做了一层封装。当需要代理没有接口的类时,此时Proxy和InvocationHandler机制就不能使用了(原因是JDK自带的代理模式的使用必须需要有接口的存在),此时可以使用CGLib库,采用底层字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截父类所有方法的调用,这种方式则采用的是横切的逻辑。而Spring AOP(横向切面)的技术就是使用的这种动态代理模式。下面来看看这种方式的具体使用。

  • CGLib的使用需要引入第三方库。
<!--CGLib-->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.2</version>
</dependency>
  • 父类:
public class CGLibSuper {
    public void doing() {
        System.out.println("CGLibSuper.doing");
    }
}
  • 辅助类的实现:
/**
 * 通过已有的类产生代理类时
 * 	在当前辅助类需要实现MethodInterceptor接口
 */
public class CGLibProxy  implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();
    
    public <T> T getProxy(Class<T> clazz) {
        //设置需要创建子类的类
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        //通过字节码动态的创建子类实例
        return (T)enhancer.create();
    }
    
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLibProxy doing");
        //拦截到父类响应的方法
        Object o1 = methodProxy.invokeSuper(o, objects);
        System.out.println("CGLibProxy doing end");
        return o1;
    }
}
  • 通过CGLib实现代理:
public static void main(String[] args) {
    CGLibProxy cgLibProxy = new CGLibProxy();
    //动态产生的CGLibSuper的子类实例
    CGLibSuper proxy = cgLibProxy.getProxy(CGLibSuper.class);
    proxy.doing();
}
  • 执行结果:

在这里插入图片描述

  • 通过执行结果可知,当前产生的代理类调用相应的方法(doing),会被代理辅助类中的intercept方法拦截,在该方法中可以实现扩展器,从而也能调用到父类中的相应方法。
Mybatis的代理模式详解

在这里插入图片描述

MyBatis中产生的代理是哪种方式???

  • 答案是:JDK自带的方式,原因是在使用时仅在其中实现了接口。

mapper是如何添加进去的???

  • mapper是通过sqlSession拿到的,sqlSession是通过sqlSessionFactory拿到的,而sqlSessionFactory是通过读取XML配置源得到的。这种方式相当于直接在主程序中进行环境配置:new Environment()方法。如图所示:

在这里插入图片描述

  • 通过代码形式产生的会话工厂实例和读取配置形式是类似的。
  • 通过代码形式中可以看到,Mapper接口是通过Configuration.addMapper()形式来添加,参数为接口的class。
  • 类似于:
Configuration configuration = new Configuration();
configuration.addMapper(StudentMapper.class); //添加mapper

addMapper()方法的实现:

  • 观察源码可知:

  • Configuration类中:

public <T> void addMapper(Class<T> type) {
    this.mapperRegistry.addMapper(type);
}
  • mepper实际上被添加到了MapperRegistry中,继续观察MapperRegistry类可得:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) { //只添加接口
        if (this.hasMapper(type)) { //不允许接口重复添加
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }

        boolean loadCompleted = false;

        try {
            this.knownMappers.put(type, new MapperProxyFactory(type)); //将接口存放到HashMap中,重点
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                this.knownMappers.remove(type);
            }

        }
    }

}
  • 通过分析得:执行的Configuration.addMapper()操作,最终是被放入到HashMap中,其名为knownMappers,knownMappers是MapperRegistry中的属性,他是一个HashMap对象,key为当前Class对象,Value为MapperProxyFactory实例。

从getMapper入手分析:

  • sqlSession中调用getMapper()方法操作实际调用到DefaultSqlSession中的实现。
  • DefaultSqlSession类:
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}
  • 其中DefaultSqlSession没有逻辑,直接调用configuration中的getMapper方法,而configuration主要是将配置信息存放在当中。
  • configuration类中的操作:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
  • 其中getMapper的实现是调用mapperRegistry中的方法,而我们知道,mapperRegistry中实际上是存放了一个HashMap。
  • mapperRegistry类:
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            return mapperProxyFactory.newInstance(sqlSession); //重点看这里,通过这个操作返回了代理对象
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}
  • 调用sqlSession.getMapper()操作,最终会到达上面的这个方法,他会根据接口在HashMap中找到对应的Value值,是一个MapperProxyFactory的对象,然后通过调用该对象的newInstance()的方法,最终获取到代理对象。
  • MapperProxyFactory类:
public class MapperProxyFactory<T> {
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class<T> getMapperInterface() {
        return this.mapperInterface;
    }

    public Map<Method, MapperMethod> getMethodCache() {
        return this.methodCache;
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
        //JDK自带的代理方式生成映射器代理对象
        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);
    }
}
  • MapperProxy类:
public class MapperProxy<T> implements InvocationHandler, Serializable {
    //当前类实现了InvocationHandler接口
    
    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;
    }

    //实现invoke方法
    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 var5) {
                throw ExceptionUtil.unwrapThrowable(var5);
            }
        } else {
            MapperMethod mapperMethod = this.cachedMapperMethod(method); //使用缓存???
            //执行CRUD相关操作
            return mapperMethod.execute(this.sqlSession, args);
        }
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
        if (mapperMethod == null) {
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            this.methodCache.put(method, mapperMethod);
        }

        return mapperMethod;
    }
}
总结

Mapper是如何注册的,Mapper接口是如何产生动态代理对象的?

  • Mapper接口在初始sqlSessionFactory的时候进行注册。
  • Mapper接口注册到了mapperRegistry类HashMap中,key是Mapper的接口类class,value是当前的Mapper工厂。
  • Mapper注册之后,可以从sqlSession中的getMapper方法获取对象。
  • sqlSession.getMapper方法使用了JDK自带的动态代理,产生目标接口Mapper的代理对象。
  • 动态代理的代理辅助类是MapperProxy。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值