第10篇:Mybatis的插件设计分析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: Mybatis 中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis 是如何处理插件。

参考文档: 官方文档

一、 插件设计介绍

Mybatis 中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis 是如何处理插件。

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  ...
}

在这里插入图片描述

名称 类型 描述
Interceptor 接口 插件都需要实现的接口,封装代理执行方法及参数信息
InterceptorChain 拦截链
InvocationHandler 接口 JDK代理的接口,凡是JDK中的代理都要实现该接口
@Intercepts 注解 用于声明要代理和 @Signature 配合使用
@Signature 注解 用于声明要代理拦截的方法
Plugin 代理的具体生成类

1.1 Interceptor

插件都需要实现的接口,封装代理执行方法及参数信息

public interface Interceptor {
    // 执行方法体的封装,所有的拦截方法逻辑都在这里面写。
  Object intercept(Invocation invocation) throws Throwable;
    // 如果要代理,就用Plugin.wrap(...),如果不代理就原样返回
  Object plugin(Object target);
    // 可以添加配置,主要是xml配置时候可以从xml中读取配置信息到拦截器里面自己解析
  void setProperties(Properties properties);
}

1.2 InterceptorChain

拦截链,为什么需要拦截链,假如我们要对A进行代理, 具体的代理类有B和C。 我们要同时将B和C的逻辑都放到代理类里面,那我们会首先将A和B生成代理类,然后在前面生成代理的基础上将C和前面生成的代理类在生成一个代理对象。这个类就是要做这件事 pluginAll

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  
  // 这里target就是A,而List中的Interceptor就相当于B和C,通过循环方式生成统一代理类
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      //1. 是否需要代理,需要代理生成代理类放回,不需要原样返回。通过for循环的方式将所有对应的插件整合成一个代理对象
      target = interceptor.plugin(target);
    }
    return target;
  }
  ...
}

1.3 InvocationHandler

JDK代理的接口,凡是JDK中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。

public interface InvocationHandler {
      public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

1.4 @Intercepts@Signature

这两个注解是配合使用的,用于指定要代理的类和方法。前面①说了,插件的核心逻辑是拦截执行器的方法,那么这里我们看下如何声明要拦截的类和方法。我们看一下分页插件如何声明拦截。

属性 解释
type 就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler)
method 要拦截的方法
args 要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)
@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class }))
public class MybatisPagerPlugin implements Interceptor {
}

args 要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数),比如 Executor 中就有2个 query 方法。所以要通过args来确定要拦截哪一个。

在这里插入图片描述

1.5 Plugin

代理的具体生成类,解析 @Intercepts@Signature 注解生成代理。

我们看几个重要的方法。

方法名 处理逻辑
getSignatureMap 解析@Intercepts和@Signature,找到要拦截的方法
getAllInterfaces 找到代理类的接口,jdk代理必须要有接口
invoke 是否需要拦截判断
public class Plugin implements InvocationHandler {
  
  //解析@Intercepts和@Signature找到要拦截的方法
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //通过方法名和方法参数查找方法
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
  
  //因为是jdk代理所以必须要有接口,如果没有接口,就不会生成代理
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
  
  @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);
    }
  }
}

二、问题总结

2.1 插件能拦截那些类?

前面已经说过了,这里在总结下。这部分的源码在 Configuration。可以看到很简单只有一行。InterceptorChain#pluginAll

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

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
  
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

2.1.1 ParameterHandler

ParameterHandler的核心方法是setParameters()方法,该方法主要负责调用PreparedStatement的set*()方法为SQL语句绑定实参:
这里能做到的扩展不多。

public interface ParameterHandler {
  // 对方法的入参进行处理,注意只有在 statementType="CALLABLE" 生效
  Object getParameterObject();
  // 预处理参数处理
  void setParameters(PreparedStatement ps) throws SQLException;
}

