关于mybatis拦截器

【背景】

希望select语句执行时,都能自动拼接del_flag和dept_id查询条件。
但现表结构混乱,并不是所有表都存在这两个字段,且并不是都叫这两个名字。

【执行逻辑】

1.项目启动时,把当前项目所连表中,需要字段读出来,

2.select语句过来时,查当前表是否存在要拼接字段,存在则做拼接

【代码实现】

第一步:

import com.yushu.base.common.utils.RedisUtils;
import com.yushu.base.framework.constant.ColumnConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Slf4j
@SpringBootApplication
public class DatabaseMetadataLoaderApplication implements CommandLineRunner {

    @Autowired
    private DataSource dataSource;

    private final static List<String> COLUMN_LIST = ColumnConstant.LIST;

    @Override
    public void run(String... args) throws Exception {
        long start = System.currentTimeMillis();
        // 加载所有表和字段信息
        loadDatabaseMetadata();
        long end = System.currentTimeMillis();
        System.out.println("[DatabaseMetadataLoaderApplication]处理时长:" + (end - start));
    }

    /**
     * 获取所有表和字段信息
     *
     * @throws SQLException
     */
    public void loadDatabaseMetadata() throws SQLException {

        // 获取数据库连接
        try (Connection connection = dataSource.getConnection()) {
            DatabaseMetaData metaData = connection.getMetaData();

            // 获取所有表
            try (ResultSet tables = metaData.getTables(connection.getCatalog(), null, "%", new String[]{"TABLE"})) {
                while (tables.next()) {
                    String tableName = tables.getString("TABLE_NAME");
                    StringBuilder value = new StringBuilder();
                    // 获取当前表的所有字段
                    try (ResultSet columnsResultSet = metaData.getColumns(connection.getCatalog(), null, tableName, "%")) {
                        while (columnsResultSet.next()) {
                            String columnName = columnsResultSet.getString("COLUMN_NAME");
                            if (COLUMN_LIST.contains(columnName)) {
                                value.append(columnName).append(",");
                            }
                        }
                    }
                    log.info("[DatabaseMetadataLoaderApplication]TABLE_NAME:{},COLUMN_NAME:{}", tableName, value);
                    RedisUtils.set(tableName, value.toString().endsWith(",") ? value.substring(0, value.length() - 1) : value);
                }
            }
        }
    }
}

第二步:


import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yushu.base.common.utils.CollectionUtil;
import com.yushu.base.common.utils.RedisUtils;
import com.yushu.base.common.utils.SecurityUtils;
import com.yushu.base.framework.constant.ColumnConstant;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
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.select.SubSelect;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.util.List;
import java.util.Properties;

@Slf4j
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisSqlLogInterceptor implements Interceptor {


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");
        MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
        SqlCommandType commandType = mappedStatement.getSqlCommandType();

        if (SqlCommandType.SELECT.equals(commandType)) {
            BoundSql boundSql = statementHandler.getBoundSql();
            Statement statement = CCJSqlParserUtil.parse(boundSql.getSql());
            PlainSelect selectBody = (PlainSelect) ((Select) statement).getSelectBody();
            FromItem fromItem =  selectBody.getFromItem();

            if (fromItem instanceof Table) {
                Table table = (Table) fromItem;
                String tableName = table.getName();
                String tableAlias = table.getAlias() == null ? null : table.getAlias().getName();
                System.out.println("=====" + tableName + " " + tableAlias);
                buildWhereClause(selectBody, getDataFilter(tableName, tableAlias));
            } else if (fromItem instanceof SubSelect) {
                // 子查询的先不管了,历史遗留查询逻辑保持原样,新逻辑不开发子查询
                // 正常如果是子查询,应递归处理
//                SubSelect subSelect = (SubSelect) fromItem;
//                handleSelectBody(subSelect.getSelectBody());
            }
            ReflectUtil.setFieldValue(boundSql, "sql", statement.toString());
        }

        return invocation.proceed();
    }

    private String getDataFilter(String tableName, String tableAlias) {
        // 项目启动时,读取所有表结构,记录表名与字段的关联关系,放入缓存
        // 以下校验表中是否存在需拼接字段,若存在则进行sql拼接
        String columnStr = RedisUtils.getStr(tableName);
        if (StringUtils.isEmpty(columnStr)) {
            return "";
        }

        StringBuilder stringBuilder = new StringBuilder();
        List<String> columnList = CollectionUtil.toList(columnStr.split(","));
        for (String column : columnList) {
            if (stringBuilder.length() > 0) {
                stringBuilder.append("and ");
            }
            switch (column) {
                // 3.组装sql
                case ColumnConstant.DEL_FLAG:
                    stringBuilder.append(" ").append(getTableAlias(tableAlias)).
                            append(ColumnConstant.DEL_FLAG).append(" = ").append("0").append(" ");
                    break;
                case ColumnConstant.SCHOOL_ID:
                    stringBuilder.append(" ").append(getTableAlias(tableAlias)).
                            append(ColumnConstant.SCHOOL_ID).append(" = ").append(SecurityUtils.getDeptId()).append(" ");
                    break;
                default:
            }
        }
        return stringBuilder.toString();
    }

    private String getTableAlias(String tableAlias) {
        if (StringUtils.isEmpty(tableAlias)) {
            return "";
        }
        return tableAlias + ".";
    }

    /**
     * 添加查询条件
     *
     * @param select
     * @param dataFilter
     * @throws JSQLParserException
     */
    private void buildWhereClause(PlainSelect select, String dataFilter) throws JSQLParserException {
        if (StringUtils.isBlank(dataFilter)) {
            return;
        }
        if (select.getWhere() == null) {
            select.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter));
        } else {
            AndExpression and = new AndExpression(
                    CCJSqlParserUtil.parseCondExpression(dataFilter), select.getWhere());
            select.setWhere(and);
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
====================   遇到的问题   ====================

【问题描述】

程序执行报错   module java.base does not "opens java.lang.reflect" to unnamed module

【原因分析】

Java9及以上版本中引入的Java Platform Module System引起的,特别是强封装的实现。它仅在特定条件下允许access,对于反射导致异常的代码尝试使用相同的限制。

【解决方案】

增加如下虚拟运行参数

--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED

17:18:24.648 [restartedMain] WARN  o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - [refresh,591] - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwTeacherInfoController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sysUserServiceImpl': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sysConfigServiceImpl': Invocation of init method failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.lang.reflect.InvocationHandler java.lang.reflect.Proxy.h accessible: module java.base does not "opens java.lang.reflect" to unnamed module @79defdc
### The error may exist in file [F:\Java\yushu-system\target\classes\mapper\system\SysConfigMapper.xml]
### The error may involve com.yushu.base.system.mapper.SysConfigMapper.selectConfigList
### The error occurred while executing a query
### Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.lang.reflect.InvocationHandler java.lang.reflect.Proxy.h accessible: module java.base does not "opens java.lang.reflect" to unnamed module @79defdc
17:18:24.650 [restartedMain] INFO  c.a.d.p.DruidDataSource - [close,2105] - {dataSource-1} closing ...
17:18:24.655 [restartedMain] INFO  c.a.d.p.DruidDataSource - [close,2178] - {dataSource-1} closed
17:18:24.670 [restartedMain] INFO  o.a.c.c.StandardService - [log,173] - Stopping service [Tomcat]
17:18:24.704 [restartedMain] ERROR o.s.b.SpringApplication - [reportFailure,870] - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwTeacherInfoController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sysUserServiceImpl': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sysConfigServiceImpl': Invocation of init method failed; nested exception is org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database.  Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.lang.reflect.InvocationHandler java.lang.reflect.Proxy.h accessible: module java.base does not "opens java.lang.reflect" to unnamed module @79defdc
### The error may exist in file [F:\Java\yushu-system\target\classes\mapper\system\SysConfigMapper.xml]
### The error may involve com.yushu.base.system.mapper.SysConfigMapper.selectConfigList
### The error occurred while executing a query
### Cause: java.lang.reflect.InaccessibleObjectException: Unable to make field protected java.lang.reflect.InvocationHandler java.lang.reflect.Proxy.h accessible: module java.base does not "opens java.lang.reflect" to unnamed module @79defdc
    at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessProperties(CommonAnnotationBeanPostProcessor.java:332)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:780)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:453)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:343)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1370)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1359)
    at com.yushu.base.YushuApplication.main(YushuApplication.java:22)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)

【问题描述】

部分sql不需要自动拼接上述查询条件。比如登录时查询用户信息,此时还没有dept_id;或者定时任务执行时也无需拼接dept_id字段

【解决方案】

以登录为例:

设置登录标志,为用户登录的线程上下文添加一个标志,在 MyBatis 拦截器中判断当前是否为登录场景。

步骤:

  1. 在登录请求处理时设置标志。

  2. 在拦截器中检查标志,决定是否跳过部门 ID 的拼接。

代码示例:

登录请求时:

public ResponseEntity<?> login(LoginRequest loginRequest) {
    try {
        LoginContext.setIsLoginRequest(true); // 设置为登录请求
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(), 
                loginRequest.getPassword()
            )
        );

        // 登录成功后清除标志
        LoginContext.clear();
        return ResponseEntity.ok(new JwtResponse(...));
    } catch (Exception e) {
        LoginContext.clear();
        throw e;
    }
}

线程上下文工具类:

public class LoginContext {
    private static final ThreadLocal<Boolean> IS_LOGIN_REQUEST = new ThreadLocal<>();

    public static void setIsLoginRequest(boolean isLogin) {
        IS_LOGIN_REQUEST.set(isLogin);
    }

    public static boolean isLoginRequest() {
        return IS_LOGIN_REQUEST.get() != null && IS_LOGIN_REQUEST.get();
    }

    public static void clear() {
        IS_LOGIN_REQUEST.remove();
    }
}

拦截器中判断:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    if (LoginContext.isLoginRequest()) {
        return invocation.proceed(); // 登录请求,直接放行
    }

    // 正常 SQL 处理逻辑
    handleDepartmentId(...);
    return invocation.proceed();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值