实际开发过程中可能会因为各种原因导致对数据库操作的代码缺少where条件,对于查询操作可能会导致全表扫描,对于更新操作可能会导致整个表的字段错误更新。大多数场景都不是符合我们的预期的。
所以做了这样一个插件(可以根据配置application.yml动态开启/关闭插件。可以检测where条件是否合法来决定打印error日志or抛出异常)
Mybatis 预留了org.apache.ibatis.plugin.Interceptor 接口,通过实现该接口,可以对Mybatis的执行流程进行拦截,接口的定义如下:
public interface Interceptor {
// 插件的核心逻辑在这个方法中
Object intercept(Invocation invocation) throws Throwable;
// 使用当前的Interceptor创建代理,通常的实现都是 Plugin.wrap(target, this),wrap方法内使用 jdk 创建动态代理对象;
Object plugin(Object target);
// 可以获取Mybatis配置文件中中设置的参数
void setProperties(Properties properties);
}
Mybatis插件可拦截的类为以下四个:
Executor
StatementHandler
ParameterHandler
ResultSetHandler
这些类的代理流程
在执行query方法之前都可以获取到我们要执行的原始sql,然后进行条件检测判断。所以我们可以织入我们代码的拦截类可以为 Executor、StatementHandler、ParameterHandler。这里选择了Executor。
mybatis的详细执行流程和动态代理实现sql代理等流程戳这里:编码技巧——数据加密(二)Mybatis拦截器
common项目:
自定义条件检测配置类(把配置文件中的mybatis配置注入到该类中,实现配置和拦截器代码分离的作用。方便各个服务灵活接入)
/**
* mybatis自定义条件检测配置类
*
* @see MybatisConditionDetectionInterceptor
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "mybatis.condition.detection")
@ConditionalOnProperty(prefix = "mybatis.condition.detection", name = "enable", havingValue = "true")
public class MybatisConditionDetectionProperty {
/**
* 条件检测拦截器开关,默认不生效。
* 仅在enable=true时条件检测拦截器才会生效
*/
private Boolean enable = false;
/**
* 检测模式,出现非法条件时 1 打印error日志、2 抛出RunTimeException。默认为打印error日志
* @see MybatisConditionDetectionEnum
*/
private Integer detectionMode = 1;
;
}
Mybatis条件检测拦截器(根据配置类的属性实现异常检测、出现异常条件时反馈机制“打日志还是抛异常”)
/**
* Mybatis条件检测拦截器
* 可支持example方式生成的sql和动态sql的条件检测。
* 参考:
* 编码技巧——数据加密(二)Mybatis拦截器
*
*
* @date 2023/06/12
* @see Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
* @see Executor#query(MappedStatement, Object, RowBounds, ResultHandler)
* @see Executor#update(MappedStatement, Object)
*/
@Slf4j
@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}),
})
public class MybatisConditionDetectionInterceptor implements Interceptor {
private static final String PSM_NAME_UNKNOWN = "unknown";
private MybatisConditionDetectionProperty mybatisConditionDetectionProperty;
private String psm;
public MybatisConditionDetectionInterceptor(MybatisConditionDetectionProperty mybatisConditionDetectionProperty, String psm) {
this.mybatisConditionDetectionProperty = mybatisConditionDetectionProperty;
this.psm = StringTools.nonBlankOrElse(psm, PSM_NAME_UNKNOWN);
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 如果没有配置或者条件检测配置未开启,直接执行sql。
if (Objects.isNull(this.mybatisConditionDetectionProperty) || Boolean.FALSE.equals(this.mybatisConditionDetectionProperty.getEnable())) {
return invocation.proceed();
}
//获取执行参数
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
// 获取当前方法的全限定名 eg:com.bytedance.homed.decorate.construction.infrastructure.repository.mysql.mapper.CameraDataInfoMapper.insert
String mapperMethodAllName = ms.getId();
int lastIndex = mapperMethodAllName.lastIndexOf(".");
// 获取当前类(接口)的全限定名 eg:com.bytedance.homed.decorate.construction.infrastructure.repository.mysql.mapper.CameraDataInfoMapper
String mapperClassStr = mapperMethodAllName.substring(0, lastIndex);
// 获取当前方法名 eg:insert
String mapperClassMethodStr = mapperMethodAllName.substring((lastIndex + 1));
// 获取类的所有方法
Class> mapperClass = Class.forName(mapperClassStr);
// 根据配置获取出现异常条件时的提示方式
MybatisConditionDetectionEnum conditionDetection = this.mybatisConditionDetectionProperty == null ?
MybatisConditionDetectionEnum.ERROR_LOG :
MybatisConditionDetectionEnum.valueOf(this.mybatisConditionDetectionProperty.getDetectionMode());
Method[] methods = mapperClass.getMethods();
for (Method method : methods) {
// 如果匹配到当前方法,进行sql条件检验
if (method.getName().equals(mapperClassMethodStr)) {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String executeSql = boundSql.getSql().toLowerCase(Locale.CHINA).replace("[\\t\\n\\r]", " ");
// 如果是插入语句,不进行检测。
if (method.getName().contains("insert")) {
continue;
}
// 如果是查询/更新/删除语句,检测是否包含where关键字和是否包含条件
else if (method.getName().contains("delete") || method.getName().contains("update") || method.getName().contains("select")) {
if (!executeSql.contains("where")) {
if (MybatisConditionDetectionEnum.THROW_ERROR == conditionDetection) {
throw new RuntimeException("警告,该sql没有where条件,请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
} else {
log.error("注意,该sql没有where条件,请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
}
}
if (executeSql.contains("where")) {
int wherePosition = executeSql.indexOf("where");
String substring = StringUtils.trim(executeSql.substring(wherePosition + 5));
if (StringUtils.isBlank(substring)) {
if (MybatisConditionDetectionEnum.THROW_ERROR == conditionDetection) {
throw new RuntimeException("警告,该sql语句where后缺少条件!请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
} else {
log.error("警告,该sql语句where后缺少条件!请检查逻辑是否正确。psm=" + this.psm + "sql=" + executeSql);
}
}
}
}
// 自定义sql方法并且不包含增删改查关键字的不予处理
else {
continue;
}
}
}
//继续执行逻辑
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
//获取代理权
if (o instanceof Executor) {
//如果是Executor(执行增删改查操作),否则拦截下来
return Plugin.wrap(o, this);
} else {
return o;
}
}
@Override
public void setProperties(Properties properties) {
}
}
条件检测插件自动装配类(根据条件检测配置类是否存在,灵活注入条件检测拦截器)
/**
* Mybatis条件检测自动装配类
*
* @date 2022/06/12
*/
@Configuration
public class MybatisConditionDetectionConfiguration {
@Value("${spring.application.name}")
private String psm;
@Bean
@ConditionalOnBean(MybatisConditionDetectionProperties.class)
public Interceptor mybatisConditionDetectionInterceptor(MybatisConditionDetectionProperties mybatisConditionDetectionProperties) {
return new MybatisConditionDetectionInterceptor();
}
}
动态sql条件检测调试
example方式生成的sql条件检测调试