我们来实现一下,我们插入user信息,通过插件的方式修改入参。

    /**
     * 注意getParameterObject只会在 statementType="CALLABLE"生效
     * insert into T_USER (token_id, uid, name)
     * values (#{tokenId,jdbcType=CHAR}, #{uid,jdbcType=INTEGER}, #{name,jdbcType=CHAR})
     */
    @Intercepts(@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class}))
    public static class ParameterInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object proceed = invocation.proceed();
            PreparedStatement preparedStatement = (PreparedStatement) invocation.getArgs()[0];
            // 插入时候修改第三个参数,也就是name = 孙悟空
            int parameterCount = preparedStatement.getParameterMetaData().getParameterCount();
            if (parameterCount != 0) {
                preparedStatement.setString(3, "孙悟空");
            }
            return proceed;
        }
    }

    @Test
    public void parameterHandler() {
        // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
        // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 获取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.addInterceptor(new ParameterInterceptor());
        // 参数: autoCommit,从名字上看就是是否自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // 获取Mapper
        TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
        TUser tUser = new TUser();
        tUser.setName("唐三藏");
        tUser.setTokenId("testTokenId1");
        mapper.insert(tUser);
        // 这里虽然设置的名字是唐三藏,但是插件中修改为了孙悟空
        System.out.println(mapper.selectAll());
        // 数据插入后,执行查询,然后回滚数据
        sqlSession.rollback();
    }

2.1.2 ResultSetHandler

从名字就可以看出来是对结果集进行处理。这里我们通过插件的方式, 在查询语句中增加一条数据库原本不存在的数据。

    /**
     * 通过对list集合的数据进行修改,增加一条数据库不存在的数据
     */
    @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
    public static class ResultSetHandlerInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object proceed = invocation.proceed();
            if (proceed instanceof List) {
                ArrayList<TUser> newResult = (ArrayList<TUser>) proceed;
                TUser tUser = new TUser();
                tUser.setName("如来佛祖");
                newResult.add(tUser);
                proceed = newResult;
            }
            return proceed;
        }
    }

    @Test
    public void resultSetHandlerTest() {
        // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
        // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 获取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.addInterceptor(new ResultSetHandlerInterceptor());
        // 参数: autoCommit,从名字上看就是是否自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // 获取Mapper
        TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
        System.out.println(mapper.selectAll());
        // 数据插入后,执行查询,然后回滚数据
        sqlSession.rollback();
    }

2.1.3 StatementHandler

    /**
     * 我们本来是一条查询语句,我们打印下sql信息
     */
    @Intercepts(@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}))
    public static class StatementHandlerInterceptor implements Interceptor {

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object proceed = invocation.proceed();
            Object[] args = invocation.getArgs();
            if (args[0] instanceof ClientPreparedStatement) {
                ClientPreparedStatement statement = (ClientPreparedStatement) args[0];
                if (statement.getQuery() instanceof ClientPreparedQuery) {
                    System.out.println(((ClientPreparedQuery) statement.getQuery()).getOriginalSql());
                }
            }
            return proceed;
        }
    }

    @Test
    public void resultSetHandlerTest() {
        // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
        // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 获取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.addInterceptor(new StatementHandlerInterceptor());
        // 参数: autoCommit,从名字上看就是是否自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // 获取Mapper
        TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
        System.out.println(mapper.selectAll());
        // 数据插入后,执行查询,然后回滚数据
        sqlSession.rollback();
    }

2.1.4 Executor

Executor 是个好东西,从他能获取基本你能想到的所有信息。你可以在这里做sql动态变更、也可以做sql语句分析,同时也可以获取某个Mapper的签名信息。总之功能非常强大。一般的插件都是
在这里做文章。如下面例子就是动态的修改了sql。

    /**
     * 动态修改sql信息。
     * 这里因为我们知道要使用查询语句,所以不做sql分析。如果要学习sql分析请看其他文章
     */
    @Intercepts(@Signature(type = Executor.class, method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
    public static class ExecutorInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            if (args[0] instanceof MappedStatement) {
                MappedStatement arg = (MappedStatement) args[0];
                Configuration configuration = arg.getConfiguration();
                StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER");
                Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource");
                sqlSourceField.setAccessible(true);
                sqlSourceField.set(arg, staticSqlSource);
            }
            return invocation.proceed();
        }
    }

    @Test
    public void executor() {
        // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
        InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
        // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
        // 获取Mybatis配置信息
        Configuration configuration = sqlSessionFactory.getConfiguration();
        configuration.addInterceptor(new ExecutorInterceptor());
        // 参数: autoCommit,从名字上看就是是否自动提交事务
        SqlSession sqlSession = sqlSessionFactory.openSession(false);
        // 获取Mapper
        TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
        System.out.println(mapper.selectAll());
        // 数据插入后,执行查询,然后回滚数据
        sqlSession.rollback();
    }

2.2 如何定义一个拦截器?

