自定义Mybatis自动填充插件----mybatis-plus 自动填充失效、分页插件不向下传递拦截链

本文讲述了如何解决Mybatis-plus自动填充问题,通过采用内置分页插件并自定义插件实现字段在INSERT和UPDATE操作中的自动填充,避免了插件拦截链中断问题。
实战业务优化方案总结—主目录https://blog.youkuaiyun.com/grd_java/article/details/124346685

问题如标题所示,mybatis自动填充不生效,使用分页插件发现这个插件没有向下传递拦截链。

解决方案

  1. 选择使用Mybatis-plus的分页插件,满足向下传递拦截链的要求。
  2. 仿造MyBatis-plus的自动填充使用,自定义自动填充插件。解决Mybatis-plus自动填充时而有效时而失效的情况。

自动填充效果展示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 分页插件

1. 使用Mybatis-plus的分页插件,满足向下传递拦截链的要求
  1. 很简单,MyBatis配置文件中,配置一下
    在这里插入图片描述
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
 * MyBatis相关配置
 */
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

2. 自定义mybatis

2. 这里推荐,自己决定使用哪些插件,包括配置DataSource,事务管理器等等
  1. 很简单,创建一个配置类配置SqlSession就可以了(这个对象保存了MyBatis很多东西,其中拦截器就在这里面)
    在这里插入图片描述
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "cn.xx.mapper",sqlSessionTemplateRef = "sqlOneSqlSessionTemplate")
public class DataSourceConfigOne {

    @Autowired
    private MyBatisConfig myBatisConfig;

    @Bean(name = "sqlOneDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.sqlone")
    @Primary
    public DataSource sqlOne1DataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "sqlOneSqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlOneSql1SessionFactory(@Qualifier("sqlOneDataSource") DataSource dataSource) throws Exception{
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/sqlone/*.xml"));
        bean.setPlugins(myBatisConfig.paginationInterceptor(),myBatisConfig.myPluginAutoFill());//将自定义自动填充拦截器添加到插件中
        return bean.getObject();
    }

    @Bean(name = "sqlOneTransactionManager")
    @Primary
    public DataSourceTransactionManager sqlOne1TransactionManager(@Qualifier("sqlOneDataSource") DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "sqlOneSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlOne1SqlSessionTemplate(@Qualifier("sqlOneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception{
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

3. 实现自动填充

3. 自定义自动填充插件(仿照mybatis-plus的自动填充),需要对MyBatis源码有深入了解。
  1. 注解用来标识字段是否需要自动填充,Fill标识字段在哪些情况下被填充
    在这里插入图片描述
    在这里插入图片描述
import java.lang.annotation.*;

/**
 * 2022-04-14===>>>yinzhipeng
 * 添加此注解的字段,可以被自动填充拦截器扫描到。
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface MyTableField {
    //数据库字段值, 不需要配置该值的情况:
    String value() default "";
    //字段自动填充策略
    MyFieldFill fill() default MyFieldFill.DEFAULT;
}
/**
 * 2022-04-14===>>>yinzhipeng
 * 字段填充策略枚举类,需要配合MyPluginAutoFill类进行指定
 */
public enum MyFieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入时填充字段
     */
    INSERT,
    /**
     * 更新时填充字段
     */
    UPDATE,
    /**
     * 插入和更新时填充字段
     */
    INSERT_UPDATE
}
  1. 在需要自动填充的字段上,添加注解
    在这里插入图片描述
  2. 编写自定义插件(我们选择拦截ParameterHandler的setParameters方法),拦截后,会对INSERT和UPDATE操作的parameterObject 进行设置值,就达到自动填充的效果了。
    在这里插入图片描述
import com.baomidou.mybatisplus.core.MybatisDefaultParameterHandler;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.sql.PreparedStatement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;

/**
 * 2022-04-14===>>>yinzhipeng
 * 根据目前实验,MyBatis plus对于不继承BaseMapper的接口有可能不生效,无法保证百分百自动填充
 * 自定义MyBatis拦截器,实现自动填充,方便日后扩展功能
 */
//@Component //因为在DataSourceConfigOne,自定义了SqlSessionFactory,并指定拦截链,所以这里不需要用注解
@Intercepts({//可以定义多个@Singnature,对多个地方拦截,都用这个拦截器
        @Signature(type = ParameterHandler.class,//指定拦截ParameterHandler接口
                method = "setParameters",//指定要拦截这个接口(指定拦截ParameterHandler接口)的具体方法名,必须写对
                args={PreparedStatement.class}//拦截方法的参数列表,必须和你要拦截的方法一一对应
        )
})
//指定在某个类加载后再加载,防止其它的拦截器,比如分页拦截器不invocation.proceed()向下传递拦截链
//因为越往后的优先级越高,所以要在其它拦截器后面加载,这样它的优先级会更高
//@AutoConfigureAfter(PaginationInterceptor.class) //因为在DataSourceConfigOne,自定义了SqlSessionFactory,并指定拦截链,所以这里不需要
public class MyPluginAutoFill implements Interceptor {
    @Autowired
    private LoginUserUtil loginUserUtil;//用来获取当前登录的用户信息

    //insert操作时,需要填充的字段
    //只填充有@MyTableField(fill = MyFieldFill.INSERT),和@MyTableField(fill = MyFieldFill.INSERT_UPDATE)注解的字段
    //字段的自动填充,需要满足两个条件,在下面指定字段,并且在需要填充字段的地方,加上上面的注解,只满足其中一个条件,不会进行填充
    private void insertBefore(Object o) throws NoSuchFieldException, IllegalAccessException {
        // 属性名称,不是字段名称
        this.doBefore("createTime",MyFieldFill.INSERT,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),o);
        this.doBefore("updateTime",MyFieldFill.INSERT,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),o);
        this.doBefore("createUser",MyFieldFill.INSERT,loginUserUtil.getCurrentAdmin().getUsername(),o);
        this.doBefore("updateUser",MyFieldFill.INSERT,loginUserUtil.getCurrentAdmin().getUsername(),o);
    }
    //update操作时,需要填充的字段
    //只填充有@MyTableField(fill = MyFieldFill.UPDATE),和@MyTableField(fill = MyFieldFill.INSERT_UPDATE)注解的字段
    private void updateBefore(Object o) throws NoSuchFieldException, IllegalAccessException {
        this.doBefore("updateTime",MyFieldFill.UPDATE,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),o);
        this.doBefore("updateUser",MyFieldFill.UPDATE,loginUserUtil.getCurrentAdmin().getUsername(),o);
    }

    //每次执行操作,都会进入此拦截
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //递归获取MybatisDefaultParameterHandler对象,因为可能是多层代理
        MybatisDefaultParameterHandler mybatisDefaultParameterHandler = realTarget(invocation.getTarget());
        //反射mappedStatement对象出来
        Field mappedStatementField = mybatisDefaultParameterHandler.getClass().getDeclaredField("mappedStatement");
        mappedStatementField.setAccessible(true);//此属性是private的,需要强制
        //获取MappedStatement
        MappedStatement mappedStatement =(MappedStatement)mappedStatementField.get(mybatisDefaultParameterHandler);
        // 如果不是insert或update,就不需要自动填充,直接传递拦截链
        if(mappedStatement.getSqlCommandType()!=SqlCommandType.INSERT && mappedStatement.getSqlCommandType()!=SqlCommandType.UPDATE)
            return invocation.proceed();//原方法执行,并返回

        // 获取parameterObject对象,用于设置参数
        Object parameterObject = realTarget(mybatisDefaultParameterHandler.getParameterObject());
        // 如果是insert或update,就执行对应的自动填充
        if(mappedStatement.getSqlCommandType()==SqlCommandType.INSERT){
            insertBefore(parameterObject);
        }else if (mappedStatement.getSqlCommandType()==SqlCommandType.UPDATE){
            updateBefore(parameterObject);
        }
        return invocation.proceed();//原方法执行,并返回,传递拦截链,不传递的话,后面的拦截器将无法获取
    }
    /**
     * 获得真正的处理对象,可能多层代理.
     */
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
                MetaObject metaObject = SystemMetaObject.forObject(target);
                return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }

    /**
     * 真正做填充的方法。填充时,只填充有特定注解的字段
     * @param fieldName 要填充的字段名
     * @param operationType 需要的注解
     * @param fieldVal 要填充的值
     * @param metaObject 要填充的对象
     */
    private void doBefore(String fieldName,MyFieldFill operationType,Object fieldVal,Object metaObject) throws NoSuchFieldException, IllegalAccessException {
        //获取对象的指定Field属性
        Field declaredField = metaObject.getClass().getDeclaredField(fieldName);
        //如果填充的值存在,属性存在,并且有指定注解
        if (Objects.nonNull(fieldVal) && declaredField!=null && declaredField.isAnnotationPresent(MyTableField.class)) {
            //获取注解
            MyTableField annotation = declaredField.getAnnotation(MyTableField.class);
            //如果注解是规定的注解就进行填充
            if (annotation.fill() == MyFieldFill.INSERT_UPDATE||
                    annotation.fill()==operationType){
                declaredField.setAccessible(true);
                declaredField.set(metaObject,fieldVal);
            }
        }
    }

//        /**
//         * 将当前拦截器,生成代理对象放在拦截链中
//         * @param target 目标对象,被代理对象,被拦截对象
//         * @return 代理对象
//         */
//        @Override
//        public Object plugin(Object target) {
//                System.out.println("为"+target+"生成代理");
//                return Plugin.wrap(target,this);
//        }
//        //获取配置文件属性,xml中配置的属性,从Properties获取,插件初始化时调用。只调用一次,插件配置的属性从这里设置
//        @Override
//        public void setProperties(Properties properties) {
//                System.out.println("插件配置的初始化参数:"+properties);
//        }
}
<think>嗯,用户的问题是关于MyBatis-Plus分页查询时,Map层返回的是List。我需要先理解用户的具体困惑在哪里。可能他们在使用MyBatis-Plus进行分页查询时,发现Mapper层返回的结果是List类型,而是预期的Page对象,导致分页信息如总条数丢失了。这时候需要分析可能的原因,并提供解决方案。 首先,我应该回顾MyBatis-Plus分页机制。通常,配置了分页插件后,使用selectPage方法会返回Page对象,其中包含数据列表和分页信息。但如果用户手动调用selectList或者其他方法,可能会直接得到List,这时候分页信息就会被处理。需要检查用户的Mapper层方法是否正确使用了分页参数。 接下来,用户提到的“Map层查出来的是list”可能指的是Mapper层返回的是List类型,而是Page。这可能是因为他们在Mapper接口中定义的方法返回类型是List,而是IPage或者Page。例如,如果Mapper方法写成List<User> selectPage(Page<User> page, @Param("ew") Wrapper<User> wrapper),这样返回的虽然是List,但实际分页参数可能没有被正确处理,导致分页信息丢失。 此外,用户可能在XML文件中自定义了SQL查询,但未正确接收分页参数。比如,在XML中如果没有包含limit和offset的参数,或者没有让MyBatis-Plus自动处理分页逻辑,也可能导致返回List而非分页对象。 还有一种可能是用户没有正确配置分页插件,导致分页功能没有生效。这时候即使调用selectPage方法,也会返回List,因为没有分页插件的拦截器来处理分页逻辑。 需要向用户解释这些可能的原因,并逐一给出解决方案。比如,检查Mapper接口的返回类型是否为IPage,确保分页插件配置正确,或者在自定义SQL时正确使用分页参数。同时,提醒用户在需要分页信息时,必须使用Page对象作为参数,并确保查询方法返回的是Page类型。 另外,用户可能对分页结果的封装有疑问,比如如何获取总条数和分页数据。需要说明Page对象的结构,包含records(数据列表)、total、size、current等属性,以及如何将这些信息正确返回给前端。 最后,可能需要给出示例代码,展示正确的Mapper接口定义和XML配置,帮助用户理解如何正确实现分页查询,避免返回List而丢失分页信息。同时,强调在自定义SQL时,必须让MyBatis-Plus处理分页逻辑,或者在XML中手动添加limit和offset参数,并处理总条数的查询。</think>--- ### 一、问题现象描述 当使用MyBatis-Plus进行分页查询时,若Mapper层直接返回`List<T>`而非`Page<T>`,会导致**分页核心数据丢失**(如总记录数`total`、总页数`pages`等),仅保留当前页数据列表。 --- ### 二、核心原因分析 #### 1. 方法定义错误 ```java // ❌ 错误示例:返回List会导致分页失效 List<User> selectPageList(Page<User> page, @Param("ew") Wrapper<User> wrapper); // ✅ 正确写法:必须返回IPage/Page类型 IPage<User> selectPageList(IPage<User> page, @Param("ew") Wrapper<User> wrapper); ``` #### 2. XML映射匹配 ```xml <!-- ❌ 错误示例:未接收分页参数 --> <select id="selectPageList" resultType="User"> SELECT * FROM user WHERE age > #{age} </select> <!-- ✅ 正确写法:参数需包含分页对象 --> <select id="selectPageList" resultType="User"> SELECT * FROM user WHERE age > #{param2.age} <!-- param2对应Wrapper --> </select> ``` --- ### 三、解决方案实践 #### 方案1:规范Mapper接口定义 ```java // Mapper接口 public interface UserMapper extends BaseMapper<User> { // 关键点:返回类型必须为IPage IPage<User> selectCustomPage( IPage<User> page, // 必须作为第一个参数 @Param("age") Integer age ); } // Service调用 public IPage<User> getPage(int pageNum, int pageSize, Integer age) { IPage<User> page = new Page<>(pageNum, pageSize); return userMapper.selectCustomPage(page, age); } ``` #### 方案2:XML映射强化 ```xml <!-- 正确接收分页参数 --> <select id="selectCustomPage" resultType="User"> SELECT * FROM user WHERE age > #{age} <!-- MyBatis-Plus自动注入分页参数 --> LIMIT #{page.current}, #{page.size} </select> ``` --- ### 四、分页数据保留原理 #### 分页对象生命周期 | 阶段 | 数据内容 | 说明 | |------------------|-------------------------|-----------------------------| | 方法调用前 | `page`为空对象 | 仅包含current/size参数 | | SQL执行后 | `page.records`填充数据 | 自动注入查询结果列表 | | 分页拦截器处理 | `page.total`获得总条数 | 通过COUNT查询自动计算 | --- ### 五、数据转换技巧 #### 从IPage提取纯列表 ```java // 获取分页对象中的纯数据列表 IPage<User> pageResult = userService.getPage(1, 10, 18); List<User> dataList = pageResult.getRecords(); // 关键转换点 // 响应示例 { "list": [ ... ], // dataList内容 "total": 156, // pageResult.getTotal() "pageSize": 10 // pageResult.getSize() } ``` --- ### 六、特殊场景处理 #### 强制返回List但保留分页信息 ```java // 1. 定义特殊返回对象 @Data public class PageList<T> { private List<T> list; private Long total; private Integer pages; } // 2. 手动封装结果 public PageList<User> getPageList(int page, int size) { IPage<User> iPage = userService.getPage(page, size, null); return new PageList<>(iPage.getRecords(), iPage.getTotal(), iPage.getPages()); } ``` --- ### 七、调试验证方法 #### 1. 检查执行的SQL ```log DEBUG 15876 --- [nio-8080-exec-1] c.b.m.e.p.i.PaginationInnerInterceptor : 已检测到分页操作 Original SQL: SELECT * FROM user WHERE age > ? Generated Count SQL: SELECT COUNT(*) FROM user WHERE age > ? Generated Limit SQL: SELECT * FROM user WHERE age > ? LIMIT ?,? ``` #### 2. 验证返回对象类型 ```java // 单元测试断言 @Test void testPageQuery() { IPage<User> result = userMapper.selectCustomPage(new Page<>(1, 10), 18); assertTrue(result instanceof Page); // 必须通过 assertNotNull(result.getTotal()); // 总条数必须存在 } ``` --- ### 八、性能优化建议 1. **禁用非必要COUNT查询**: ```java Page<User> page = new Page<>(1, 10); page.setSearchCount(false); // 关闭自动COUNT ``` 2. **分页参数合理化**: ```java // application.yml配置 mybatis-plus: global-config: db-config: max-limit: 1000 # 单页最大记录数限制 optimize-count-sql: true # 智能COUNT优化 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值