万隆的笔记 万隆的笔记
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
博文索引
笔试面试
  • 在线学站

    • 菜鸟教程 (opens new window)
    • 入门教程 (opens new window)
    • Coursera (opens new window)
  • 在线文档

    • w3school (opens new window)
    • Bootstrap (opens new window)
    • Vue (opens new window)
    • 阿里开发者藏经阁 (opens new window)
  • 在线工具

    • tool 工具集 (opens new window)
    • bejson 工具集 (opens new window)
    • 文档转换 (opens new window)
  • 更多在线资源
  • Changlog
  • Aboutme
GitHub (opens new window)
  • MyBatis

    • MyBatis 简介
    • MyBatis简单应用
    • MyBatis常⽤配置
    • MyBatis 单表 CRUD 操作
    • MyBatis动态 SQL
    • MyBatis复杂映射
    • MyBatis注解开发
    • MyBatis缓存
    • MyBatis插件
      • Mybatis插件介绍
      • 允许拦截的方法
      • 插件原理
      • 自定义插件实现解析
      • 源码解析
      • PageHelper分⻚插件
      • 通用Mapper
    • ⾃定义持久层框架
    • MyBatis架构原理
    • MyBatis源码剖析
    • MyBatis设计模式
  • Spring-MyBatis

  • MyBatis-Plus

  • MyBatis
  • MyBatis
2022-11-14
目录

MyBatis插件

# MyBatis插件

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易⻅的,一是增加了框架的灵活性;二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。以MyBatis为例,我们可基于MyBatis插件机制实现分⻚、分表、监控等功能。由于插件和业务无关,业务也无法感知插件的存在,因此可以无感植入插件,在无形中增强功能。

# Mybatis插件介绍

MyBatis作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插件扩展机制。MyBatis对持久层的操作就是借助于这四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对MyBatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象。

# 允许拦截的方法

  • 执行器-Executor (update、query、commit、rollback等方法);
  • SQL语法构建器-StatementHandler (prepare、parameterize、batch、updates query等方法);
  • 参数处理器-ParameterHandler (getParameterObject、setParameters方法);
  • 结果集处理器-ResultSetHandler (handleResultSets、handleOutputParameters等方法);

# 插件原理

在四大对象创建的时候:

  1. 每个创建出来的对象不是直接返回的,而是InterceptorChain.pluginAll方法返回。例如:

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
      // ...
      executor = (Executor) interceptorChain.pluginAll(executor);
      return executor;
    }
    
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
      ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
      parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
      return parameterHandler;
    }
    // ......
    
    
  2. 获取到所有的Interceptor (拦截器,插件需要实现的接口),调用 interceptor.plugin(target),返 回 target 包装后的对象。

插件机制:我们可以使用插件为目标对象创建一个代理对象。在MyBatis中,我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行。

# 如何拦截的?

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说:

// Configuration的newParameterHandler方法
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

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

interceptorChain保存了所有的拦截器(interceptors),是MyBatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为MyBatis中的四大对象,返回的target是被重重代理后的对象。

如果我们想要拦截Executor的query方法,那么可以这样定义插件:

