package com.demo.common.interceptor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.demo.common.annotation.Permission;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
*
* SqlInterceptor
*
* Description:
*
* org.apache.ibatis.executor.Executor;
* 是 Mybatis 的内部执行器,它负责调用 StatementHandler 操作数据库,并把结果集通过 ResultSetHandler 进行自动映射,另外,它还处理了二级缓存的操作。
* Executor创建StatementHandler对象,
* 同时,创建ParameterHandler和ResultSetHandler对象,而ParameterHandler和ResultSetHandler都依赖TypeHandler;
*
*
* org.apache.ibatis.executor.statement.StatementHandler;
* 是 Mybatis 直接和数据库执行 sql 脚本的对象,另外,它也实现了 Mybatis 的一级缓存。
*
*
* org.apache.ibatis.executor.parameter.ParameterHandler;
* 是 Mybatis 实现 sql 入参设置的对象。
*
*
* org.apache.ibatis.executor.resultset.ResultSetHandler;
* 是 Mybatis 把 ResultSet 集合映射成 POJO 的接口对象。处理查询结果集;
*
* @date 2020-09-22 下午 01:46
*/
@Slf4j
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PermissionSqlInterceptor implements Interceptor {
/** mapper层数据权限 @Param 注解的value值 */
public static final String PARAM_VALUE_PERMISSION_DATA = "PD";
/** 忽略不存在的字段 */
static ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取拦截方法的参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
// 获取 Class Method
String id = ms.getId();
String clazzName = id.substring(0, id.lastIndexOf('.'));
String mapperMethod = id.substring(id.lastIndexOf('.') + 1);
Class> clazz = Class.forName(clazzName);
Method[] methods = clazz.getMethods();
// 是否有Permission注解 false:无;true:有
boolean hasPermissionAnnotation = false;
if (!ArrayUtils.isEmpty(methods)) {
for (Method method : methods) {
String methodName = method.getName();
if (mapperMethod.equals(methodName)) {
Permission permission = method.getAnnotation(Permission.class);
if (permission != null && permission.usePermission()) {
hasPermissionAnnotation = true;
}
break;
}
}
}
if (hasPermissionAnnotation) {
Object parameterObject = args[1];
BoundSql boundSql = ms.getBoundSql(parameterObject);
String oldSql = boundSql.getSql();
Map methodParamMap = (Map) parameterObject;
PermissionData permissionData = (PermissionData) methodParamMap.get(PARAM_VALUE_PERMISSION_DATA);
// 核心方法 拼接sql
String newSql = modifySql(oldSql, permissionData);
// 重新new一个查询语句对像
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
// 把新的查询放到statement里
MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
// 重要!! 重新赋值 把值传递到下去
args[0] = newMs;
// 如果直接返回executor.query() 会导致PageHelper等其他拦截器生效
}
// 其作用是将拦截器责任链向后传递
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* TODO
* 修改sql核心方法
* 各系统可根据业务实际情况重写此方法
* 异常处理,根据实际情况抛出异常
*
* @param oldSql
* @param data
* @return
* @date 2020-09-22 下午 06:19
*/
protected String modifySql(String oldSql, PermissionData data) throws Exception {
String permissionSql = StringUtils.EMPTY;
if (data == null) {
throw new NullPointerException("==> PermissionSqlInterceptor.modifySql,参数错误");
}
String id = data.getPermissionId();
if (StringUtils.isBlank(id)) {
throw new NullPointerException("==> PermissionSqlInterceptor.modifySql,菜单ID为空");
}
String userAccount = data.getCurrentUserAccount();
if (StringUtils.isBlank(userAccount)) {
throw new NullPointerException("==> PermissionSqlInterceptor.modifySql,当前登录人为空");
}
// TODO 各系统可根据业务实际情况重写此方法
return "select * from ( " + oldSql + " ) tmp_permission " + permissionSql;
}
/**
* 重新生成一个 MappedStatement
*
* @param ms
* @param newSqlSource
* @return
* @date 2020-09-23 下午 02:26
*/
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(ms.getKeyProperties()[0]);
}
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();
}
public static class BoundSqlSqlSource implements SqlSource {
private BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
package com.demo.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
*
* Permission
*
* Description: 权限注解
*
* @date 2020-09-22 下午 02:03
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Permission {
/** 是否使用数据权限 */
boolean usePermission() default true;
}
package com.demo.common.interceptor;
import com.google.common.collect.Lists;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.List;
/**
*
* PermissionData
*
* Description: 数据权限动态sql mapper层传递数据实体
*
* @date 2020-09-25 上午 09:15
*/
@Data
public class PermissionData implements Serializable {
private static final long serialVersionUID = 4982167779930712419L;
/** 当前登录人账号,必填 */
private String currentUserAccount;
/** 菜单id,必填 */
private String permissionId;
/** 动态sql数据,非必填 */
private List sqlKeyValueList;
public PermissionData(String currentUserAccount) {
this.currentUserAccount = currentUserAccount;
}
public PermissionData(String currentUserAccount, List sqlKeyValueList) {
this.currentUserAccount = currentUserAccount;
this.sqlKeyValueList = sqlKeyValueList;
}
public void clear() {
this.currentUserAccount = StringUtils.EMPTY;
this.sqlKeyValueList = Lists.newArrayList();
}
public static PermissionData empty() {
return new PermissionData(StringUtils.EMPTY, Lists.newArrayList());
}
@Data
public static class SqlKeyValue implements Serializable {
private static final long serialVersionUID = 8321308244032368498L;
/** 动态sql的key,与uap中数据权限规则的字段名匹配 */
private String key;
/** 动态sql的value值 */
private Object value;
public SqlKeyValue(String key, Object value) {
this.key = key;
this.value = value;
}
}
}
package com.demo.common.interceptor;
import lombok.Data;
import java.io.Serializable;
/**
*
* PermissionRule
*
* Description:
*
* @date 2020-09-26 下午 03:44
*/
@Data
public class PermissionRule implements Serializable {
private static final long serialVersionUID = 2291165317076536867L;
/** 规则唯一id */
private String id;
/** 对应的菜单id */
private String permissionId;
/** 规则名称 */
private String ruleName;
/** 字段 */
private String ruleColumn;
/** 条件 */
private String ruleConditions;
/** 规则值 */
private String ruleValue;
/** 状态值 1:有效 0:无效 */
private String status;
}
package com.demo.common.interceptor;
import org.apache.commons.lang3.StringUtils;
/**
*
* QueryRuleEnum
*
* Description: Query 规则 常量
* @date 2020-09-27 下午 04:50
*/
public enum QueryRuleEnum {
GT(">", "大于"),
GE(">=", "大于等于"),
LT("<", "小于"),
LE("<=", "小于等于"),
EQ("=", "等于"),
NE("!=", "不等于"),
IN("IN", "包含"),
LIKE("LIKE", "全模糊"),
LEFT_LIKE("LEFT_LIKE", "左模糊"),
RIGHT_LIKE("RIGHT_LIKE", "右模糊"),
SQL_RULES("USE_SQL_RULES", "自定义SQL片段");
private String value;
private String msg;
QueryRuleEnum(String value, String msg) {
this.value = value;
this.msg = msg;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public static QueryRuleEnum getByValue(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (QueryRuleEnum val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}
使用方法如下:
修改配置文件mybatis-config.xml,plugins标签的最后 添加以下,放在PageHelper插件后面
放在第一位 必须优先执行 因为PageHelper 插件直接返回的数据 如果不是优先执行会导致自定义拦截器不生效
注意:pagehaper插件版本 4.1.6 与 4.2.1 差异较大 本插件依赖4.2.1版本
com.github.pagehelper
pagehelper
4.2.1
使用示例如下,注解加到mapper上
/**
* 根据搜索条件获取人员列表
*
* @param name 姓名/账号
* @return
* @date 2019-04-11 下午 05:07
*/
@Permission
List getByName(@Param(PermissionSqlInterceptor.PARAM_VALUE_PERMISSION_DATA) PermissionData permissionData,
@Param("name") String name, @Param("emp") AdEmployee employee);