SpringBoot自定义Mybatis拦截器实现扩展功能(比如数据权限控制)
SpringBoot自定义Mybatis拦截器
- 一、Mybatis执行过程
- 1 核心对象
- 2.执行过程分析
- 二、Mybatis拦截器相关介绍
- 2.1@Signature注解及type属性
- 2.2实现org.apache.ibatis.plugin.Interceptor接口
- 三、项目实战
- 3.1自定义注解RequiredPermission
- 3.2创建拦截器PermissionInterceptor
- 3.3 MountainController
- 3.4 UserMountainService
- 3.5 UserMountainServiceImpl
- 3.6 MountainMapper
- 3.7 反射获取指定对象ReflectUtil工具类
- 四、项目测试
- 4.1根据用户权限拼接sql,这里设置角色为管理员
- 4.2根据用户权限拼接sql,这里设置角色为非管理员
- 五、项目结构及下载
项目中需要根据用户角色对数据进行权限控制,不同角色拥有不同的数据权限。首先想到的就是去给每个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拦截器-数据权限设计