30.MyBatis插件原理与Spring集成

目录

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

1.插件使用与原理

1.1.插件使用

编写插件

编写拦截器类,以PageHelper为例

1)实现Interceptor接口

public class PageInterceptor implements Interceptor {

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

30.MyBatis插件原理与Spring集成_第1张图片

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">
        
        
        
        
        <property name="offsetAsPageNum" value="true"/>
        
        
        <property name="rowBoundsWithCount" value="true"/>
        
        
        <property name="pageSizeZero" value="true"/>
        
        
        
        <property name="reasonable" value="true"/>
        
        
        
        
        <property name="params" value="pageNum=start;pageSize=limit;"/>
        
        <property name="supportMethodsArguments" value="true"/>
        
        <property name="returnPageInfo" value="check"/>
    plugin>
plugins>

解析注册插件

myBatis启动时扫描标签,注册到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

下面两张图:

30.MyBatis插件原理与Spring集成_第2张图片

30.MyBatis插件原理与Spring集成_第3张图片

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

Executor会拦截CachingExecutor或BaseExecutor。

DefaultSqlSessionFactory.openSessionFromDataSource():

30.MyBatis插件原理与Spring集成_第4张图片

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

所以拦截的是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对象拿。

总结:

30.MyBatis插件原理与Spring集成_第5张图片

配置顺序与执行顺序?

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

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

30.MyBatis插件原理与Spring集成_第6张图片

总结

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对不同数据库有不同实现

30.MyBatis插件原理与Spring集成_第7张图片

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

30.MyBatis插件原理与Spring集成_第8张图片

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

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

image-20200513213835626

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

30.MyBatis插件原理与Spring集成_第9张图片

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

image-20200513214245386

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

关键类

30.MyBatis插件原理与Spring集成_第10张图片

1.5.应用场景分析

image-20200513214416419

30.MyBatis插件原理与Spring集成_第11张图片

2.与Spring 整合分析

2.1.关键配置

pom依赖

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

叫mybatis-spring。版本要对应。


<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatis-springartifactId>
    <version>${mybatis-spring.version}version>
dependency>


<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatisartifactId>
    <version>${mybatis.version}version>
dependency>

SqlSessionFactoryBean

applicationContext.xml配置这个类。

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

属性要指定mybatis-config.xml和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>

MapperScannerConfigurer

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

方法一:


<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>

方法三:

30.MyBatis插件原理与Spring集成_第12张图片

Spring集成mybatis原理:

1)SqlSessionFactory在哪创建?

2)SqlSession在哪创建?

3)代理类在哪创建?

2.2.创建会话工厂SqlSessionFactory

image-20200513221618565

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

30.MyBatis插件原理与Spring集成_第13张图片

InitializingBean

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

30.MyBatis插件原理与Spring集成_第14张图片

调用了buildSqlSesssionFactory方法。

创建Configuration对象。

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

image-20200513222146162

FactoryBean接口

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

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

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

30.MyBatis插件原理与Spring集成_第15张图片

ApplicationListener

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

检查ms是否加载完毕。

30.MyBatis插件原理与Spring集成_第16张图片

SqlSessionFactoryBean用到的Spring扩展点

image-20200513223151002

2.3.创建会话SqlSession

DefaultSqlSession是线程不安全的。

30.MyBatis插件原理与Spring集成_第17张图片

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

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

image-20200513223739900

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

image-20200513224100671

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

image-20200513224225317

怎么拿到一个SqlSessionTemplate?

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

30.MyBatis插件原理与Spring集成_第18张图片

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

最终会调用sqlSessionTemplate的selectOne方法。

30.MyBatis插件原理与Spring集成_第19张图片

2.4.接口的扫描注册

MapperScannerConfigurer用来扫描Mapper接口的。

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。

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

30.MyBatis插件原理与Spring集成_第20张图片

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中

30.MyBatis插件原理与Spring集成_第21张图片

设计模式总结:

30.MyBatis插件原理与Spring集成_第22张图片

参考资料:

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

你可能感兴趣的:(设计模式)