SpringBoot自定义Mybatis拦截器实现扩展功能(比如数据权限控制)


项目中需要根据用户角色对数据进行权限控制,不同角色拥有不同的数据权限。首先想到的就是去给每个sql上加个where条件?然后我又马上否定了,这样做不现实,如果每个sql都要去修改加上条件判断将会很冗余并且不利于维护和修改。后来跟同事沟通讨论,最终敲定了利用Mybatis拦截器去尝试实现,建立一个Mybatis拦截器用于拦截Executor接口的query/update方法,在拦截之后实现自己的query/update方法逻辑。话不多说,直接开干!

一、Mybatis执行过程

1 核心对象

1.Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
2.SqlSessionFactory:SqlSession工厂。
3.SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
4.Executor:MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
5.StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
6.ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
7.ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
8.TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
9.MappedStatement:MappedStatement维护了一条<select|update|delete|insert>节点的封装。
10.SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
11.BoundSql:表示动态生成的SQL语句以及相应的参数信息。

2.执行过程分析

在这里插入图片描述

二、Mybatis拦截器相关介绍

实际工作中,使用Mybatis拦截器可以做一些数据过滤、数据加密脱敏、SQL执行时间性能监控和告警等业务。MyBatis拦截器默认可以拦截的类型只有四种,即四种接口类型Executor、StatementHandler、ParameterHandler和ResultSetHandler。对于我们的自定义拦截器必须使用MyBatis提供的@Intercepts注解来指明我们要拦截的是四种类型中的哪一种接口。

2.1@Signature注解及type属性

@Intercepts	// 描述:标志该类是一个拦截器
@Signature 	// 描述:指明该拦截器需要拦截哪一个接口的哪一个方法
type; // 四种类型接口中的某一个接口,如Executor.class;
method; // 对应接口中的某一个方法名,比如Executor的query方法;
args; // 对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法;

MyBatis拦截器默认会按顺序拦截以下的四个接口中的所有方法:

org.apache.ibatis.executor.CachingExecutor
org.apache.ibatis.executor.statement.RoutingStatementHandler
org.apache.ibatis.scripting.defaults.DefaultParameterHandler
org.apache.ibatis.executor.resultset.DefaultResultSetHandler

在这里插入图片描述

2.2实现org.apache.ibatis.plugin.Interceptor接口

实现Interceptor接口,主要是实现下面几个方法:intercept(Invocation invocation)、plugin(Object target) 、setProperties(Properties properties);
intercept
进行拦截的时候要执行的方法。该方法参数Invocation类中有三个字段:

private final Object target;
private final Method method;
private final Object[] args;

可通过这三个字段分别获取下面的信息:

Object target = invocation.getTarget();//被代理对象
Method method = invocation.getMethod();//代理方法
Object[] args = invocation.getArgs();//方法参数

拦截接口以及对应的接口实现类
在这里插入图片描述
plugin
插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin.wrap(target, this);可以在这个方法中提前进行拦截对象类型判断,提高性能。

MyBatis拦截器用到责任链模式+动态代理+反射机制;
所有可能被拦截的处理类都会生成一个代理类,如果有N个拦截器,就会有N个代理,层层生成动态代理是比较耗性能的。而且虽然能指定插件拦截的位置,但这个是在执行方法时利用反射动态判断的,初始化的时候就是简单的把拦截器插入到了所有可以拦截的地方。所以尽量不要编写不必要的拦截器。另外我们可以在调用插件的地方添加判断,只要是当前拦截器拦截的对象才进行调用,否则直接返回目标对象本身,这样可以减少反射判断的次数,提高性能。

setProperties
如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,类似Spring中的@Value(“${}”)从application.properties文件获取自定义变量属性,这个时候我们就可以使用这个方法。
(1)在application.properties文件中添加配置:

mybatis.config-location=classpath:mybatis-config.xml

(2)在resources目录下添加mybatis-config.xml配置文件,并添加插件和属性配置。添加完需要注意去掉自定义MyBatis拦截器上的@Component注解,否则该拦截器相当于注册了两个,会执行两遍拦截方法。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="com.example.demo.mapper.plugin.MyPlugin">
<property name="key1" value="value1"/>
<property name="key2" value="value2"/>
<property name="key3" value="value3"/>
</plugin>
</plugins>
</configuration>

