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

发布时间:2023年12月18日

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

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

Executor 拦截器:

介绍说明: Executor 拦截器主要用于拦截数据库的执行器,它负责执行 MyBatisSQL 语句。
作用: 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 配置中设置自定义属性。

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();
    }
}

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

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

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

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

文章来源:https://blog.csdn.net/weixin_45817985/article/details/135063391
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。