by shihang.mai
1. mybatis層次結(jié)構(gòu)
2. 插件實(shí)現(xiàn)
代碼思路,就是利用mybatis插件,在上面流程中進(jìn)行攔截,做自己的業(yè)務(wù)邏輯
3. 代碼實(shí)現(xiàn)(核心步驟)
注解:是否開啟多租戶和是否開啟敏感sql
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface MultiTenant {
/**
* 是否開啟使用框架層的多租戶鸳惯,默認(rèn)開啟
* @return
*/
boolean flag() default true;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface SqlLimit {
/**
* 項(xiàng)目默認(rèn)禁用drop,create,alter,truncate sql
* @return
*/
boolean flag() default true;
}
核心配置類PluginConfiguration螟炫,需要boostrap中加入
@Configuration
public class PluginConfiguration {
@Resource
private BeanFactory beanFactory;
//為了提速,不是重點(diǎn)
@Bean
public MultiTenantMapperCacheManager multiTenantMapperCacheManager() {
return new MultiTenantMapperCacheManager();
}
//為了提速,不是重點(diǎn)
@Bean
public SqlLimitMapperCacheManager sqlLimitMapperCacheManager() {
return new SqlLimitMapperCacheManager();
}
@Bean
public Interceptor tenantInterceptor(){
Interceptor interceptor = new TenantInterceptor(multiTenantMapperCacheManager());
Properties properties = new Properties();
properties.setProperty(TenantConstant.DIALECT, "postgresql");
properties.setProperty(TenantConstant.TENANTID_FIELD, TenantConstant.TENANT_ID);
interceptor.setProperties(properties);
return interceptor;
}
@Bean
public Interceptor sqlCheckInterceptor(){
Interceptor interceptor = new SqlCheckInterceptor(sqlLimitMapperCacheManager());
Properties properties = new Properties();
properties.setProperty(TenantConstant.DIALECT, "postgresql");
interceptor.setProperties(properties);
return interceptor;
}
/**
* 多租戶線程池宣肚,為了解決異步線程租戶id的傳遞
* @return MultiTenantLazyTraceThreadPoolTaskExecutor
*/
@Bean
public MultiTenantLazyTraceThreadPoolTaskExecutor multiTenantLazyTraceThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(3);
threadPoolTaskExecutor.setKeepAliveSeconds(60);
threadPoolTaskExecutor.setMaxPoolSize(5);
threadPoolTaskExecutor.setQueueCapacity(1000);
threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
threadPoolTaskExecutor.setThreadNamePrefix("base-multitsenant-pool-");
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
threadPoolTaskExecutor.initialize();
return new MultiTenantLazyTraceThreadPoolTaskExecutor(this.beanFactory, threadPoolTaskExecutor);
}
}
租戶攔截類TenantInterceptor
@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}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Order(-20)
public class TenantInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(TenantInterceptor.class);
/**
* 當(dāng)前數(shù)據(jù)庫(kù)的方言
*/
private String dialect;
/**
* 多租戶字段名稱
*/
private String tenantIdField;
private SqlConditionHelper conditionHelper;
private SqlLimitHelper sqlLimitHelper;
private AnnotationHelper annotationHelper;
private final MultiTenantMapperCacheManager multiTenantMapperCacheManager;
public TenantInterceptor(MultiTenantMapperCacheManager multiTenantMapperCacheManager) {
this.multiTenantMapperCacheManager = multiTenantMapperCacheManager;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
String tenantId = MultiTenantHolders.getTenantId();
//租戶id為空時(shí)不做處理
if (StringUtils.isBlank(tenantId)) {
//todo 測(cè)試tenant_id暫時(shí)固定
tenantId = "0210000001";
//return invocation.proceed();
}
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
BoundSql boundSql = ms.getBoundSql(parameter);
//判斷調(diào)用棧的mapper類跟方法是否有注解
ClazzMethodInfo clazzMethodInfo = annotationHelper.getClassAndMethod(ms.getId());
boolean flag = checkAnnotation(clazzMethodInfo);
logger.info("old sql:{}", boundSql.getSql());
if (flag) {
String newSql = addTenantCondition(boundSql.getSql(), tenantId);
logger.info("new sql:{}", newSql);
//重新構(gòu)造MappedStatement
buildMappedStatement(ms, args, newSql);
}
return invocation.proceed();
}
/**
* 重新構(gòu)造mappedStatement
* @param mappedStatement
* @param args
* @param sql
*/
private void buildMappedStatement(MappedStatement mappedStatement, final Object[] args, String sql) {
// 獲取攔截方法的參數(shù)
BoundSql currentBoundSql = mappedStatement.getBoundSql(args[1]);
BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql,
currentBoundSql.getParameterMappings(), currentBoundSql.getParameterObject());
// 把新的查詢放到statement里
MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
for (ParameterMapping mapping : currentBoundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (currentBoundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, currentBoundSql.getAdditionalParameter(prop));
}
}
args[0] = newMs;
}
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();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 設(shè)置屬性
* @Param properties 注入屬性參數(shù)
*/
@Override
public void setProperties(Properties properties) {
dialect = properties.getProperty(TenantConstant.DIALECT);
if (StringUtils.isBlank(dialect)) {
throw new IllegalArgumentException("MultiTenantPlugin need dialect property value");
}
tenantIdField = properties.getProperty(TenantConstant.TENANTID_FIELD);
if (StringUtils.isBlank(tenantIdField)) {
throw new IllegalArgumentException("MultiTenantPlugin need tenantIdField property value");
}
//多租戶條件字段決策器
conditionHelper = new SqlConditionHelper(() -> false);
sqlLimitHelper = new SqlLimitHelper();
annotationHelper = new AnnotationHelper();
}
/**
* 校驗(yàn)注解 注解可作用在類與方法上,方法注解優(yōu)先于類注解
* @param data 類方法信息
* @return true/false
*/
private boolean checkAnnotation(ClazzMethodInfo data) {
if (Objects.isNull(data) || Objects.isNull(data.getMethodName()) || Objects.isNull(data.getClassName())) {
return false;
}
String namespace = data.getNamespace();
String cacheKey = getCacheKey(namespace);
if (StringUtils.isNotEmpty(cacheKey)) {
return Boolean.valueOf(cacheKey);
}
MultiTenant annotation = annotationHelper.checkAnnotation(data, MultiTenant.class);
if (Objects.isNull(annotation)) {
setCacheKey(namespace, false);
return false;
}
setCacheKey(namespace, annotation.flag());
return annotation.flag();
}
// private boolean checkAnnotation(ClazzMethodInfo data) {
/*if (Objects.isNull(data) || Objects.isNull(data.getMethodName()) || Objects.isNull(data.getClassName())) {
return false;
}
String namespace = data.getNamespace();
//緩存校驗(yàn)悠栓,定位到緩存值直接返回
String cacheKey = getCacheKey(namespace);
if (StringUtils.isNotEmpty(cacheKey)) {
return Boolean.valueOf(cacheKey);
}
try {
//攔截方法級(jí)別的注解
Method[] methods = Class.forName(data.getClassName()).getMethods();
for (int i = 0; i < methods.length; i++) {
if (data.getMethodName().equals(methods[i].getName())) {
MultiTenant annotation = methods[i].getAnnotation(MultiTenant.class);
if (Objects.nonNull(annotation)) {
setCacheKey(namespace, annotation.flag());
return annotation.flag();
}
}
}
//攔截類級(jí)別的注解霉涨,在方法級(jí)別沒(méi)有定位到注解按价,才去定位類注解
MultiTenant annotation = Class.forName(data.getClassName()).getAnnotation(MultiTenant.class);
if (Objects.isNull(annotation)) {
setCacheKey(namespace, false);
return false;
}
return annotation.flag();
} catch (ClassNotFoundException e) {
ExceptionLogger.log(e);
}
setCacheKey(namespace, false);*/
// return false;
// }
private void setCacheKey(String namespace, boolean flag) {
logger.info("設(shè)置annotation緩存:{}, flag:{}", namespace, flag);
multiTenantMapperCacheManager.update(namespace, flag);
}
private String getCacheKey(String namespace) {
logger.info("get annotation緩存:{}", namespace);
return multiTenantMapperCacheManager.get(namespace);
}
/**
* 給sql語(yǔ)句where添加租戶id過(guò)濾條件
* @param sql 要添加過(guò)濾條件的sql語(yǔ)句
* @param tenantId 當(dāng)前的租戶id
* @return 添加條件后的sql語(yǔ)句
*/
private String addTenantCondition(String sql, String tenantId) {
//todo throw
if (StringUtils.isBlank(sql) || StringUtils.isBlank(tenantIdField)) return sql;
//處理limit offset size
SqlLimiter sqlLimiter = sqlLimitHelper.splitLimitOffsetSize(sql);
if (StringUtils.isNotEmpty(sqlLimiter.getSql())){
sql = sqlLimiter.getSql();
}
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dialect);
if (CollectionUtils.isEmpty(statementList)) return sql;
SQLStatement sqlStatement = statementList.get(0);
conditionHelper.addStatementCondition(sqlStatement, tenantIdField, tenantId);
String sqllimit = StringUtils.isNotEmpty(sqlLimiter.getLimit())?" "+sqlLimiter.getLimit():"";
return SQLUtils.toSQLString(statementList, DbType.postgresql) + sqllimit;
}
}
敏感sql攔截類SqlCheckInterceptor
@Intercepts({
/*@Signature(type = StatementHandler.class, method = "update",
args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}),*/
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class}),
})
@Order(-10)
public class SqlCheckInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlCheckInterceptor.class);
/**
* 當(dāng)前數(shù)據(jù)庫(kù)的方言
*/
private String dialect;
private SqlConditionHelper conditionHelper;
private SqlLimitHelper sqlLimitHelper;
private AnnotationHelper annotationHelper;
private final SqlLimitMapperCacheManager sqlLimitMapperCacheManager;
public SqlCheckInterceptor(SqlLimitMapperCacheManager sqlLimitMapperCacheManager) {
this.sqlLimitMapperCacheManager = sqlLimitMapperCacheManager;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取代理對(duì)象
StatementHandler target = (StatementHandler) invocation.getTarget();
// 獲取sql語(yǔ)句的id(id取法直接通過(guò)getter方法獲取,因此這里通過(guò)反射進(jìn)行獲取)
MetaObject metaObject = MetaObject.forObject(target, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
String id = (String) metaObject.getValue("delegate.mappedStatement.id");
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
logger.info("old sql:{}", boundSql.getSql());
//判斷調(diào)用棧的mapper類跟方法是否有注解
ClazzMethodInfo clazzMethodInfo = annotationHelper.getClassAndMethod(id);
boolean flag = checkAnnotation(clazzMethodInfo);
//攔截敏感sql
if (flag) {
checkCondition(boundSql.getSql());
}
return invocation.proceed();
}
/**
* 校驗(yàn)注解 注解可作用在類與方法上笙瑟,方法注解優(yōu)先于類注解
* @param data 類方法信息
* @return true/false
*/
private boolean checkAnnotation(ClazzMethodInfo data) {
if (Objects.isNull(data) || Objects.isNull(data.getMethodName()) || Objects.isNull(data.getClassName())) {
return false;
}
String namespace = data.getNamespace();
String cacheKey = getCacheKey(namespace);
if (StringUtils.isNotEmpty(cacheKey)) {
return Boolean.valueOf(cacheKey);
}
SqlLimit annotation = annotationHelper.checkAnnotation(data, SqlLimit.class);
if (Objects.isNull(annotation)) {
setCacheKey(namespace, false);
return false;
}
setCacheKey(namespace, annotation.flag());
return annotation.flag();
}
private void setCacheKey(String namespace, boolean flag) {
logger.info("設(shè)置annotation緩存:{}, flag:{}", namespace, flag);
sqlLimitMapperCacheManager.update(namespace, flag);
}
private String getCacheKey(String namespace) {
logger.info("get annotation緩存:{}", namespace);
return sqlLimitMapperCacheManager.get(namespace);
}
//todo throw
private void checkCondition(String sql) {
//update/delete no condition
//drop/create/alter limit
//update 條件沒(méi)有定位到索引
if (StringUtils.isBlank(sql)) return;
//處理limit offset size
SqlLimiter sqlLimiter = sqlLimitHelper.splitLimitOffsetSize(sql);
if (Objects.nonNull(sqlLimiter) && StringUtils.isNotEmpty(sqlLimiter.getSql())){
sql = sqlLimiter.getSql();
}
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dialect);
if (CollectionUtils.isEmpty(statementList)) return;
SQLStatement sqlStatement = statementList.get(0);
conditionHelper.checkNonCondition(sqlStatement);
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 設(shè)置屬性
* @Param properties 注入屬性參數(shù)
*/
@Override
public void setProperties(Properties properties) {
dialect = properties.getProperty("dialect");
if (StringUtils.isBlank(dialect)) {
throw new IllegalArgumentException("MultiTenantPlugin need dialect property value");
}
//決策器
conditionHelper = new SqlConditionHelper(() -> false);
sqlLimitHelper = new SqlLimitHelper();
annotationHelper = new AnnotationHelper();
}
}