当前位置: 首页>前端>正文

springboot框架mybatis还需要tomact springboot的mybatis

1.Mybatis介绍

mybatis是一款轻量的半ORM框架(因为SQL要自己编写,所以是半ORM)和持久化框架,它将业务代码和SQL语句进行解耦,并且内部封装了JDBC,减免了开发者对数据库连接和释放等代码的控制,使开发者专注于SQL和业务代码的编写,并且提供了ORM映射(支持XML或注解方式配置映射信息),减少了大量getset代码的编写和JDBC代码的编写,还支持外部连接池。

半自动和全自动ORM框架

半自动:需要自己定义查询结果和目标对象属性的关联关系,用户编写完映射关系后才能够拿到对应的java对象

全自动:无需自己定义查询结果与对象属性的关联关系,无感知使用,全自动实现映射,用户拿到对象后直接使用即可

2.MyBatis在springboot初始化流程

MyBatis的自动化配置是在Mybatis自己的jar中设置自动配置类mybatis-spring-boot-autoconfigure.jar中的spring.factories

1.通过自动注册加载MybatisAutoConfiguration和MybatisLanguageDriverAutoConfiguration(对各个视图语言的支持)
2.MybatisAutoConfiguration负责创建sqlSessionFactory,sqlSessionTemplate和MapperScannerRegistrarNotFoundConfiguration,其中在创建MapperScannerRegistrarNotFoundConfiguration的时候有一个@Import注解,引入了MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class会将这个类注册为SpringBean(这个类为ImportBeanDefinitionRegistrar)。

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_spring boot,第1张

ImportBeanDefinitionRegistrar是SpringBoot中负责注册beanDefinition的接口,但是只能被@Import注解导入,使用者可以通过接口中的方法向springboot中添加beanDefinition,上面的AutoConfiguredMapperScannerRegistrar就引入了一个MapperScannerConfigurer

3.springboot在refresh容器的时候会将MapperScannerConfigurer实例化,因为它是一个BeanDefinitionRegistryPostProcessor,他会被springboot调用postProcessBeanDefinitionRegistry方法,在这个方法中会创建ClassPathMapperScanner进行mybatis的xml类的扫描,创建对应dao接口的beandefinition类,这个beanDefinition就是将来实例话dao接口的模板类


至此mybatis的所有相关的beanDefinition都已经被扫描到了spring容器中,接下来就是bean的实例话过程

4.实例化和正常实例化一样,都是在beanFactory的preInstantiateSingletons中完成的,sqlSessionFactory,sqlSessionTemplate就是普通的beanDefinition,其中sqlSessionFactory在创建的时候会进行所有xml的解析并将结果封装进config属性(每个sql都会封装成mappedStatement),自定义的dao接口的beandefition会被定义为factoryBean,这类bean不是直接创建的,而是先实例化factory对象,再调用getObject方法创建对应的dao的实现类,这个实现类是一个动态代理类MapperProxy,里面持有SqlSessionTemplate对象,该代理类只会代理业务方法,不会代理Object类中的方法。

sqlSessionFactory的datasource等属性都是从springboot容器中自动获取了,不再是进行繁琐的xml配置解析进行获取了,原来的xml配置都已经被自动注入类改写成从application.yml或者application.properties文件中获得了

至此MyBatis的所有类都已经注入到spring中,整个springboot的容器也已经启动起来,完成了各个bean的实例话,初始化和注入

3.MyBatis查询的具体执行流程

1.当请求接口类的方法时,由于spring注入的是代理类,所以会被请求进代理类MapperProxy,然后会封装对应的MapperMethod(会存储当前SQL的类型select,update等,还会存储方法的返回类型,方法返回的数据结构),然后会执行mapperMethod的execute方法,会传入一个SqlSession对象和方法执行对应的参数

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_二级缓存_02,第2张

但是这个SqlSession不是负责执行Sql的SqlSession,这是一个SqlSessionTemplate,里面存有SqlSessionFactory等对象

2.进入execute方法后根据mapperMethod返回值数据结构调用SqlSessionTemplate的selectOne/selectMap等方法