@Intercepts({
        @Signature(type = Executor.class,
                method = "query",
                args= { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class ExamplePlugin implements Interceptor {
   // ......
}

除此之外,我们还需将插件配置到sqlMapConfig.xml中:

<plugins>
  <plugin interceptor="org.example.hello.mybatis.plugin.ExamplePlugin"/>
</plugins>

这样MyBatis在启动时可以加载插件,并保存插件实例到拦截器链(InterceptorChain)中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory创建SqlSession,Executor实例会在创建SqlSession的过程中被创建,Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在Executor相关方法被调用前执行,以上就是MyBatis插件机制的基本原理。

# 自定义插件实现解析

实现自定义插件,需要实现 MyBatis 插件接口:Interceptor, 这个接口主要有三个抽象方法:

  • intercept方法,插件的核心方法
  • plugin方法,生成target的代理对象
  • setProperties方法,传递插件所需参数

现在详细看看上述例子中的代码实现:

@Intercepts({
        // 这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
        @Signature(
                // 指拦截哪个接口
                type = Executor.class,
                // 拦截接口的某个方法名
                method = "query",
                // 拦截的方法的入参,注意顺序要正确。 拦截器根据方法名+参数 确认唯一性。
                args= { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        )
})
public class ExamplePlugin implements Interceptor {
    /**
     * 每次执行操作的时候,都会进行这个拦截器的方法内
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("对方法进行了增强....");
        return invocation.proceed(); //执行原方法
    }

    /**
     * 包装目标对象 为目标对象创建代理对象。
     * 目的是把这个拦截器生成一个代理放到拦截器链中
     * @param target 要拦截的对象
     * @return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("将要包装的目标对象:" + target);
        return Plugin.wrap(target, this);
    }

    /**
     * 获取配置文件的属性。
     * 插件初始化的时候调用,仅调用一次。
     * @param properties 插件配置的属性
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:" + properties );
    }
}

# 源码解析

在上面的实现解析中,我们知道plugin方法会包装目标对象,把我们的拦截器的代理放到拦截器中,所以我们可以从Plugin入手分析。

查看源码我们可以发现Plugin实现了InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // 获取被拦截方法列表,比如:signatureMap.get(Executor.class), 可能返回 [query, update, commit, ...]
    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);
  }
}

invoke方法会检测被拦截方法是否配置在插件的 @Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept方法中,该方法的参数类型为Invocationo invocation主要用于存储目标类,方法以及方法参数列表:

public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;
  
  // ...... (省略构造方法)

  // 调用被拦截的方法
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

至此,插件的执行逻辑到此结束。

# PageHelper分⻚插件

MyBatis可以使用第三方的插件来对功能进行扩展,分⻚助手PageHelper是将分⻚的复杂操作进行封装,使用简单的方式即可获得分⻚的相关数据。

# 引入PageHelper

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>3.7.5</version>
</dependency>
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>0.9.1</version>
</dependency>

# 配置PageHelper插件

在MyBatis核心配置文件中,配置PageHelper插件

<!--注意:分⻚助手的插件 配置在通用mapper之前-->
<plugin interceptor="com.github.pagehelper.PageHelper">
  <!--指定方言-->
  <property name="dialect" value="mysql"/>
</plugin>

# 测试分⻚&数据获取

@Test
public void testPageHelper() {
  // 设置分⻚参数
  PageHelper.startPage(1, 2);
  List<User> select = userMapper.findAll();

  for (User user : select) {
    System.out.println(user);
  }
  // 其他分⻚的数据
  PageInfo<User> pageInfo = new PageInfo<User>(select);
  System.out.println("总条数:" + pageInfo.getTotal());
  System.out.println("总⻚数:" + pageInfo. getPages ());
  System.out.println("当前⻚:" + pageInfo. getPageNum());
  System.out.println("每⻚显万⻓度:" + pageInfo.getPageSize());
  System.out.println("是否第一⻚:"+ pageInfo.isIsFirstPage());
  System.out.println("是否最后一⻚:" + pageInfo.isIsLastPage());
}

# 通用Mapper

通用Mapper就是为了解决单表增删改查,基于Mybatis的插件机制,开发人员不需要编写SQL,不需要在DAO中增加方法,只需写好实体类,就能支持相应的增删改查方法,这里介绍tk.mybatis插件。

# 引入tk.mybatis

<dependency>
  <groupId>tk.mybatis</groupId>
  <artifactId>mapper</artifactId>
  <version>3.1.2</version>
</dependency>

# 配置tk.mybatis插件

<plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
  <!--通用Mapper接口,多个通用接口用逗号隔开-->
  <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
</plugin>

# 配置实体类

@Table(name = "user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;
    
    // ... 逻辑字段使用@Transient注解
}

# 定义Mapper

只需要继承tk提供的通用mapper即可。

public interface UserMapper extends Mapper<User> {

}

# 测试通用方法

@Test
public void testTk() {
  User user = new User();
  user.setId(4);
  // 1. mapper基础接口
  // 1.1 select 接口, 查询条件都是等号
  User user1 = userMapper.selectOne(user); // 根据实体中的属性进行查询,只能有—个返回值
  List<User> users = userMapper.select(null); // 查询全部结果
  userMapper.selectByPrimaryKey(1); // 根据主键查询
  userMapper.selectCount(user); // 根据实体中的属性查询总数
  // 1.2 insert 接口
  int insert = userMapper.insert(user); // 保存实体,null值也会保存,不会使用数据库默认值
  int insert2 = userMapper.insertSelective(user); // 保存实体,null的属性不会保存,会使用数据库默认值
  // 1.3 update 接口
  int update1 = userMapper.updateByPrimaryKey(user); // 根据主键更新实体全部字段, null值会被更新
  // 1.3 delete 接口
  int delete = userMapper.delete(user); // 根据实体属性作为条件进行删除,查询条件使用等号
  userMapper.deleteByPrimaryKey(4); // 根据主键字段进行删除

  // 2. example方法
  // 2.1 自定义查询
  Example example = new Example(User.class);
  example.createCriteria().andEqualTo("id", 1);
  example.createCriteria().andLike("username", "jack");
  List<User> users1 = userMapper.selectByExample(example);
}
上次更新: 5/30/2023, 10:53:02 PM
⾃定义持久层框架

⾃定义持久层框架→

最近更新
01
2025
01-15
02
Elasticsearch面试题
07-17
03
Elasticsearch进阶
07-16
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式