属性 解释
type 就是要拦截的类(Executor/ParameterHandler/ResultSetHandler/StatementHandler)
method 要拦截的方法
args 要拦截的方法的参数(因为有相同的方法,所以要指定拦截的方法和方法参数)
    @Intercepts(@Signature(type = Executor.class, method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
    public static class ExecutorInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            if (args[0] instanceof MappedStatement) {
                MappedStatement arg = (MappedStatement) args[0];
                Configuration configuration = arg.getConfiguration();
                StaticSqlSource staticSqlSource = new StaticSqlSource(configuration, "select name from T_USER");
                Field sqlSourceField = arg.getClass().getDeclaredField("sqlSource");
                sqlSourceField.setAccessible(true);
                sqlSourceField.set(arg, staticSqlSource);
            }
            return invocation.proceed();
        }
    }

2.3 插件的设计缺陷

InterceptorChain 的设计非常简单,里面就是一个list集合。但是在进行代理的时候,并没有顺序。假设我们要对sql进行代理。

  1. 第一个插件,我们在sql后加上 where id > 1
  2. 第二个插件,我们在sql后机上 limit 10

按照我们设想的最终sql会变成 select * from users where id > 1 limit 10

但是我们知道mybatis是没有顺序的, 那么很可能会出现最终的sql变成 select * from user limit 10 where id > 1,此时就会报错。

所以我们要注意这里。

  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }
  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

三、可以借鉴的知识点

3.1 插件的设计模式

拦截链 + 插件设计

public class Test {
    public static void main(String[] args) {
        InterceptorChain chain = new InterceptorChain();
        PrintInterceptor printInterceptor = new PrintInterceptor();
        Properties properties = new Properties();
        properties.setProperty("name","https://blog.springlearn.cn");
        printInterceptor.setProperties(properties);
        chain.addInterceptor(printInterceptor);
        Animal person = (Animal) chain.pluginAll(new Person());
        String nihao = person.say("nihao");
        System.out.println(nihao);
    }

    public interface Animal{
        String say(String message);
        String say(String name, String message);
    }

    public static class Person implements Animal {
        public String say(String message) {
            return message;
        }

        public String say(String name, String message) {
            return name + " say: " + message;
        }
    }

    @Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
    public static class PrintInterceptor implements Interceptor {
        private String name;

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println(name + ": before print ...");
            Object proceed = invocation.proceed();
            System.out.println(name + ": after print ...");
            return proceed;
        }
    }
}
相关文章
|
3月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
4月前
|
SQL XML Java
8、Mybatis-Plus 分页插件、自定义分页
这篇文章介绍了Mybatis-Plus的分页功能,包括如何配置分页插件、使用Mybatis-Plus提供的Page对象进行分页查询,以及如何在XML中自定义分页SQL。文章通过具体的代码示例和测试结果,展示了分页插件的使用和自定义分页的方法。
8、Mybatis-Plus 分页插件、自定义分页
|
1月前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
209 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
1月前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
1月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
3月前
|
SQL Java 数据库连接
解决mybatis-plus 拦截器不生效--分页插件不生效
本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
979 6
解决mybatis-plus 拦截器不生效--分页插件不生效
|
3月前
|
SQL XML Java
springboot整合mybatis-plus及mybatis-plus分页插件的使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis-Plus及其分页插件,包括依赖引入、配置文件编写、SQL表创建、Mapper层、Service层、Controller层的创建,以及分页插件的使用和数据展示HTML页面的编写。
springboot整合mybatis-plus及mybatis-plus分页插件的使用
|
4月前
|
Java 数据库 Spring
MyBatisPlus分页插件在SpringBoot中的使用
这篇文章介绍了如何在Spring Boot项目中配置和使用MyBatis-Plus的分页插件,包括创建配置类以注册分页拦截器,编写测试类来演示如何进行分页查询,并展示了测试结果和数据库表结构。
MyBatisPlus分页插件在SpringBoot中的使用
|
5月前
|
SQL 监控 Java
IDEA插件-Mybatis Log Free日志替换
MyBatis Log Free 是一个免费的用于在 IntelliJ IDEA 中显示 MyBatis 日志的插件。它可以帮助您更方便地查看和分析 MyBatis 的 SQL 执行情况,以及定位潜在的性能问题,提高开发效率。
425 0
IDEA插件-Mybatis Log Free日志替换
|
5月前
|
Java 数据库连接 mybatis
mybatis包扫描环境分析,最简单的环境准备
mybatis包扫描环境分析,最简单的环境准备
下一篇
DataWorks