3.SqlSessionTemplate调用selectOne等方法会交给内部代理类执行(SqlSessionTemplate的内部类SqlSessionInterceptor)执行查询

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_mybatis_03,第3张

4.获取真正的SqlSession,SqlSession内会持有executor,如果开启了@Transcational注解则会将这个SqlSession放入TransactionSynchronizationManager中存储,保证一个事务使用的session是同一个,然后调用这个session的方法selectOne/selectMap等方法(这个方法是根据2.返回值判断来的)

5.上一部获取的SqlSession得到的是DefaultSqlSession,不论调用SelectOne还是SelectMap等方法最后真正执行的都是DefaultSqlSession中的SelectList方法,SelectList方法会先从configuration中获取mappedStatement,然后交给executor执行真正的查询

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_spring boot_04,第4张

6.执行executor的query方法后,如果开启了二级缓存会从mappedStatement中获得Cache,如果未开启二级缓存则会获得null的缓存,现在先不看开启二级缓存的情况,则会直接由executor执行query查询。

7.executor的query方法会先从本地缓存中查询(这个本地缓存就是一级缓存),如果有则会返回,如果没有则会创建jdbc prepareStatement进行查询,查询出结果后由typeHandler进行属性orm映射(该过程类似spring bean的创建过程,是先创建空对象,然后调用各个属性的set方法进行映射),映射完Object后返回数据。

4.Mybatis的缓存机制

Mybatis缓存是分为一级缓存和二级缓存,一级缓存是executor级别的,二级缓存是mappedStatement级别的(接口方法级)。一二级缓存执行流程盗图一下。

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_缓存_05,第5张

图中出现的第三方缓存库,因为Mybatis的缓存其实都是用HashMap来实现的,所以可以配置自定义的缓存类,在自定义缓存类中可以实现使用第三方的缓存库来实现缓存,比如Redis,MamerCache等,使用方法就是自定义实现Cache接口,将二级缓存的cache对象配置成自定义的这个Cache,就可以达到使用第三方缓存库来做二级缓存

4.1 一级缓存

Mybatis一级缓存是session级别的,因为这个缓存是存储在executor中的localCache中,而每个session中都只有一个executor,所以说一级缓存是session级别的,其实一级缓存就是一个hashMap,一级缓存也是默认开启的(代码就那么写的,你想关也关不上)。

生效条件
要想一级缓存生效就要保证是在同一个sqlSession内进行多次查询,并且每次查询的参数要一致。
有两种方法可以达到同一个sqlSession

1.在方法上加上@Transactional注解,spring的TranscationManager就会在一个方法执行过程中缓存你的sqlSession
2.自己在方法内调用SqlSessionFactory的openSession开启session,然后一直用这个session进行查询

springboot框架mybatis还需要tomact springboot的mybatis,springboot框架mybatis还需要tomact springboot的mybatis_spring boot_06,第6张

一级缓存会在执行insert,update,delete,commit,rollback方法后会清空。

缓存原理

mybatis原理就是靠执行器内的localCache缓存的,以下代码就是MyBatis的BaseExecutor执行查询的代码
本质上就是一个HashMap

protected PerpetualCache localCache;

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                //先从localCache中获得对象
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    //缓存获取不到再执行真正地查询
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }

                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }

            return list;
        }
    }

localCache是一个PerpetualCache对象,而这个类可以明显看到起缓存作用的就是一个HashMap

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();
}

一级缓存的优缺点
优点很明显,就是加快了同一个查询的速度,缺点也很明显,作用范围太小,一次请求过后一级缓存就会消失,因为一次请求的sqlSession不会保存到下一次,每个sqlSession的缓存不共通,不能互相访问

4.2 二级缓存

通常说二级缓存是namaspace级别的,其实更准确应该是方法方法级别的,因为这个cache是存储在mappedStatement当中的,由于这个mappedStatment是全局唯一的,所以它的缓存也是在全局中缓存的,就意味着多个SqlSession可以共享

mappedStatement对应的就是xml中的一个方法,在Mybatis解析xml的时候会将方法的Sql,sql类型,返回类型等信息封装成mappedStatement

生效条件

开启二级缓存首先要在application.yml中开启mybatis缓存

