30.MyBatis插件原理与Spring集成

目录

  1. 插件使用与原理
  2. 编写自定义插件
  3. 掌握Spring集成myBatis原理

1.插件使用与原理

1.1.插件使用

编写插件

编写拦截器类,以PageHelper为例

1)实现Interceptor接口

public class PageInterceptor implements Interceptor {
    
    

2)实现方法。intercept就是拦截方法,增强代码写里面。

image-20200513193405564

3)在拦截器类上加上参数。注解签名注明拦截对象、拦截方法、拦截方法参数。

下面拦截Executor中的两个query方法。

@Intercepts(
    {
    
    
        @Signature(type = Executor.class, method = "query", args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)

插件配置

在mybatis-config.xml注册插件,配置属性

<!--分页插件的注册-->
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
        <property name="dialect" value="mysql"/>  -->
        <!-- 该参数默认为false -->
        <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
        <!-- 和startPage中的pageNum效果一样-->
        <property name="offsetAsPageNum" value="true"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
        <property name="rowBoundsWithCount" value="true"/>
        <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
        <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
        <property name="pageSizeZero" value="true"/>
        <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
        <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
        <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
        <property name="reasonable" value="true"/>
        <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
        <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
        <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
        <!-- 不理解该含义的前提下,不要随便复制该配置 -->
        <property name="params" value="pageNum=start;pageSize=limit;"/>
        <!-- 支持通过Mapper接口参数来传递分页参数 -->
        <property name="supportMethodsArguments" value="true"/>
        <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
        <property name="returnPageInfo" value="check"/>
    </plugin>
</plugins>

解析注册插件

myBatis启动时扫描<plugins>标签,注册到Configuration对象的InterceptorChain中。通过setProperties将参数放到property里。

XMLConfigBuilder类

  private void pluginElement(XNode parent) throws Exception {
    
    
    if (parent != null) {
    
    
      for (XNode child : parent.getChildren()) {
    
    
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

解析时将所有插件存到Configuration的InterceptorChain中,它是list类型。

public class InterceptorChain {
    
    

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    
    
    for (Interceptor interceptor : interceptors) {
    
    
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    
    
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    
    
    return Collections.unmodifiableList(interceptors);
  }

}

1.2.插件理解

不修改代码怎么增强功能?

代理模式,myBatis插件实现原理

多个插件怎么拦截?

责任链模式,链路执行,层层拦截。

什么对象可以被拦截?

有哪些对象和方法可以被拦截?

https://mybatis.org/mybatis-3/zh/configuration.html#plugins

下面两张图:

image-20200513200921903

image-20200513201032407

Executor有可能被二级缓存装饰。

Executor会拦截CachingExecutor或BaseExecutor。

DefaultSqlSessionFactory.openSessionFromDataSource():

image-20200513202001530

先创建基本类型,再二级缓存装饰,最后插件拦截。

所以拦截的是CachingExecutor。

1.3.插件实现原理

代理模式,需要解决的问题:

1.代理类怎么创建?

2.什么时候创建?

3.调用流程什么样?

代理类什么时候创建?

Executor拦截代理类是openSession时创建

怎么创建?

遍历InterceptorChain,使用Interceptor实现类的plugin方法,对目标对象进行代理。

public Object pluginAll(Object target) {
    
    
  for (Interceptor interceptor : interceptors) {
    
    
    target = interceptor.plugin(target);
  }
  return target;
}
@Override
public Object plugin(Object target) {
    
    
    return Plugin.wrap(target, this);
}

这个plugin方法是自己实现的,返回一个代理对象。

JDK动态代理,需要实现InvocationHandler接口触发管理类。

用Proxy.nexProxyInstance创建对象。

myBatis插件机制将这些类封装好了,提供了一个触发管理类Plugin,

实现了InvocationHandler。

创建代理对象的newProxyInstance方法也进行了封装,就是wrap。

public static Object wrap(Object target, Interceptor interceptor) {
    
    
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    
    
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

被代理后的调用流程

先触发管理类Plugin的invoke方法

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
  try {
    
    
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
    
    
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    
    
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

如果被拦截方法不为空,进入Plugin的invoke方法,调用interceptor的intercept方法,到我们自己实现的拦截逻辑。

return interceptor.intercept(new Invocation(target, method, args));

new Invocation(target, method, args) 对象是对被拦截对象、方法、参数的封装。

被代理对象执行它的方法从Invocation对象拿。

总结:

image-20200513210358416

配置顺序与执行顺序?

配置与执行顺序是相反的。

InterceptorChain从上往下添加,执行从最后开始。

image-20200513211041190

总结

image-20200513211329804

1.4.PageHelper原理

翻页

使用RowBounds翻页,在内存中筛选数据。

使用

public String getEmps(@RequestParam(value = "pn", defaultValue = "1") Integer pn, Model model) {
    
    
    PageHelper.startPage(pn, 10);
    List<Employee> emps = employeeService.getAll();
    PageInfo page = new PageInfo(emps, 10);
    //连续显示的页数是10页
    //包装查出来的结果,只需要将pageInfo交给页面,封装了详细的分页信息
    //包括查询出来的数据
    model.addAttribute("pageInfo", page);

    return "list";
}

原理

拦截器类PageInterceptor。

先判断是否需要count获得总数,默认true。

获得count后,判断是否需要分页,如果pageSize>0,就分页。

下面通过getPageSql方法生成新BoundSql:

image-20200513212633576

getPageSql对不同数据库有不同实现

image-20200513213223301

实际是添加了LIMIT语句,加上了起始与结束。

image-20200513213358885

插件是如何获取页码和每页数量?

PageHelper.startPage方法,调用了PageMethod的setLocalPage方法,包装了一个Page对象,并且把对象放到ThreadLocal中。

image-20200513213835626

AbstractHelperDialect中,Page对象中的翻页信息是通过getLocalPage()取出的:

image-20200513214158800

调的就是PageHelper的getLocalPage,从ThreadLocal中获取到

image-20200513214245386

每次查询(每个线程)都有线程私有Page对象,里面有页码和每页数量。

关键类

image-20200513214332501

1.5.应用场景分析

image-20200513214416419

image-20200513214444323

2.与Spring 整合分析

2.1.关键配置

pom依赖

出了mybatis依赖,还需要mybais和spring整合包。

叫mybatis-spring。版本要对应。

<!--mybatis 和Spring整合 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${mybatis-spring.version}</version>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis.version}</version>
</dependency>

SqlSessionFactoryBean

applicationContext.xml配置这个类。

这个Bean会初始化SqlSessionFactory,用来创建SqlSession。

属性要指定mybatis-config.xml和Mapper映射器文件。

<!-- 在Spring启动时创建 sqlSessionFactory -->
<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>

MapperScannerConfigurer

applicationContext.xml配置扫描Mapper接口路径。

方法一:

<!--配置扫描器,将mybatis的接口实现加入到  IOC容器中  -->
<mybatis-spring:scan #base-package="com.gupaoedu.crud.dao"/>

方法二:

<bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.gupaoedu.crud.dao"/>
</bean>

方法三:

image-20200513221322936

Spring集成mybatis原理:

1)SqlSessionFactory在哪创建?

2)SqlSession在哪创建?

3)代理类在哪创建?

2.2.创建会话工厂SqlSessionFactory

image-20200513221618565

实现了三个接口:InitializingBean、FactoryBean、ApplicationListener

image-20200513221659140

InitializingBean

实现AfterPropertiesSet方法,在bean属性值设置完后调用

image-20200513222010210

调用了buildSqlSesssionFactory方法。

创建Configuration对象。

创建解析全局配置文件XMLConfigBuilder。

image-20200513222146162

FactoryBean接口

让用户自定义实例化Bean逻辑。

获取SqlSessionFactoryBean,就会调用它的getObject方法。

getObject方法调用了afterPropertiesSet方法,做mybatis解析配置,返回DefaultSqlSessionFactory。

image-20200513222955430

ApplicationListener

监听ContextRefreshedEven(上下文刷新实践),会在SPring容器加载完后执行。

检查ms是否加载完毕。

image-20200513223120564

SqlSessionFactoryBean用到的Spring扩展点

image-20200513223151002

2.3.创建会话SqlSession

DefaultSqlSession是线程不安全的。

image-20200513223248486

mybatis-spring包,提供了线程安全的SqlSession包装类,SqlSessionTemplate。

可以在所有DAO层共享实例(默认单例)

image-20200513223739900

SqlSessionTemplate,增删改查都是调用代理对象的方法。

image-20200513224100671

代理对象在构造方法通过JDK动态代理创建:

image-20200513224225317

怎么拿到一个SqlSessionTemplate?

提供抽象支持类SqlSessionDaoSupport,持有一个SqlSessionTemplate对象,提供getSqlSession方法。

image-20200513225010059

在实现类得方法里,可以直接调用父类封装的selectOne方法,

最终会调用sqlSessionTemplate的selectOne方法。

image-20200513225620053

2.4.接口的扫描注册

MapperScannerConfigurer用来扫描Mapper接口的。

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。

BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor子类,里面有一个postProcessBeanDefintionRegistry方法。

image-20200513230645916

MapperScannerConfigurer重写了postProcessBeanDefintionRegistry方法。

创建了scanner对象,设置属性。

2.5.接口注入使用

Spring如何把 mybatis集成进去?

1.提供SqlSession替代品SqlSessionTemplate,里面有一个实现InvocationHandler的内部SqlSessionInterceptor,本质是对SqlSession的代理.

2.提供获取SqlSessionTemplate的抽象类SqlSessionDaoSupport

3.扫描Mapper接口,注册到容器中的是MapperFactoryBean

4.把Mapper注入使用的时候,调用的是getObject方法

5.执行Mapper接口任意方法,会走到触发管理类MapperProxy,进去SQL处理流程

学到了?

1.为组件预留扩展接口

2.利用Spring扩展机制,把组件集成到mybatis中

image-20200513235325925

设计模式总结:

image-20200513235342958

参考资料:

1.咕泡学院·MyBatis插件原理与Spring集成·青山

猜你喜欢

转载自blog.csdn.net/u012138605/article/details/106116957