贴上自己的码云地址
https://gitee.com/imbobo_bo/angel-bo 下面有不懂得可以直接下载源码,进行一次深入了解
需求场景
在做项目几年经常设计各种权限,比如资源权限,还有就是数据权限了,要实现可配置的
原理
这几天闲来无事,在研究mybatis的分页和拦截器,想到了用拦截器来实现数据权限的问题,用拦截器拦截sql,然后拼装sql完成数据权限的读取
实践
第一次实践
既然要用到mybatis拦截器,顺手拿来一个写好的mybatis拦截器修改
@Intercepts(value = {
@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, CacheKey.class,BoundSql.class}),
@Signature(type=Executor.class, method="query", args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
public class SQLMonitorPlugin implements Interceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(SQLMonitorPlugin.class);
//是否监控显示SQL
private static final boolean SHOWSQL = true;
//慢SQL时间阀值,单位毫秒
private static final int SLOWER = 3000;
//大集合监控阀值,单位条
private static final int MAXCOUNT = 80;
public SQLMonitorPlugin() {
}
@Override
@SuppressWarnings("ALL")
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
//请求参数
Object parameter = null;
if (1 < invocation.getArgs().length) {
parameter = invocation.getArgs()[1];
}
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql();
//执行的sql所在的mapper文件
String resource = mappedStatement.getResource();
//执行sql的dao文件的包名+方法名
String daoMethod = mappedStatement.getId();
//去除sql文中的换行
sql = sql.replace("\n", "").replaceAll("\\s+", " ");
//sql语句类型 select、delete、insert、update
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
if (SHOWSQL) {
LOGGER.warn("[SQLMonitorPlugin]SQL监控:{}|{},执行SQL:{},参数:{}", resource, daoMethod,
sql, JsonSerializeUtil.toJson(parameter));
}
try {
//这里处理SQL 替换SQL
long start = System.currentTimeMillis();
Object e = invocation.proceed();
long timeTicket = System.currentTimeMillis() - start;
if (SLOWER < timeTicket) {
LOGGER.warn("[SQLMonitorInterceptor]SQL监控:{}|{},慢SQL({}/{}ms){},参数:{}",
resource, daoMethod, timeTicket, SLOWER, sql, JsonSerializeUtil.toJson(parameter));
}
if (e instanceof Collection) {
int ct = ((Collection<?>) e).size();
if (MAXCOUNT < ct) {
LOGGER.warn("[SQLMonitorInterceptor]SQL监控:{}|{},大集合({}/{}){},参数:{}",
resource, daoMethod, ct, MAXCOUNT, sql, JsonSerializeUtil.toJson(parameter));
}
}
return e;
} catch (Exception e) {
LOGGER.warn("[SQLMonitorPlugin]SQL监控:{}|{},执行出错:{},参数:{}", resource, daoMethod,
sql, JsonSerializeUtil.toJson(parameter), e);
throw e;
}
}
。。。省略了
}
想着可以在这个记录SQL的拦截器里直接修改SQL,很可惜,失败了,因为拦截器有几种具体要拦截的阶段,向上边的代码如果修改了SQL然而执行的还是原SQL,所以达不到想要的结果。 思考之后,决定再另起一个拦截器
第二次尝试
这次选择了拦截器
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
这个的意思是拦截SQL构建过程,在执行之前 摘一段码云上的代码,代码并不完整,只体会题意
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class })
})
@Component
public class PrepareInterceptor implements Interceptor {
/** 日志 */
private static final Logger log = LoggerFactory.getLogger(PrepareInterceptor.class);
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
@Override
public Object intercept(Invocation invocation) throws Throwable {
if(log.isInfoEnabled()){
log.info("进入 PrepareInterceptor 拦截器...");
}
if(invocation.getTarget() instanceof RoutingStatementHandler) {
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
//通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
//千万不能用下面注释的这个方法,会造成对象丢失,以致转换失败
//MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
PermissionAop permissionAop = PermissionUtils.getPermissionByDelegate(mappedStatement);
if(permissionAop == null){
if(log.isInfoEnabled()){
log.info("数据权限放行...");
}
return invocation.proceed();
}
if(log.isInfoEnabled()){
log.info("数据权限处理【拼接SQL】...");
}
BoundSql boundSql = delegate.getBoundSql();
ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql()));
}
return invocation.proceed();
}
/**
* 权限sql包装
* @author GaoYuan
* @date 2018/4/17 上午9:51
*/
protected String permissionSql(String sql) {
StringBuilder sbSql = new StringBuilder(sql);
String userMethodPath = PermissionConfig.getConfig("permission.client.userid.method");
//当前登录人
String userId = (String)ReflectUtil.reflectByPath(userMethodPath);
//如果用户为 1 则只能查询第一条
if("1".equals(userId)){
//sbSql = sbSql.append(" limit 1 ");
//如果有动态参数 regionCd
if(true){
String premission_param = "regionCd";
//select * from (select id,name,region_cd from sys_exam ) where region_cd like '${}%'
String methodPath = PermissionConfig.getConfig("permission.client.params." + premission_param);
String regionCd = (String)ReflectUtil.reflectByPath(methodPath);
sbSql = new StringBuilder("select * from (").append(sbSql).append(" ) s where s.regionCd like concat("+ regionCd +",'%') ");
}
}
return sbSql.toString();
}
}
运行结果是:SQL确实拼装完成,加入了权限控制的代码段,但是再运行时与mybatis分页插件起了冲突
SELECT count(0)
FROM base_user u LEFT JOIN base_organization o ON u.organization_id = o.organization_id
LEFT JOIN (SELECT
GROUP_CONCAT(r.role_name) AS role_name,
ur.user_id
FROM base_user_role ur LEFT JOIN base_role r ON r.role_id = ur.role_id
) roleT ON roleT.user_id = u.user_id
WHERE 1 = 1
拼接成了
SELECT b.*
FROM (SELECT count(0)
FROM base_user u LEFT JOIN base_organization o ON u.organization_id = o.organization_id
LEFT JOIN (SELECT
GROUP_CONCAT(r.role_name) AS role_name,
ur.user_id
FROM base_user_role ur LEFT JOIN base_role r ON r.role_id = ur.role_id
GROUP BY ur.user_id) roleT ON roleT.user_id = u.user_id
WHERE 1 = 1) AS b
WHERE b.create_user_id = 1
SQL执行报错了,分析原因,肯定是分页的拦截器优先于权限拦截器了 这时候去看分页的源码了 我这里用的pagehelper 5.0.4 码云上有的
**
* Mybatis - 通用分页拦截器<br/>
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
//缓存count查询的ms
protected Cache<String, MappedStatement> msCountMap = null;
private Dialect dialect;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private Field additionalParametersField;
private String countSuffix = "_COUNT";
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
原来 Executor拦截器优先于 StatementHandler ,由于第一次尝试的时候用了 Executor 所以貌似进入了死胡同
第三次尝试
有没有办法在执行分页的时候或者之前完成拼装SQL呢
if (!dialect.skip(ms, parameter, rowBounds)) {
//反射获取动态参数
String msId = ms.getId();
Configuration configuration = ms.getConfiguration();
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
String countMsId = msId + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
if(countMs != null){
count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
//处理查询总数
//返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
找到pagehelper拦截器执行count方法executeAutoCount
private Long executeAutoCount(Executor executor, MappedStatement countMs,
Object parameter, BoundSql boundSql,
RowBounds rowBounds, ResultHandler resultHandler) throws IllegalAccessException, SQLException {
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
//调用方言获取 count sql
String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
//countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
return count;
}
主要是
String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
再看 dialect是啥
@Override
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
try {
//反射获取 BoundSql 中的 additionalParameters 属性
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new PageException(e);
}
}
原来是初始化的时候注入的 默认使用的是
package com.github.pagehelper;
import com.github.pagehelper.dialect.AbstractHelperDialect;
import com.github.pagehelper.page.PageAutoDialect;
import com.github.pagehelper.page.PageMethod;
import com.github.pagehelper.page.PageParams;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.RowBounds;
import java.util.List;
import java.util.Properties;
/**
* Mybatis - 通用分页拦截器<br/>
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
public class PageHelper extends PageMethod implements Dialect {
private PageParams pageParams;
private PageAutoDialect autoDialect;
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
}
public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
return autoDialect.getDelegate().getPageSql(sql, page, pageKey);
}
那autoDialect是啥呢 在拦截器注入的时候调用了这个方法
@Override
public void setProperties(Properties properties) {
pageParams = new PageParams();
autoDialect = new PageAutoDialect();
pageParams.setProperties(properties);
autoDialect.setProperties(properties);
}
继续深入
public void setProperties(Properties properties) {
//多数据源时,获取 jdbcurl 后是否关闭数据源
String closeConn = properties.getProperty("closeConn");
if (StringUtil.isNotEmpty(closeConn)) {
this.closeConn = Boolean.parseBoolean(closeConn);
}
//指定的 Helper 数据库方言,和 不同
String dialect = properties.getProperty("helperDialect");
//运行时获取数据源
String runtimeDialect = properties.getProperty("autoRuntimeDialect");
//1.动态多数据源
if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
this.autoDialect = false;
this.properties = properties;
}
//2.动态获取方言
else if (StringUtil.isEmpty(dialect)) {
autoDialect = true;
this.properties = properties;
}
//3.指定方言
else {
autoDialect = false;
this.delegate = initDialect(dialect, properties);
}
}
看下initDialect这个方法
/**
* 初始化 helper
*
* @param dialectClass
* @param properties
*/
private AbstractHelperDialect initDialect(String dialectClass, Properties properties) {
AbstractHelperDialect dialect;
if (StringUtil.isEmpty(dialectClass)) {
throw new PageException("使用 PageHelper 分页插件时,必须设置 helper 属性");
}
try {
Class sqlDialectClass = resloveDialectClass(dialectClass);
if (AbstractHelperDialect.class.isAssignableFrom(sqlDialectClass)) {
dialect = (AbstractHelperDialect) sqlDialectClass.newInstance();
} else {
throw new PageException("使用 PageHelper 时,方言必须是实现 " + AbstractHelperDialect.class.getCanonicalName() + " 接口的实现类!");
}
} catch (Exception e) {
throw new PageException("初始化 helper [" + dialectClass + "]时出错:" + e.getMessage(), e);
}
dialect.setProperties(properties);
return dialect;
}
/**
* 反射类
*
* @param className
* @return
* @throws Exception
*/
private Class resloveDialectClass(String className) throws Exception {
if (dialectAliasMap.containsKey(className.toLowerCase())) {
return dialectAliasMap.get(className.toLowerCase());
} else {
return Class.forName(className);
}
}
原来这里是获取数据库方言,然后分页时调用数据库方言的实现类,完成分页SQL拼装的,所以接下来就是看下这个方言了,从上面代码可以看到方言是可以加载指定类的
return Class.forName(className);
static {
//注册别名
dialectAliasMap.put("hsqldb", HsqldbDialect.class);
dialectAliasMap.put("h2", HsqldbDialect.class);
dialectAliasMap.put("postgresql", HsqldbDialect.class);
dialectAliasMap.put("phoenix", HsqldbDialect.class);
dialectAliasMap.put("mysql", MySqlDialect.class);
dialectAliasMap.put("mariadb", MySqlDialect.class);
dialectAliasMap.put("sqlite", MySqlDialect.class);
dialectAliasMap.put("oracle", OracleDialect.class);
dialectAliasMap.put("db2", Db2Dialect.class);
dialectAliasMap.put("informix", InformixDialect.class);
dialectAliasMap.put("sqlserver", SqlServerDialect.class);
dialectAliasMap.put("sqlserver2012", SqlServer2012Dialect.class);
dialectAliasMap.put("derby", SqlServer2012Dialect.class);
}
public class MySqlDialect extends AbstractHelperDialect {
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getPageSize());
} else {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getStartRow());
sqlBuilder.append(",");
sqlBuilder.append(page.getPageSize());
pageKey.update(page.getStartRow());
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
}
最终发现分页调用的SQL为
/**
* 针对 PageHelper 的实现
*
* @author liuzh
* @since 2016-12-04 14:32
*/
public abstract class AbstractHelperDialect extends AbstractDialect {
/**
* 获取分页参数
*
* @param <T>
* @return
*/
public <T> Page<T> getLocalPage() {
return PageHelper.getLocalPage();
}
@Override
public final boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
//该方法不会被调用
return true;
}
@Override
public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
return page.isCount();
}
@Override
public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
Page<Object> page = getLocalPage();
String countColumn = page.getCountColumn();
if (StringUtil.isNotEmpty(countColumn)) {
return countSqlParser.getSmartCountSql(boundSql.getSql(), countColumn);
}
return countSqlParser.getSmartCountSql(boundSql.getSql());
}
@Override
public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
page.setTotal(count);
if (rowBounds instanceof PageRowBounds) {
((PageRowBounds) rowBounds).setTotal(count);
}
//pageSize < 0 的时候,不执行分页查询
//pageSize = 0 的时候,还需要执行后续查询,但是不会分页
if (page.getPageSize() < 0) {
return false;
}
return count > 0;
}
@Override
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
return parameterObject;
}
@Override
public boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
if (page.getPageSize() > 0) {
return true;
}
return false;
}
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = getLocalPage();
return getPageSql(sql, page, pageKey);
}
/**
* 单独处理分页部分
*
* @param sql
* @param page
* @param pageKey
* @return
*/
public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
@Override
public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
Page page = getLocalPage();
if (page == null) {
return pageList;
}
page.addAll(pageList);
if (!page.isCount()) {
page.setTotal(-1);
} else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
page.setTotal(pageList.size());
}
return page;
}
@Override
public void afterAll() {
}
@Override
public void setProperties(Properties properties) {
}
}
这就好办了,重写这个,然后让功能加载这个自定义方言就可以了
@Slf4j
public abstract class PermissionDialect extends AbstractHelperDialect{
/**
* 单独处理分页部分
*
* @param sql
* @param page
* @param pageKey
* @return
*/
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getPageSize());
} else {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getStartRow());
sqlBuilder.append(",");
sqlBuilder.append(page.getPageSize());
pageKey.update(page.getStartRow());
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
@Override
public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
ReflectUtil.setFieldValue(boundSql, "sql", permissionSql(boundSql.getSql()));
return super.getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
}
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
return super.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
}
public abstract String permissionSql(String sql);
}
在需要实现的地方,集成这个类,并实现permissionSql方法,然后指定加载这个就可以了
/**
* 数据权限处理SQL
*/
@Slf4j
public class SQLPermissionDialect extends PermissionDialect {
@Override
public String permissionSql(String sql) {
PermissionService permissionService = getBean(PermissionService.class);
WebReturn webReturn ;
AuthUtil authUtil = getBean(AuthUtil.class);
UserRole userRole = new UserRole();
userRole.setUserId(authUtil.getUserIdFromToken());
webReturn = permissionService.getThePermissionDataByCurrentUser(userRole);
if(webReturn != null && "".equals(webReturn.getData())){
return sql;
}
StringBuilder sbSql = new StringBuilder(sql);
sbSql = new StringBuilder("select permissionDataTable.* from (").append(sbSql).append(" ) as permissionDataTable ");
if(webReturn != null && webReturn.getData() != null){
sbSql.append(webReturn.getData());
}
log.info("权限拼装SQL:{}", sbSql.toString());
return sbSql.toString();
}
}
pagehelper:
helper-dialect: com.angel.bo.admin.plugin.SQLPermissionDialect
reasonable: true
support-methods-arguments: true