Mybatis插件原理及Spring集成

一,MyBatis 插件原理与自定义插件

1,问题

MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理 SQL,处理结果。

问题一:

不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?

代理模式,这也确实是Mybatis的插件原理。

问题二:

我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是 HR 审批,再到总经理审批,怎么实现层层的拦截?

插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

问题三:

如果是用代理模式,有哪些对象允许被代理?有哪些方法可以被拦截?

在这里插入图片描述

Executor 会拦截到 CachingExcecutor 或者 BaseExecutor。因为创建 Executor 时是先创建 Executor,再拦截。

问题四:

如果我们用 JDK 的动态代理,要有一个实现了 InvocationHandler 的代理类,用来包装被代理对象,这个类是我们自己创建还是谁来创建?

什么时候创建代理对象?是在 MyBatis 启动的时候创建,还是调用的时候创建?

被代理后,调用的是什么方法?怎么调用到原被代理对象的方法(比如Executor 的 query()方法)?

2,插件编写与注册

(基于 spring-mybatis)运行自定义的插件,需要 3 步,我们以 PageHelper 为例:

1)、编写自己的插件类

1)实现 Interceptor 接口

这个是所有的插件必须实现的接口。

2)添加@Intercepts({@Signature()}),指定拦截的对象和方法、方法参数,方法名称+参数类型,构成了方法的签名,决定了能够拦截到哪个方法。

问题:拦截签名跟参数的顺序有关系吗?

3)实现接口的 3 个方法

// 用于覆盖被拦截对象的原有方法(在调用代理对象 Plugin 的 invoke()方法时被调用)
Object intercept(Invocation invocation) s throws Throwable;
// target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
Object plugin(Object target);
// 设置参数
 void setProperties(Properties properties);

2)、插件注册

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <property name="offsetAsPageNum" value="true"/>
        <!--……后面全部省略……-->
    </plugin>
</plugins>

3)、插件登记

MyBatis 启 动 时 扫 描 标 签 , 注 册 到 Configuration 对 象 的InterceptorChain 中。property 里面的参数,会调用 setProperties()方法处理。

3,代理和拦截是怎么实现的

四大对象什么时候被代理,也就是:代理对象是什么时候创建的?

Executor 是 openSession() 的 时 候 创 建 的 ; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的 ParameterHandler 和处理结果集的 ResultSetHandler 的创建,创建之后即调用 InterceptorChain.pluginAll(),返回层层代理后的对象。

多个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系?

可以被代理,debug。

在这里插入图片描述

谁来创建代理对象?

Plugin 类 。 在 我 们 重 写 的 plugin() 方 法 里 面 可 以 直 接 调 用 return Plugin.wrap(target, this);返回代理对象。

被代理后,调用的是什么方法?怎么调用到原被代理对象的方法?

因为代理类是 Plugin,所以最后调用的是 Plugin 的 invoke()方法。它先调用了定义的拦截器的 intercept()方法。可以通过 invocation.proceed()调用到被代理对象被拦截的方法。

Mybatis插件调用流程

在这里插入图片描述

对象作用
Interceptor自定义插件需要实现接口,实现 3 个方法
InterceptChainInterceptChain 配置的插件解析后会保存在 Configuration 的InterceptChain 中
Plugin用来创建代理对象,包装四大对象
Invocation对被代理类进行包装,可以调用 proceed()调用到被拦截的方法

4,PageHelper原理

1)用法

PageHelper. startPage (pn, 10);
List<Employee> emps =  employeeService.getAll();
PageInfo page = w new PageInfo(emps, 10);

2)源码

先看 PageHelper jar 包中 PageInterceptor 的源码。拦截的是 Executor 的两个query()方法。在这里对 SQL 进行了改写

跟踪到最后,是在 MySqlDialect.getPageSql()对 SQL 进行了改写,翻页参数是从一个 Page 对象中拿到的,那么 Page 对象是怎么传到这里的呢?

上一步,AbstractHelperDialect.getPageSql()中:Page 对象是从一个 ThreadLocal<>变量中拿到的,那它是什么时候赋值的?

PageHelper.startPage()方法,把分页参数放到了 ThreadLocal<>变量中。

5,应用场景分析

作用实现方式
水平分表对 query update 方法进行拦截在接口上添加注解,通过反射获取接口注解,根据注解上配置的参数进行分表,修改原 SQL,例如 id 取模,按月分表
数据加解密update——加密;query——解密获得入参和返回值
菜单权限控制对 query 方法进行拦截在方法上添加注解,根据权限配置,以及用户登录信息,在 SQL 上加上权限过滤条件

二,与 Spring 整合分析

大部分时候我们不会在项目中单独使用 MyBatis 的工程,而是集成到 Spring 里面使用,但是却没有看到这三个对象在代码里面的出现。我们直接注入了一个 Mapper 接口,调用它的方法。

SqlSessionFactory 是什么时候创建的?

SqlSession 去哪里了?为什么不用它来 getMapper?

为什么@Autowired 注入一个接口,在使用的时候却变成了代理对象?在 IOC的容器里面我们注入的是什么? 注入的时候发生了什么事情?

1,关键配置

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>

然后在 Spring 的 applicationContext.xml 里面配置 SqlSessionFactoryBean,它是用来帮助我们创建会话的,其中还要指定全局配置文件和 mapper 映射器文件的路径。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="configLocation" value="classpath:mybatis-config.xml"></ property>
    <property name="mapperLocations" value="classpath:mapper/*.xml"></ property>
    <property name="dataSource" ref="dataSource"/>
</ bean>

然后在 applicationContext.xml 配置需要扫描 Mapper 接口的路径。

<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.yhd.crud.dao"/>
</ bean>
<mybatis-springn:scan  base-package ="com.yhd.crud.dao"/>
@MapperScan( "com.yhd.crud.dao")

Spring 对 MyBatis 的对象进行了管理,但是并不会替换 MyBatis 的核心对象。也就意味着:MyBatis jar 包中的 SqlSessionFactory、SqlSession、MapperProxy 这些都会用到。而 mybatis-spring.jar 里面的类只是做了一些包装或者桥梁的工作。

2,创建会话工厂

我们在 Spring 的配置文件中配置了一个 SqlSessionFactoryBean,我们来看一下这个类。

在这里插入图片描述

它实现了 InitializingBean 接口,所以要实现 afterPropertiesSet()方法,这个方法会在 bean 的属性值设置完的时候被调用

另外它实现了 FactoryBean 接口,所以它初始化的时候,实际上是调用 getObject()方法,它里面调用的也是 afterPropertiesSet()方法。

在 afterPropertiesSet()方法里面:解析配置文件,指定事务工厂。

3,创建SqlSession

1)可以直接使用 DefaultSqlSession 吗?

现在已经有一个 DefaultSqlSessionFactory,按照编程式的开发过程,我们接下来就会创建一个 SqlSession 的实现类,但是在 Spring 里面,我们不是直接使用DefaultSqlSession 的,而是对它进行了一个封装,这个 SqlSession 的实现类就是SqlSessionTemplate。这个跟 Spring 封装其他的组件是一样的,比如 JdbcTemplate,RedisTemplate 等等,也是 Spring 跟 MyBatis 整合的最关键的一个类。

为什么不用 DefaultSqlSession?它是线程不安全的,注意看类上的注解:而 SqlSessionTemplate 是线程安全的。

Note that this class is not Thread-Safe.

2)怎么拿到一个 SqlSessionTemplate ?

MyBatis提供了一个 SqlSessionDaoSupport,里面持有一个SqlSessionTemplate 对象,并且提供了一个 getSqlSession()方法,让我们获得一个SqlSessionTemplate。

public  abstract  class SqlSessionDaoSupport  extends DaoSupport {
     private SqlSessionTemplate  sqlSessionTemplate;
     public SqlSession getSqlSession() {
     	return  this.sqlSessionTemplate;
    }
}