mybatis:
  configuration:
    cache-enabled: true

然后在对应的mapper.xml中开启缓存

<!-- 开启二级缓存 -->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"></cache>

缓存原理

与一级缓存不同,二级缓存的executor不再是BaseExecutor而是CachingExecutor,这是在执行获取SqlSession的时候创建的,会根据你是否在springboot配置中开启了mybatis缓存来包装你的executor,变成CachingExecutor,自然而然以后执行query查询的也是这个CachingExecutor

以下是CachingExecutor执行查询的方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        //这个cache是MappedStatement中的Cache,如果这就证明了二级缓存是方法级的,同样也是namespace级别的
        Cache cache = ms.getCache();
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                //这就是从二级缓存中获取缓存结果
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
                    //如果获取不到则使用委托类执行查询请求,这个委托类就是真正能执行查询的baseExecutor
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    //查询完成后将结果加入缓存中
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }
        //如果mappedStatement没有开启二级缓存的话则直接使用委托类执行查询
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

上面的tcm(TransactionalCacheManager)是事务缓存查询管理器管理的是每个mappedStatement的缓存,所有的二级缓存都存储在这个tcm中,这个缓存其实也就是一个HashMap,key是mappedStatment中的cache对象,value则是TransactionalCache真正缓存数据的cache

在SQLSession查询结束后会执行commit方法,这时候会将cachingExecutor中的缓存放入真正的二级缓存中,如果失败rollback则会将cachingExecutor中的缓存清空。

二级缓存具有过期时间,默认是一个小时,在获取缓存前会先判断缓存创建时间距离当前时间是否过了一个小时,如果过了则会将整个二级缓存清空。

4.3 总结

综合一级二级缓存来看,可以看出在一二级缓存同时生效的情况下,Mybatis是先从二级缓存中获取数据,获取不到再从一级缓存中获取数据,再获取不到则执行真正的查询。一级二级缓存都会在commit或者rollback后清空。区别在于一级缓存是真的清空,二级缓存是要在commit的时候将缓存刷入全局缓存中

5 MyBatis的动态标签

5.1 DDL类

INSERT:表示语句是插入语句
UPDATE:表示语句是更新语句
SELECT:表示语句是查询语句
DELETE:表示语句是删除语句
SQL:表示是一个SQL代码块

5.2 SQL类

if:当满足某个条件触发
choose,when,otherwise: 相当于多条件的if,else if,else
where:可以自动去除在where子句中出现的多个if中最前面的多余的and,防止出现where and condition1 and condition2的情况(会自动变成where condition1 and condition2)
set:类似于where标签,用在update的set子句中,自动去掉最开始的“,”
trim:自定义的去除两侧字符的标签,可以替代where或者set,里面有四个属性prefix替换的前缀,suffix替换的后缀,prefixOverrides被替换的前缀,需要多个被替换用“|”分割,suffixOverrides被替换的后缀
bind:自定义绑定的一个属性名比如gender,可以在后面的sql中使用#{gender}或者${gender}使用

5.3 映射类

collection:用于关联集合类映射(一对多/多对多)
association:用于关联对象映射(一对一)
resultMap:用于sql查询字段和对象映射

5.4 动态SQL执行原理

前期准备
要想实现SQL动态拼接,首先就是要解析xml中动态定义的标签,这个过程在创建SQLSessionFactory的过程中就会解析,通过xml解析器递归的解析自己定义的<select>,<insert>等标签,解析完成后形成一个树状的sqlSource对象,组装进MappedStatement.Builder对象,然后调用build方法,创建mappedStatement对象加入到SQLSessionFactory的configuration中

sqlSource对象有四个实现类,分别是DynamicSqlSource,ProviderSqlSource,RowSqlSource,StaticSqlSource

1.StaticSqlSource:用来存储纯静态的Sql,里面有个String类型的属性用来存储sql