(3)在拦截器插件的setProperties方法中进行。这些自定义属性参数会在项目启动的时候被加载。

@Override
public void setProperties(Properties properties) { 

System.out.println("key1=" + properties.getProperty("key1"));
System.out.println("key2=" + properties.getProperty("key2"));
System.out.println("key3=" + properties.getProperty("key3"));
}

三、项目实战

3.1自定义注解RequiredPermission

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredPermission {
    String value();
}

3.2创建拦截器PermissionInterceptor

@Slf4j
@Intercepts(
        { @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})})
@Component
public class PermissionInterceptor implements Interceptor {
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            if (invocation.getTarget() instanceof RoutingStatementHandler) {
                //获取路由RoutingStatementHandler
                RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
                //获取StatementHandler
                StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");

                //获取sql
                BoundSql boundSql = delegate.getBoundSql();

                //获取mapper接口
                MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
                //获取mapper类文件
                Class<?> clazz = Class.forName(mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf(".")));
                //获取mapper执行方法名
                int length=mappedStatement.getId().length();
                String mName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, length);

                //遍历方法
                for (Method method : clazz.getDeclaredMethods()) {
                    //方法是否含有RequiredPermission注解,如果含有注解则将数据结果过滤
                    if (method.isAnnotationPresent(RequiredPermission.class) && mName.equals(method.getName())) {
                        RequiredPermission requiredPermission =  method.getAnnotation(RequiredPermission.class);
                        String value = requiredPermission.value();
                        String sql = boundSql.getSql();
                        //判断是否为select语句
                        if (Common.CHECK.equals(value) && mappedStatement.getSqlCommandType().toString().equals("SELECT")) {
                            //根据用户权限拼接sql,这里假设角色为管理员
                            //Boolean adminFlag = true;
                            //根据用户权限拼接sql,这里假设角色为非管理员
                            Boolean adminFlag = false;

                            //从权限表获取当前用户是管理员,则可以查询所有数据,否则只查询未删除的数据
                            if(!adminFlag){
                                //非管理员
                                sql = "select * from ( "+sql+" ) temp where temp.status != 1";
                            }
                        }
                        //将sql注入boundSql
                        ReflectUtil.setFieldValue(boundSql, "sql", sql);
                        break;
                    }
                }
            }
            return invocation.proceed();
    }

    //代理配置
    @Override
    public Object plugin(Object arg0) {
        if (arg0 instanceof StatementHandler) {
            return Plugin.wrap(arg0, this);
        } else {
            return arg0;
        }
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

3.3 MountainController

/**
 * 上古神山控制层
 *
 * @author hua
 */
@RestController
@RequestMapping(value = "/mountain")
public class MountainController {
    @Autowired
    UserMountainService userMountainService;

    @GetMapping(value = "/getMountainUserInfo")
    public ResponseEntity<List<Mountain>> getUserInfo() {
        List<Mountain> users = userMountainService.queryAllUser();
        HttpStatus status = users == null ? HttpStatus.NOT_FOUND: HttpStatus.OK;
        return new ResponseEntity<>(users, status);
    }
}

3.4 UserMountainService

public interface UserMountainService extends IService<Mountain> {

    /**
     * 获取所有上古神山相关人员信息
     * @return List
     */
    List<Mountain> queryAllUser();
}

3.5 UserMountainServiceImpl

@Service
public class UserMountainServiceImpl extends ServiceImpl<MountainMapper, Mountain> implements UserMountainService {

    @Autowired
    private MountainMapper mountainMapper;

    /**
     * 获取所有上古神山相关人员信息
     * @return List
     */
    @Override
    public List<Mountain> queryAllUser() {
        return mountainMapper.queryAllUser();
    }
}

3.6 MountainMapper

public interface MountainMapper extends BaseMapper<Mountain> {
    @RequiredPermission("check")
    @Select("select * from mountain")
    List<Mountain> queryAllUser();
}

3.7 反射获取指定对象ReflectUtil工具类

public class ReflectUtil {
    /**
     * 利用反射获取指定对象的指定属性
     *
     * @param obj       目标对象
     * @param fieldName 目标属性
     * @return 目标属性的值
     */
    public static Object getFieldValue(Object obj, String fieldName) {
        Object result = null;
        Field field = ReflectUtil.getField(obj, fieldName);
        if (field != null) {
            field.setAccessible(true);
            try {
                result = field.get(obj);
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 利用反射获取指定对象里面的指定属性
     *
     * @param obj       目标对象
     * @param fieldName 目标属性
     * @return 目标字段
     */
    private static Field getField(Object obj, String fieldName) {
        Field field = null;
        for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e) {
                // 这里不用做处理,子类没有该字段可能对应的父类有,都没有就返回null。
            }
        }
        return field;
    }

    /**
     * 利用反射设置指定对象的指定属性为指定的值
     *
     * @param obj        目标对象
     * @param fieldName  目标属性
     * @param fieldValue 目标值
     */
    public static void setFieldValue(Object obj, String fieldName, String fieldValue) {
        Field field = ReflectUtil.getField(obj, fieldName);
        if (field != null) {
            try {
                field.setAccessible(true);
                field.set(obj, fieldValue);
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }


    /**
     * @Description 获取反射对象
     **/
    public static Object reflectByPath(String path) {
        try {
            //获取类名
            String className = path.substring(0, path.lastIndexOf("."));
            //获取方法名
            String methodName = path.substring(path.lastIndexOf(".") + 1, path.length());
            // 获取字节码文件对象
            Class c = Class.forName(className);

            Constructor con = c.getConstructor();
            Object obj = con.newInstance();

            // public Method getMethod(String name,Class<?>... parameterTypes)
            // 第一个参数表示的方法名,第二个参数表示的是方法的参数的class类型
            Method method = c.getMethod(methodName);
            // 调用obj对象的 method 方法
            return method.invoke(obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

四、项目测试

4.1根据用户权限拼接sql,这里设置角色为管理员

请添加图片描述
请添加图片描述

4.2根据用户权限拼接sql,这里设置角色为非管理员

请添加图片描述
请添加图片描述
可以对比看出,通过角色和注解,能够控制SQL的执行条件,实现全局过滤查询功能。

五、项目结构及下载

在这里插入图片描述
源码下载,欢迎Star!
SpringBoot自定义Mybatis拦截器

参考资料
Springboot 自定义mybatis 拦截器,实现我们要的扩展
SpringBoot使用自定义Mybatis拦截器
mybatisplus自定义拦截器_springboot自定义拦截器
springboot自定义注解+mybatis拦截器-数据权限设计

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot 和 MyBatis 结合使用时,自定义拦截是一种强大的工具,允许开发者在执行 SQL 之前、之后或在特定业务逻辑点进行额外的操作。以下是如何使用自定义拦截的一般步骤: 1. **创建拦截接口**: 首先,你需要定义一个实现了 `org.apache.ibatis.interceptor.Interceptor` 接口的类,这个类通常是抽象的,包含你想要执行的业务逻辑方法。 ```java public interface CustomInterceptor extends Interceptor { // 自定义方法,例如预处理SQL前、后操作 Object before(Invocation invocation); // 其他可能的方法,如执行SQL后处理 Object after(Invocation invocation) throws Throwable; } ``` 2. **实现具体拦截类**: 在具体类中,你需要重写上述接口的方法,并添加你需要的业务逻辑。 ```java public class YourCustomInterceptor implements CustomInterceptor { @Override public Object before(Invocation invocation) { // 在这里执行预处理操作 Object parameter = invocation.getArgs(); // 获取参数 // ...你的代码... return parameter; // 返回处理后的参数 } @Override public Object after(Invocation invocation) { // 执行SQL后处理 // ...你的代码... } } ``` 3. **注册拦截**: 在 Spring Boot 的 MyBatis 配置中,通过 `SqlSessionFactoryBean` 或者 `SqlSessionBuilder` 注册你的拦截。可以通过 `interceptors` 属性来添加自定义拦截列表。 ```java @Configuration public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(MyBatisMapperScannerConfigurer scannerConfigurer) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setMapperScannerConfigurer(scannerConfigurer); // 添加你的拦截 factoryBean.setPlugins(Arrays.asList(new YourCustomInterceptor())); return factoryBean.getObject(); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值