先创建一个 BaseDao 继承 SqlSessionDaoSupport。在BaseDao 里面封装对数据库的操作,包括 selectOne()、selectList()、insert()、delete()这些方法,子类就可以直接调用。

然后让我们的实现类继承 BaseDao 并且实现我们的 DAO 层接口,这里就是我们的Mapper 接口。实现类需要加上@Repository 的注解。

在实现类的方法里面,我们可以直接调用父类(BaseDao)封装的 selectOne()方法,那么它最终会调用 sqlSessionTemplate 的 selectOne()方法。

3)有没有更好的拿到 SqlSessionTemplate

我们的每一个 DAO 层的接口(Mapper 接口也属于),如果要拿到一个 SqlSessionTemplate,去操作数据库,都要创建实现一个实现类,加上@Repository 的注解,继承 BaseDao,这个工作量也不小。

另外一个,我们去直接调用 selectOne()方法,还是出现了 Statement ID 的硬编码,MapperProxy 在这里根本没用上。

4,接口的扫描注册

在 applicationContext.xml 里 面 配 置 了 一 个MapperScannerConfigurer。

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,BeanDefinitionRegistryPostProcessor 是 BeanFactoryPostProcessor 的子类,可以通过编码的方式修改、新增或者删除某些 Bean 的定义。

在这里插入图片描述

我们只需要重写 postProcessBeanDefinitionRegistry()方法,在这里面操作 Bean就可以了。

在这个方法里面:

scanner.scan() 方 法 是 ClassPathBeanDefinitionScanner 中 的 , 而 它 的 子 类ClassPathMapperScanner 覆 盖 了 doScan() 方 法 , 在 doScan() 中 调 用 了processBeanDefinitions,它先调用父类的 doScan()扫描所有的接口。

processBeanDefinitions 方法里面,在注册 beanDefinitions 的时候,BeanClass被改为 MapperFactoryBean(注意灰色的注释)。

为什么要把 BeanClass 修改成 MapperFactoryBean,这个类有什么作用?

MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 可 以 拿 到SqlSessionTemplate。

5,接口注入使用

我们使用 Mapper 的时候,只需要在加了 Service 注解的类里面使用@Autowired注入 Mapper 接口就好了。

 @Service
 public  class EmployeeService {
    @Autowired
    EmployeeMapper  employeeMapper;
     
     public List<Employee> getAll() {
     	return  employeeMapper.selectByMap( null);
     }
}

Spring 在启动的时候需要去实例化 EmployeeService。

EmployeeService 依赖了 EmployeeMapper 接口(是 EmployeeService 的一个属性)。

Spring 会根据 Mapper 的名字从 BeanFactory 中获取它的 BeanDefination,再从BeanDefination 中 获 取 BeanClass , EmployeeMapper 对 应 的 BeanClass 是MapperFactoryBean(上一步已经分析过)。

接下来就是创建 MapperFactoryBean,因为实现了 FactoryBean 接口,同样是调用 getObject()方法。

// MapperFactoryBean.java
 public T getObject()  throws Exception {
 return getSqlSession().getMapper( this. mapperInterface);
}

因为 MapperFactoryBean 继 承 了 SqlSessionDaoSupport , 所 以 这 个getSqlSession()就是调用父类的方法,返回 SqlSessionTemplate。

// SqlSessionDaoSupport.java
public SqlSession getSqlSession() {
 return  this. sqlSessionTemplate;
}

我们注入到 Service 层的接口,实际上还是一个 MapperProxy 代理对象。所以最后调用 Mapper 接口的方法,也是执行 MapperProxy 的 invoke()方法。

DaoSupport , 所 以 这 个getSqlSession()就是调用父类的方法,返回 SqlSessionTemplate。

// SqlSessionDaoSupport.java
public SqlSession getSqlSession() {
 return  this. sqlSessionTemplate;
}

我们注入到 Service 层的接口,实际上还是一个 MapperProxy 代理对象。所以最后调用 Mapper 接口的方法,也是执行 MapperProxy 的 invoke()方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值