MyBatis学习之实现Interceptor接口支持功能扩展

本文介绍了如何在MyBatis中使用Interceptor实现不侵入业务代码的SQL拦截,以加密敏感数据并针对不同SQL类型进行重写。通过自定义拦截器和JDK动态代理,实现在执行增删改查操作时动态添加加密字段并替换原始SQL语句。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

  • 场景

    mybatis中,如何实现在不侵入原有业务代码的情况下拦截sql,执行特定的某些逻辑。如:分页工具PageHelper、敏感信息加密或多数据库适配等。

  • 基本介绍

    在mybatis中也为开发者预留了拦截器接口Interceptor,通过实现自定义拦截器这一功能,可以实现我们自己的插件,允许用户在不改动mybatis的原有逻辑的条件下,实现自己的逻辑扩展。

使用说明

  • 拦截接口

    Executor、StatementHandler、ParameterHandler和ResultSetHandler接口所有方法。ParameterHandler主要解析参数,为Statement设置参数,ResultSetHandler主要是负责把ResultSet转换成Java对象。

    mybatis四大核心组件作用

  • 内容说明

    Interceptor接口、Invocation调用类、@Intercepts和@Signature注解

Interceptor接口

  • 源码分析

    
        public interface Interceptor {
            // 该方法就是MyBatis在使用前面提到的四个接口中的方法时进行回调的方法。
            Object intercept(Invocation invocation) throws Throwable;
    
            // 用来为目标对象创建代理对象的方法,通常不需要覆写。
            default Object plugin(Object target) {
                return Plugin.wrap(target, this);
            }
    
            // 设置一些属性
            default void setProperties(Properties properties) {
            }
        }
    
    
    
    

Invocation类

  • 源码分析

    
        public class Invocation {
    
            private final Object target; // 目标对象
            private final Method method; // 执行方法
            private final Object[] args; // 方法参数
    
            public Invocation(Object target, Method method, Object[] args) {
                this.target = target;
                this.method = method;
                this.args = args;
            }
    
            public Object getTarget() {
                return target;
            }
    
            public Method getMethod() {
                return method;
            }
    
            public Object[] getArgs() {
                return args;
            }
    
            // 执行目标方法
            public Object proceed() throws InvocationTargetException, IllegalAccessException {
                return method.invoke(target, args);
            }
    
        }
    
    
    
    

@Intercepts和@Signature注解

  • @Intercepts

    @Intercepts注解只有一个属性,即value,其返回值类型是一个@Signature类型的数组,表示我们可以配置多个@Signature注解。

  • @Signature

    @Signature注解其实就是一个方法签名,其共有三个属性,分别为:

    type指接口的class,
    method指接口中的方法名,
    args指的是方法参数类型(该属性返回值是一个数组)。

    举例说明

    比如:我们需要代理Executor的query方法,而查询方法有2个,一个有(CacheKey cacheKey, BoundSql boundSql)参数,另外一个没有,如果我们要代理的是没有这2个参数的方法,那么需要在配置中的args参数里就不写这2个参数

  • 源码分析

    
        public @interface Intercepts {
            /**
            * 方法签名
            */
            Signature[] value();
        }
    
        public @interface Signature {
            /**
            * 用来指定接口类型
            */
            Class<?> type();
    
            /**
            * 用来指定方法名
            */
            String method();
    
            /**
            * 用来指定方法参数
            */
            Class<?>[] args();
        }
    
    
    

代码示例

需求描述

  • 敏感信息密文加密

    对于新增和修改,如果有username和phone字段,则需要动态添加字段username_enc和phone_enc为对应字段的密文,并保存到数据库。

实现流程

  • 引入依赖

    
        <?xml version="1.0"?>
        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
            http://maven.apache.org/xsd/maven-4.0.0.xsd">
            <modelVersion>4.0.0</modelVersion>
            <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <!--<version>2.2.5.RELEASE</version>-->
                <version>1.5.5.RELEASE</version>
                <relativePath/>
            </parent>
            <groupId>com.example</groupId>
            <artifactId>test-shardingsphere</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <packaging>jar</packaging>
            <description>Demo project for Spring Boot</description>
            <dependencies>
                <!--swagger ui-->
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger2</artifactId>
                    <version>2.9.2</version>
                </dependency>
                <dependency>
                    <groupId>io.springfox</groupId>
                    <artifactId>springfox-swagger-ui</artifactId>
                    <version>2.9.2</version>
                </dependency>
                <!--springboot-->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-configuration-processor</artifactId>
                </dependency>
                <!--mybatis starter-->
                <dependency>
                    <groupId>org.mybatis.spring.boot</groupId>
                    <artifactId>mybatis-spring-boot-starter</artifactId>
                    <version>1.3.2</version>
                </dependency>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.41</version>
                </dependency>
                <!--lombok-->
                <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>1.16.10</version>
                </dependency>
                <dependency>
                    <groupId>org.aspectj</groupId>
                    <artifactId>aspectjrt</artifactId>
                    <version>1.8.13</version>
                </dependency>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-aop</artifactId>
                </dependency>
                <!--sql解析-->
                <dependency>
                    <groupId>com.github.jsqlparser</groupId>
                    <artifactId>jsqlparser</artifactId>
                    <version>3.1</version>
                </dependency>
            </dependencies>
    
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>repackage</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-maven-plugin</artifactId>
                        <version>1.3.2</version>
                        <configuration> <!--配置文件的位置-->
                            <configurationFile>src/main/resources/generatorConfig.xml<configurationFile>
                            <verbose>true</verbose>
                            <overwrite>true</overwrite>
                        </configuration>
                        <executions>
                            <execution>
                                <id>Generate MyBatis Artifacts</id>
                                <goals>
                                    <goal>generate</goal>
                                </goals>
                            </execution>
                        </executions>
                        <dependencies>
                            <dependency>
                                <groupId>org.mybatis.generator</groupId>
                                <artifactId>mybatis-generator-core</artifactId>
                                <version>1.3.2</version>
                            </dependency>
                        </dependencies>
                    </plugin>
                </plugins>
            </build>
        </project>
    
    
    
    
    
  • 自定义拦截器

    
    
        /**
        * 
        */
        @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})
        })
        @Slf4j
        public class MyBatisInterceptor implements Interceptor{
    
            @Override
            public Object intercept(Invocation invocation) throws Throwable {
                // 拦截sql
                //被代理对象的被代理方法参数,如Executor的update方法,那么args[0]为MappedStatement类型对象,args[1]为类型object对象
                //args[1]具体为什么对象呢:当前执行查询的mapper方法对应的参数及其值
                //如:当前我执行的是UserMapper的findUser(@Param("user")User oneUser),那么这里的args[1]为一个map,key为user,值为形参oneUser对应的的值
                Object[] args = invocation.getArgs();
                MappedStatement statement = (MappedStatement) args[0];
    
                Object parameterObject = args[1];
                BoundSql boundSql = statement.getBoundSql(parameterObject);
                String sql = boundSql.getSql();
                if (sql==null||"".equals(sql)) {
                    return invocation.proceed();
                }
    
                // 重写sql
                resetSql2Invocation(invocation,sql);
    
                return invocation.proceed();
    
            }
    
    
            private void resetSql2Invocation(Invocation invocation,String sql) throws SQLException {
                final Object[] args = invocation.getArgs();
                MappedStatement statement = (MappedStatement) args[0];
                MapperMethod.ParamMap parameterObject = (MapperMethod.ParamMap)args[1];
                final BoundSql boundSql = statement.getBoundSql(parameterObject);
                List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
                //根据不同的sql类型重新构建新的sql语句
                String newsql = "";
                MyOperationType myOperationType = null;
                switch (statement.getSqlCommandType()) {
                    case INSERT:
                        myOperationType = new MyInsertOperationType();
                        newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql);
                        break;
                    case UPDATE:
                        myOperationType = new MyUpdateOperationType();
                        newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql);
                        break;
                    case DELETE:
                        myOperationType = new MyDeleteOperationType();
                        newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql);
                        break;
                    case SELECT:
                        myOperationType = new MySelectOperationType();
                        newsql = myOperationType.handle(statement,parameterMappings, parameterObject, sql);
                        break;
                    default:
                        break;
                }
    
                // 重新new一个查询语句对像
                BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), newsql, parameterMappings,
                        parameterObject);
                // 把新的查询放到statement里
                MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));
    
                // 重新设置新的参数
                args[0] = newStatement;
                System.out.println("sql语句:"+newsql);
            }
            //构造新的statement
            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) {
                    StringBuilder keyProperties = new StringBuilder();
                    for (String keyProperty : ms.getKeyProperties()) {
                        keyProperties.append(keyProperty).append(",");
                    }
                    keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
                    builder.keyProperty(keyProperties.toString());
                }
                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();
            }
    
            @Override
            public Object plugin(Object target) {
                return Plugin.wrap(target, this);
            }
    
            @Override
            public void setProperties(Properties properties) {
                // TODO Auto-generated method stub
    
            }
    
            class BoundSqlSqlSource implements SqlSource {
                private BoundSql boundSql;
    
                public BoundSqlSqlSource(BoundSql boundSql) {
                    this.boundSql = boundSql;
                }
    
                @Override
                public BoundSql getBoundSql(Object parameterObject) {
                    return boundSql;
                }
            }
    
        }
    
    
    
    
    
    
  • MyBatis拦截器配置

    
        @Configuration
        public class MyBatisConfig {
            @Autowired
            private SqlSessionFactory sqlSessionFactory;
    
            @PostConstruct
            public void addMySqlInterceptor() {
                MyBatisInterceptor interceptor = new MyBatisInterceptor();
                sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
            }
        }
    
    
    
  • jsqlparser重写sql

    MyOperationType接口

    
    
        public interface MyOperationType {
    
            String handle(MappedStatement statement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql);
    
            default boolean isNotIn(ParameterMapping parameterMapping,List<ParameterMapping> parameterMappings){
                if(parameterMappings!=null){
                    for(ParameterMapping it:parameterMappings){
                        if(it.getProperty().equals(parameterMapping.getProperty())){
                            return false;
                        }
                    }
                }
                return true;
            }
    
            default List<String> encList(String tableName,List<Column> columns){
                List<String> result = new ArrayList<>();
                List<String> list = getEncMap().get(tableName);
                if(list!=null&&columns!=null){
                    for(Column col:columns){
                        if(list.contains(col.getColumnName())){
                            result.add(col.getColumnName());
                        }
                    }
                }
                return result;
            }
    
            default String getColumnValue(MapperMethod.ParamMap parameterObject,List<ParameterMapping> parameterMappings,List<Column> columns,String column){
                if(columns!=null){
                    for(int i=0;i<columns.size();i++){
                        Column col = columns.get(i);
                        if(column.equals(col.getColumnName())){
                            ParameterMapping parameterMapping = parameterMappings.get(i);
                            String property = parameterMapping.getProperty();
                            Object result = parameterObject.get(property);
                            return (String)result;
                        }
                    }
                }
                return null;
            }
            Map<String,List<String>> getEncMap();
        }
    
    
    
    

    MyDefaultOperationType抽象类

    
        public abstract class MyDefaultOperationType implements MyOperationType{
            Map<String,List<String>> encMaps = new HashMap<>();
    
            /**
            * 假设只对t_user表的username和phone2个字段进行加密存储
            */
            public MyDefaultOperationType(){
                encMaps.put("t_user", Arrays.asList("username","phone"));
            }
    
            @Override
            public Map<String, List<String>> getEncMap() {
                return encMaps;
            }
        }
    
    
    
    

    MyInsertOperationType新增操作类型子类

    
        public class MyInsertOperationType extends MyDefaultOperationType{
            @Override
            public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) {
                try {
                    //解析sql语句
                    Statement statement = CCJSqlParserUtil.parse(sql);
                    //强制转换成insert对象
                    Insert insert = (Insert) statement;
                    //从insert中获取表名
                    Table table = insert.getTable();
                    //从insert中获取字段名
                    List<Column> columns = insert.getColumns();
    
                    List<String> needEncs = encList(table.getName(), columns);
                    if (!needEncs.isEmpty()) {
                        ExpressionList list = (ExpressionList) insert.getItemsList();
    
                        for(String col:needEncs) {
                            String newCol = col+"_enc";
                            Column miwen = new Column(newCol);
                            //在insert里添加一个字段
                            columns.add(miwen);
                            //在insert的value里添加一个占位符?
                            list.getExpressions().add(new JdbcParameter());
    
                            //将新增的字段添加到mybatis框架的字段集合中
                            ParameterMapping parameterMapping = new ParameterMapping.Builder(mystatement.getConfiguration(),newCol,String.class).build();
                            if(isNotIn(parameterMapping,parameterMappings)) {
                                parameterMappings.add(parameterMapping);
                            }
                            //添加参数值
                            parameterObject.put(newCol, getColumnValue(parameterObject,parameterMappings,columns,col)+"123");
                        }
    
                        insert.setItemsList(list);
                    }
                    return insert.toString();
                }catch (Exception e){
                    throw new RuntimeException("解析sql异常",e);
                }
            }
        }
    
    
    
    
    

    MyUpdateOperationType修改操作类型子类

    
        public class MyUpdateOperationType extends MyDefaultOperationType {
            @Override
            public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) {
                try {
                    Statement statement = CCJSqlParserUtil.parse(sql);
                    Update update = (Update)statement;
                    Table table = update.getTable();
                    List<Column> columns = update.getColumns();
    
                    List<String> needEncs = encList(table.getName(), columns);
                    if (!needEncs.isEmpty()) {
                        List<Expression> expressions = update.getExpressions();
    
                        for(String col:needEncs) {
                            String newCol = col+"_enc";
                            Column miwen = new Column(table,newCol);
    
                            Expression expression = new JdbcParameter();
                            expressions.add(expression);
    
                            //添加参数key
                            ParameterMapping parameterMapping = new ParameterMapping.Builder(mystatement.getConfiguration(),newCol,String.class).build();
                            if(isNotIn(parameterMapping,parameterMappings)) {
                                int index = parameterMappings.size()-(parameterMappings.size()- columns.size());
                                parameterMappings.add(index,parameterMapping);
                            }
                            columns.add(miwen);
    
                            //添加参数值
                            parameterObject.put(newCol, getColumnValue(parameterObject,parameterMappings,columns,col)+"123");
                        }
    
                        Expression where = update.getWhere();
                        EqualsTo status = new EqualsTo();
                        status.setLeftExpression(new Column(table, "status"));
                        StringValue stringValue = new StringValue("0");
                        status.setRightExpression(stringValue);
                        if(where!=null) {
                            AndExpression lastwhere = new AndExpression(where, status);
                            update.setWhere(lastwhere);
                        }else{
                            update.setWhere(status);
                        }
    
                        update.setExpressions(expressions);
                    }
                    return update.toString();
                }catch (Exception e){
                    throw new RuntimeException("解析sql异常",e);
                }
            }
        }
    
    

    MyDeleteOperationType删除操作类型子类

        public class MyDeleteOperationType extends MyDefaultOperationType {
            @Override
            public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) {
                try {
                    Statement statement = CCJSqlParserUtil.parse(sql);
                    Delete delete = (Delete) statement;
                    Table table = delete.getTable();
    
                    Update update = new Update();
                    update.setTable(table);
                    update.setColumns(Arrays.asList(new Column(table,"status")));
                    update.setExpressions(Arrays.asList(new StringValue("1")));
                    update.setWhere(delete.getWhere());
    
                    return update.toString();
                }catch (Exception e){
                    throw new RuntimeException("解析sql异常",e);
                }
            }
        }
    
    

    MySelectOperationType查询操作类型子类

    
        public class MySelectOperationType extends MyDefaultOperationType {
            @Override
            public String handle(MappedStatement mystatement,List<ParameterMapping> parameterMappings, MapperMethod.ParamMap parameterObject, String sql) {
                try {
                    Statement statement = CCJSqlParserUtil.parse(sql);
                    Select select = (Select) statement;
                    PlainSelect plain = (PlainSelect) select.getSelectBody();
    
                    FromItem fromItem = plain.getFromItem();
                    String tableName = fromItem.toString();
                    Table table = new Table();
                    table.setAlias(fromItem.getAlias());
                    table.setName(tableName);
    
                    Expression where = plain.getWhere();
                    EqualsTo status = new EqualsTo();
                    status.setLeftExpression(new Column(table, "status"));
                    StringValue stringValue = new StringValue("0");
                    status.setRightExpression(stringValue);
                    if(where!=null) {
                        AndExpression lastwhere = new AndExpression(where, status);
                        plain.setWhere(lastwhere);
                    }else{
                        plain.setWhere(status);
                    }
                    return select.toString();
                }catch (Exception e){
                    throw new RuntimeException("解析sql异常",e);
                }
            }
        }
    
    
    

总结

  • 流程说明

    MyBatis中的拦截器机制其实就是基于JDK的动态代理实现,在创建完目标对象后,循环用户注册的所有拦截器实现,然后对每个拦截器的@Intercepts中@Signature注解中配置的类型来和目标对象所实现的接口进行匹配,如果匹配上了,则说明目标对象是当前拦截器所关注的。

    在代理对象执行的时候,对目标方法进行拦截判断是不是当前拦截器所关注的方法,如果是则执行拦截器的intercept方法,否则直接执行目标方法。

其他

  • MappedStatement

    SQL映射语句(Mapper.xml文件每一个方法对应一个MappedStatement对象)

参考链接

  • MyBatis中拦截器(Interceptor)实现原理分析

    https://blog.csdn.net/m0_43448868/article/details/112184232

  • Mybatis执行SQL的4大基础组件详解

    https://blog.csdn.net/prestigeding/article/details/90578125

  • mybatis拦截器,并使用jsqlparser重写sql

    https://blog.csdn.net/zhaocuit/article/details/120913046

  • #私藏项目实操分享# Mybatis自定义拦截器与插件开发

    https://blog.51cto.com/u_15363374/4838804

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值