2.DynamicSqlSource:用来存储动态的需要解析<if><where>等标签的sqlSource,内部结构有一个类型为SqlNode的rootSqlNode,树形存储Sql中定义的那些标签,标签内部的属性也会动态的存储到对应的Node中,例如if标签的test,用来在使用的时候能够进行动态的判断
(SqlNode实现类有ChooseSqlNode,ForEachSqlNode,IfSqlNode,MixedSqlNode,SetSqlNode,StaticTextSqlNode,TextSqlNode,TrimSqlNode,varDeclSqlNode,WhereSqlNode,每个Node用来存储哪些标签对应的Sql很清晰明了)

3.RowSqlSource:用来将Sql中的#{}替换为?,内部持有其他类型的SqlSource

使用阶段
解析完SqlSource之后,在实用阶段就是调用SqlSource中的getBoundSql,动态Sql主要就是DynamicSqlSource的getBoundSql中实现的,就是根据rootSqlNode调用apply方法将一个上下文对象传入,然后树形结构的rootSqlNode层层解析对应曾的参数来判断是否将对应的Sql拼入上下文的Sql中,这里test等各种判断条件使用的是Mybatis中OGNL表达式实现的。OGNL代码我看不懂了--!

下面是对应的代码

publicclass DynamicSqlSource implements SqlSource { 

  privatefinal Configuration configuration;
  privatefinal SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { 
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  //parameterObject 使用dao层接口时传入的参数
  public BoundSql getBoundSql(Object parameterObject) { 
    //动态Sql解析上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //rootSqlNode就是我们前面讲解的,把动态Sql解析成SqlNode对象。外层为MixedSqlNode节点,节点存储了
    //节点下的所有子节点。里面递归调用并根据传入参数的属性检查是否需要拼接sql
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //把我们动态Sql中的#{}替换成?
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { 
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

}

6 Mybatis的分页

6.1 基于SQL的分页

自定义分页查询SQL,如:select * from user limit (#{pageNum} - 1) * #{pageSize},#{pageSize}

缺点:代码复杂度高,对不同数据库查询语句不同

6.2 使用RowBounds进行分页

MyBatis提供了RowBounds进行分页,内部两个参数offset和limit

用法:

//接口中
List<User> selectUserByPage(RowBounds rowBounds);

//xml 正常写查询即可
<select id="selectUserByPage" resultType="xxx.xxx.xxx.User">
    select * from user
</select>

//业务代码
List<User> users = userMapper.selectUserByPage(new RowBounds(10,5));

优点:与数据库耦合度不大,使用方便
缺点:对数据库压力大,因为中间的分页查询结果会暂时缓存在数据库中,Mybatis是获取部分ResultSet然后进行内存分页

6.3 自定义Mybatis Interceptor

Mybatis支持自定义拦截器来进行statement,resultSet等对象的处理,因此我们可以在statemt执行前使用拦截器讲SQL进行修改。拦截器详细介绍在后面的章节介绍,在此只举例说明

用法:

//定义你的拦截器类
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DefinedPageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取StatementHandler,默认的是RoutingStatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取StatementHandler的包装类
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        //分隔代理对象
        while (metaObject.hasGetter("h")) {
            Object obj = metaObject.getValue("h");
            metaObject = SystemMetaObject.forObject(obj);
        }
        while (metaObject.hasGetter("target")) {
            Object obj = metaObject.getValue("target");
            metaObject = SystemMetaObject.forObject(obj);
        }
        //获取查看接口映射的相关信息
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String mapId = mappedStatement.getId();
        //拦截以ByInterceptor结尾的请求,统一实现分页
        if (mapId.matches(".+ByInterceptor$")) {
            System.out.println("LOG:已触发分页拦截器");
            //获取进行数据库操作时管理参数的Handler
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            // 分页对象需要自己构建,到时候分页方法中要传入 
            //获取请求时的参数
            PageInfo info = (PageInfo) parameterHandler.getParameterObject();
            //获取原始SQL语句
            String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
            //构建分页功能的SQL语句
            String sql = originalSql.trim() + " limit " + info.getPageNum() + ", " + info.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
        //调用原对象方法,进入责任链下一级
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        //生成Object对象的动态代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //如果分页每页数量是统一的,可以在这里进行统一配置,也就无需再传入PageInfo信息了
    }
}

    //创建自定义SqlSessionFactory,并把自定义拦截器加入Factory
    //也可以在XML中进行配置
    @Bean("oneSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        try {
            sqlSessionFactoryBean.setMapperLocations(
                    // 设置mybatis的xml所在位置
                    new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/one/*Mapper.xml"));
                    //设置自定义的插件
            sqlSessionFactoryBean.setPlugins(new DefinedPageInterceptor());
            return sqlSessionFactoryBean.getObject();
        } catch (Exception e) {
            return null;
        }
    }

//然后正常根据你定义的拦截器的拦截规则定义你的查询方法即可
//mapper接口
   /**
     * Interceptor 实现分页,必须以ByInterceptor结构,自定义的Interceptor才能
     * 识别出来,并且必须传入PageInfo
     * @param pageInfo 自定义的分页
     * @return
     */
    List<User> selectPageByInterceptor(PageInfo pageInfo);

//mapper XML定义
  <select id="selectPageByInterceptor" resultType="com.example.demo.mapper.one.User">
        select * from user
    </select>

缺点:和6.1本质上没有区别,对不同数据库也做不了共通支持,但是也可以在拦截器中做判断根据你的数据库自定义分页SQL(也算减少了代码的耦合度),用拦截器做分页感觉有些鸡肋,但是也是一个思路,类似于spring的AOP。同时和6.1共通的毛病,对数据量较多的表用SQL分页在后面的页的时候都会造成查询慢和性能浪费

6.4 使用第三方分页插件

例如 pagehelper,使用方法在此不再赘述,使用比较方便请自行百度

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>

7 Mybatis的延迟加载

7.1 什么是延迟加载

Mybatis中Mapper配置文件中的resultMap可以实现高级映射(使用association、collection实现一对一及一对多(多对多)映射),同样的association、collection具备延迟加载功能。所谓延迟加载,就是先单表查询,需要时再从关联表去关联查询(同样也可能只是是单表查询),大大单表查询速度更快,所以可以间接的提高数据库性能。

延迟加载是为了解决我们有时候只需要单表数据即可完成业务处理,不需要查询关联表,而在需要关联表的数据时可以发起二次查询,这样就可以减少我们业务处理时间。

7.2 使用方法

xml配置

<settings>
  <!--懒加载模式在Mybatis中默认是关闭的-->
  <setting name="lazyLoadingEnabled" value="true"/>
  <!--不同于懒加载的:积极加载方式,所以在懒加载的时候设置该属性为false-->
  <setting name="aggressiveLazyLoading" value="false"></setting>
 </settings>

application.yml配置

mybatis:
    configuration:
        lazy-loading-enable:true
        aggressive-lazy-loading:false

两者选一配置即可

7.3 原理

使用CGLIB创建目标对象的代理对象,调用方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null,就会单独发送事先保存好的查询关联B对象的sql,把B查询出来。然后调用a.setB(b),这样a对象的b属性就会有值了,接着完成a.getB().getName()方法的调用,这就是延迟加载的基本原理

8 Mybatis拦截器

8.1 什么是拦截器?

MyBatis的拦截器顾名思义,就是对某些操作进行拦截。通过拦截器可以对某些方法执行前后进行拦截,添加一些处理逻辑。

MyBatis的拦截器可以对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理。

拦截器设计的初衷就是为了让用户在MyBatis的处理流程中不必去修改MyBatis的源码,能够以插件的方式集成到整个执行流程中。

比如MyBatis中的Executor有BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor,如果这几种实现的query方法都不能满足你的需求,我们可以不用去直接修改MyBatis的源码,而通过建立拦截器的方式,拦截Executor接口的query方法,在拦截之后,实现自己的query方法逻辑。

8.2 使用方法

/**

 * mybatis 自定义拦截器

 * 三步骤:

 * 1 实现 {@link Interceptor} 接口

 * 2 添加拦截注解 {@link Intercepts}

 * 3 配置文件中添加拦截器

 *

 * 1 实现 {@link Interceptor} 接口

 *   具体作用可以看下面代码每个方法的注释

 * 2 添加拦截注解 {@link Intercepts}

 *   mybatis 拦截器默认可拦截的类型只有四种,即四种接口类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler

 *   对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口

 *   具体规则如下:

 *     a:Intercepts 拦截器: 标识我的类是一个拦截器

 *     b:Signature 署名: 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法

 *       type  对应四类接口中的某一个,比如是 Executor

 *       method 对应接口中的哪类方法,比如 Executor 的 update 方法

 *       args  对应接口中的哪一个方法,比如 Executor 中 query 因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法

 * 3 配置文件中添加拦截器

 *   拦截器其实就是一个 plugin,在 mybatis 核心配置文件中我们需要配置我们的 plugin :

 *     <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor">

 *       <property name="username" value="LiuYork"/>

 *       <property name="password" value="123456"/>

 *     </plugin>

 * 或者自定义@Configuration可以自定义SqlSessionFactory
 
 *  @Bean("oneSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        try {
            sqlSessionFactoryBean.setMapperLocations(
                    // 设置mybatis的xml所在位置
                    new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/one/*Mapper.xml"));
                    //设置自定义的插件
            sqlSessionFactoryBean.setPlugins(new DefinedPageInterceptor());
            return sqlSessionFactoryBean.getObject();
        } catch (Exception e) {
            return null;
        }
    }


 * 拦截器顺序

 * 1 不同拦截器顺序:

 *   Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

 *

 * 2 对于同一个类型的拦截器的不同对象拦截顺序:

 *   在 mybatis 核心配置文件根据配置的位置,拦截顺序是 从上往下

 */

@Intercepts({

    @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),

    @Signature(method = "query", type = StatementHandler.class, args = {Statement.class, ResultHandler.class})

})

public class MyInterceptor implements Interceptor {

 

  /**

   * 这个方法很好理解

   * 作用只有一个:我们不是拦截方法吗,拦截之后我们要做什么事情呢?

   *   这个方法里面就是我们要做的事情

   *

   * 解释这个方法前,我们一定要理解方法参数 {@link Invocation} 是个什么鬼?

   * 1 我们知道,mybatis拦截器默认只能拦截四种类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler

   * 2 不管是哪种代理,代理的目标对象就是我们要拦截对象,举例说明:

   *   比如我们要拦截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法,

   *   那么 Invocation 就是这个对象,Invocation 里面有三个参数 target method args

   *     target 就是 Executor

   *     method 就是 update

   *     args  就是 MappedStatement ms, Object parameter

   *

   *  如果还是不能理解,我再举一个需求案例:看下面方法代码里面的需求

   *

   * 该方法在运行时调用

   */

  @Override

  public Object intercept(Invocation invocation) throws Throwable {

 

    /*

     * 需求:我们需要对所有更新操作前打印查询语句的 sql 日志

     * 那我就可以让我们的自定义拦截器 MyInterceptor 拦截 Executor 的 update 方法,在 update 执行前打印sql日志

     * 比如我们拦截点是 Executor 的 update 方法 : int update(MappedStatement ms, Object parameter)

     *

     * 那当我们日志打印成功之后,我们是不是还需要调用这个query方法呢,如何如调用呢?

     * 所以就出现了 Invocation 对象,它这个时候其实就是一个 Executor,而且 method 对应的就是 query 方法,我们

     * 想要调用这个方法,只需要执行 invocation.proceed()

     */

 

    /* 因为我拦截的就是Executor,所以我可以强转为 Executor,默认情况下,这个Executor 是个 SimpleExecutor */

    Executor executor = (Executor)invocation.getTarget();

 

    /*

     * Executor 的 update 方法里面有一个参数 MappedStatement,它是包含了 sql 语句的,所以我获取这个对象

     * 以下是伪代码,思路:

     * 1 通过反射从 Executor 对象中获取 MappedStatement 对象

     * 2 从 MappedStatement 对象中获取 SqlSource 对象

     * 3 然后从 SqlSource 对象中获取获取 BoundSql 对象

     * 4 最后通过 BoundSql#getSql 方法获取 sql

     */

    MappedStatement mappedStatement = ReflectUtil.getMethodField(executor, MappedStatement.class);

    SqlSource sqlSource = ReflectUtil.getField(mappedStatement, SqlSource.class);

    BoundSql boundSql = sqlSource.getBoundSql(args);

    String sql = boundSql.getSql();

    logger.info(sql);

 

    /*

     * 现在日志已经打印,需要调用目标对象的方法完成 update 操作

     * 我们直接调用 invocation.proceed() 方法

     * 进入源码其实就是一个常见的反射调用 method.invoke(target, args)

     * target 对应 Executor对象

     * method 对应 Executor的update方法

     * args  对应 Executor的update方法的参数

     */

 

    return invocation.proceed();

  }

 

  /**

   * 这个方法也很好理解

   * 作用就只有一个:那就是Mybatis在创建拦截器代理时候会判断一次,当前这个类 MyInterceptor 到底需不需要生成一个代理进行拦截,

   * 如果需要拦截,就生成一个代理对象,这个代理就是一个 {@link Plugin},它实现了jdk的动态代理接口 {@link InvocationHandler},

   * 如果不需要代理,则直接返回目标对象本身

   *

   * Mybatis为什么会判断一次是否需要代理呢?

   * 默认情况下,Mybatis只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler

   * 通过 {@link Intercepts} 和 {@link Signature} 两个注解共同完成

   * 试想一下,如果我们开发人员在自定义拦截器上没有指明类型,或者随便写一个拦截点,比如Object,那Mybatis疯了,难道所有对象都去拦截

   * 所以Mybatis会做一次判断,拦截点看看是不是这四个接口里面的方法,不是则不拦截,直接返回目标对象,如果是则需要生成一个代理

   *

   * 该方法在 mybatis 加载核心配置文件时被调用

   */

  @Override

  public Object plugin(Object target) {

    /*

     * 看了这个方法注释,就应该理解,这里的逻辑只有一个,就是让mybatis判断,要不要进行拦截,然后做出决定是否生成一个代理

     *

     * 下面代码什么鬼,就这一句就搞定了?

     * Mybatis判断依据是利用反射,获取这个拦截器 MyInterceptor 的注解 Intercepts和Signature,然后解析里面的值,

     * 1 先是判断要拦截的对象是四个类型中 Executor、StatementHandler、ParameterHandler、 ResultSetHandler 的哪一个

     * 2 然后根据方法名称和参数(因为有重载)判断对哪一个方法进行拦截 Note:mybatis可以拦截这四个接口里面的任一一个方法

     * 3 做出决定,是返回一个对象呢还是返回目标对象本身(目标对象本身就是四个接口的实现类,我们拦截的就是这四个类型)

     *

     * 好了,理解逻辑我们写代码吧~~~ What !!! 要使用反射,然后解析注解,然后根据参数类型,最后还要生成一个代理对象

     * 我一个小白我怎么会这么高大上的代码嘛,怎么办?

     *

     * 那就是使用下面这句代码吧 哈哈

     * mybatis 早就考虑了这里的复杂度,所以提供这个静态方法来实现上面的逻辑

     */

    return Plugin.wrap(target, this);

  }

 

  /**

   * 这个方法最好理解,如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,

   * 类似Spring中的@Value("${}")从application.properties文件获取

   * 这个时候我们就可以使用这个方法

   *

   * 如何使用?

   * 只需要在 mybatis 配置文件中加入类似如下配置,然后 {@link Interceptor#setProperties(Properties)} 就可以获取参数

   *   <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor">

   *      <property name="username" value="LiuYork"/>

   *      <property name="password" value="123456"/>

   *   </plugin>

   *   方法中获取参数:properties.getProperty("username");

   *

   * 问题:为什么要存在这个方法呢,比如直接使用 @Value("${}") 获取不就得了?

   * 原因是 mybatis 框架本身就是一个可以独立使用的框架,没有像 Spring 这种做了很多依赖注入的功能

   *

   * 该方法在 mybatis 加载核心配置文件时被调用

   */

  @Override

  public void setProperties(Properties properties) {

    String username = properties.getProperty("username");

    String password = properties.getProperty("password");

    // TODO: 2019/2/28 业务逻辑处理...

  }

}

https://www.xamrdz.com/web/2dw1964361.html

相关文章: