暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

自定义Mybatis拦截器与动态SQL的完美结合

一安未来 2023-12-18
39

大家好,我是一安~

前言

在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作,比如每次操作基本后都需要记录操作时间又或者是需要对表数据进行权限控制时,你会如何设计?

一般设计

  • 在每个业务处理的代码中,每次操作后手动更新记录时间
  • 在每个业务处理的代码中,每个脚本上手动加上权限控制

这种设计有很大的缺陷,就是在业务模型变多后每个模型的业务方法中都要进行设置,重复代码太多

优雅设计

MyBatis
的插件主要分为四大类,分别拦截四大核心对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler
。这些插件可以用来实现多种功能,例如性能监控、事务处理、安全控制等。

Executor
拦截器:

  • 介绍说明: Executor
    拦截器主要用于拦截数据库的执行器,它负责执行 MyBatis
    SQL
    语句。
  • 作用: Executor
    拦截器可以拦截执行器的 update
    (写操作)和 query
    (读操作)方法,使你能够在执行 SQL
    语句前后注入自定义逻辑。
  • 使用场景: 适用于需要在数据库写入或读取操作前后执行额外逻辑的情况,比如日志记录、性能监控等。

StatementHandler
拦截器:

  • 介绍说明: StatementHandler
    拦截器主要用于拦截 SQL
    语句的处理,包括 SQL
    语句的创建和参数的设置。
  • 作用: 可以在 SQL
    语句执行之前对其进行修改,也可以拦截参数的设置过程。
  • 使用场景: 适用于需要在 SQL
    语句执行前对其进行动态修改或在参数设置时执行特定逻辑的场景。

ParameterHandler
拦截器:

  • 介绍说明: ParameterHandler
    拦截器主要用于拦截参数的设置过程。
  • 作用: 允许你拦截参数设置的过程,可以在执行 SQL
    语句前修改参数的值。
  • 使用场景: 适用于需要在执行 SQL
    语句前对参数进行额外处理的情况,例如参数加密、验证等。

ResultSetHandler
拦截器:

  • 介绍说明: ResultSetHandler
    拦截器主要用于拦截结果集的处理过程。
  • 作用: 可以在 MyBatis
    处理查询结果集之前或之后执行自定义逻辑。
  • 使用场景: 适用于需要对查询结果集进行额外处理的情况,例如结果集的转换、过滤等。

通过MyBatis
提供的强大机制,使用插件是非常简单的,只需实现Interceptor
接口,并指定想要拦截的方法签名即可。

代码实践

这是自定义拦截器的核心接口。要创建一个自定义拦截器,你需要实现此接口

public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}

  • intercept()
    方法允许你在 MyBatis
    执行的每个方法周围执行逻辑。
  • plugin()
    方法用于为目标对象创建代理。
  • setProperties()
    方法允许你从 XML
    配置中设置自定义属性。

核心代码:

package org.example.mysql.interceptor;

import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.example.mysql.entity.TimeEntity;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
@Slf4j
@Intercepts({
        @Signature( type = Executor.class, method = "update",args = {MappedStatement.class, Object.class}),
        @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})

})
public class IbatisAuditDataInterceptor implements Interceptor {

    private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取当前执行的SQL语句
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        /**
         *  通过MetaObject优雅访问对象的属性,这里是访问MappedStatement的属性;
         *  MetaObject是Mybatis提供的一个用于方便、优雅访问对象属性的对象,通过它可以简化代码
         *  不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
        */
        MetaObject msObject =  MetaObject.forObject(ms, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
        Object parameterObject = args[1];
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        String sql = boundSql.getSql();
        log.info("原SQL:{}", sql.replaceAll("\\n"""));
        // 获取当前执行的SQL语句的操作类型
        SqlCommandType sqlCommandType = ms.getSqlCommandType();

        //解析SQL语句
        Statement statement = CCJSqlParserUtil.parse(sql);

        //请求头获取用户权限PID
        HttpServletRequest request = getHttpServletRequest();
        String pid = request.getHeader("pid");
        if(sqlCommandType == SqlCommandType.SELECT){
            Select selectStatement = (Select) statement;
            PlainSelect plain = (PlainSelect) selectStatement.getSelectBody();
            FromItem fromItem = plain.getFromItem();
            StringBuffer whereSql = new StringBuffer();
            if (fromItem.getAlias() != null) {
                whereSql.append(fromItem.getAlias().getName()).append(".pid = ").append(pid);
            } else {
                whereSql.append("pid = ").append(pid);
            }
            Expression where = plain.getWhere();
            if (where != null) {
                whereSql.append(" and ( " + where + " )");
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);

            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), selectStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
            MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
            args[0] = mappedStatement;
            log.info("现SQL:{}", selectStatement.toString().replaceAll("\\n"""));
        }else if(sqlCommandType == SqlCommandType.INSERT){
            if(parameterObject instanceof TimeEntity){
                BeanUtils.setProperty(parameterObject, "time", dtf.format(LocalDateTime.now()));
                BeanUtils.setProperty(parameterObject, "pid", pid);
            }
        }else if(sqlCommandType == SqlCommandType.UPDATE){
            if(parameterObject instanceof TimeEntity){
                BeanUtils.setProperty(parameterObject, "time", dtf.format(LocalDateTime.now()));
            }
            Update updateStatement = (Update) statement;
            Expression where = updateStatement.getWhere();
            StringBuffer whereSql = new StringBuffer();
            if (where != null) {
                whereSql.append("pid = ").append(pid).append(" and ( " + where + " )");
            }else{
                whereSql.append("pid = ").append(pid);
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            updateStatement.setWhere(expression);
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), updateStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
            MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
            args[0] = mappedStatement;
            log.info("现SQL:{}", updateStatement.toString().replaceAll("\\n"""));
        }else if(sqlCommandType == SqlCommandType.DELETE){
            Delete deleteStatement = (Delete) statement;
            Expression where = deleteStatement.getWhere();
            StringBuffer whereSql = new StringBuffer();
            if (where != null) {
                whereSql.append("pid = ").append(pid).append(" and ( " + where + " )");
            }else{
                whereSql.append("pid = ").append(pid);
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            deleteStatement.setWhere(expression);
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), deleteStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
            MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
            args[0] = mappedStatement;
            log.info("现SQL:{}", deleteStatement.toString().replaceAll("\\n"""));
        }
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }


    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(
                ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if(ms.getKeyProperties() != null && ms.getKeyProperties().length > 0)builder.keyProperty(String.join(",", ms.getKeyProperties()));
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    private static class BoundSqlSqlSource implements SqlSource {
        private final BoundSql boundSql;

        BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        return sra.getRequest();
    }
}

配置拦截器:

@Configuration
public class MybatisPlusConfig {


    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/**/*.xml"));
        //map接收返回值值为null的问题,默认是当值为null,将key返回
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setCallSettersOnNulls(true);
//        configuration.setLogImpl(StdOutImpl.class);

        IbatisAuditDataInterceptor interceptor = new IbatisAuditDataInterceptor();
  configuration.addInterceptor(interceptor);

        sessionFactory.setConfiguration(configuration);
        return sessionFactory.getObject();
    }
}

测试:

2023-11-21 14:17:16.468  INFO 22336 --- [nio-8080-exec-6] o.e.m.i.IbatisAuditDataInterceptor       : 原SQL:select * from user
2023-11-21 14:17:16.470  INFO 22336 --- [nio-8080-exec-6] o.e.m.i.IbatisAuditDataInterceptor       : 现SQL:SELECT * FROM user WHERE pid = 3

2023-11-21 14:19:22.772 INFO 22336 --- [nio-8080-exec-9] o.e.m.i.IbatisAuditDataInterceptor       : 原SQL:update user set age = ? where name = ?
2023-11-21 14:19:22.774 INFO 22336 --- [nio-8080-exec-9] o.e.m.i.IbatisAuditDataInterceptor       : 现SQL:UPDATE user SET age = ? WHERE pid = 3 AND (name = ?)

总结

MyBatis
插件的核心是拦截器,它能够捕获并处理特定的事件,比如SQL
语句的执行、参数的处理等。通过这种方式,插件可以实现各种功能,比如性能监控、事务处理、安全控制等。

MyBatis
插件的使用非常灵活,开发者可以根据实际需求选择不同的插件,并将其应用到MyBatis
的四大核心对象中。这种插件机制不仅提高了MyBatis
的灵活性,也使得开发者可以更加方便地对框架进行扩展和定制。

此外,MyBatis
插件的实现原理是基于动态代理的,也就是说,MyBatis
的四大核心对象实际上都是代理对象。这种机制使得插件可以在不修改原有代码的基础上,对核心对象进行拦截和处理。

总的来说,MyBatis
插件是一种非常强大的扩展机制,它使得开发者可以更加灵活地使用MyBatis
框架,并且可以根据实际需求对框架进行定制和扩展。


如果这篇文章对你有所帮助,或者有所启发的话,帮忙 分享、收藏、点赞、在看,你的支持就是我坚持下去的最大动力!

SpringBoot 插件化开发模式,强烈推荐!


Map+函数式接口方法 优雅的解决 if-else


牢记这16个SpringBoot 扩展接口,写出更加漂亮的代码

文章转载自一安未来,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论