mybatis 插件机制
mybatis 提供了一种插件功能,虽然叫插件,其实是拦截器功能。底层通过采用责任链设计模式,代理原来的 mybatis 相关对象来改变默认行为,比如修改 sql 等
没有插件的运行图
有插件的运行图
拦截对象与方法
如上图,允许拦截的对象有 4 个,分别是 Executor(执行器)、ParameterHandler(参数处理)、ResultSetHandler(结果处理)、StatementHandler(sql 构建处理)。允许拦截的方法就是这 4 个对象具有的方法。里面的方法都很清晰,比如 Executor 的 query、update 方法对应查询和更新、 StatementHandler 的 preper 方法对应设置 sql 参数
插件示例
举例打印 sql 和数据权限
插件编写
package com.dtyunxi.yundt.cube.center.item.svr.config;
import com.dtyunxi.yundt.cube.center.item.api.Permission;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.core.annotation.Order;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
/**
* 1,可以拦截多个对象和方法,@Intercepts 是个数组
* 2,@Signature - type:拦截对象,method:拦截对象的方法名,args:拦截对象方法的参数
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisInterceptorUpdateSql implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// invocation.args() 表示方法的入参,比如这里的 args()[0] 就是 connection
// StatementHandler 获取 MappedStatement 比较麻烦, 如果是 Exectour 的一些方法就很方便了, 因为参数就带上了,直接 args()[0]
// 目标对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 目标对象可能是多层代理对象,拿到真正的目标对象,使用 mybatis 提供的一个工具类 MetaObject 获取
Object o = realTarget(statementHandler);
// 拿到真正的目标对象后再次通过 MetaObject 封装一下方便获取一些未提供 getter、setter 方法的属性
MetaObject metaObject = SystemMetaObject.forObject(o);
// 拿到 MappedStatement 对象
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 非查询不拦截
if (!mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) {
return invocation.proceed();
}
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
// 反射找到目标方法
Method targetM = null;
Class<?> clazz = Class.forName(className);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
String name = method.getName();
if (name.equals(methodName)){
targetM = method;
break;
}
}
if (targetM == null){
return invocation.proceed();
}
// 判断方法是否有注解
boolean needPermission = targetM.isAnnotationPresent(Permission.class);
if (needPermission) {
BoundSql boundSql = statementHandler.getBoundSql();
// 获取到原始 sql 语句
String sql = boundSql.getSql();
String showSql = showSql(mappedStatement.getConfiguration(), boundSql);
System.out.println("原始执行 sql ==> " + showSql);
// 新的 sql(模拟权限,增加实例id)
String newSql = sql + " and id > 0";
// 通过反射修改 sql语句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
String newShowSql = showSql(mappedStatement.getConfiguration(), boundSql);
System.out.println("修改后的 sql ==> " + newShowSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* 执行 sql
*
* @param configuration mybatis 配置对象
* @param boundSql boundSql
* @return sql
*/
private static String showSql(Configuration configuration, BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
}
}
}
}
return sql;
}
/**
* 参数
*
* @param obj 参数对象
* @return 参数
*/
private static String getParameterValue(Object obj) {
String value = null;
if (obj instanceof String) {
value = "'" + obj.toString() + "'";
value = value.replaceAll("\\\\", "\\\\\\\\");
value = value.replaceAll("\\$", "\\\\\\$");
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(obj) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "''";
}
}
return value;
}
/**
* 获取真实对象
*
* @param target 代理对象
* @return 真实对象
*/
public static Object realTarget(Object target) {
if (Proxy.isProxyClass(target.getClass())) {
MetaObject metaObject = SystemMetaObject.forObject(target);
if (metaObject.hasGetter("h.target")) {
Object value = metaObject.getValue("h.target");
return realTarget(value);
}
}
return target;
}
}
插件注册
springboot 注册方式
@Configuration
public class MybatisPlugin {
@Resource
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void addInterceptor() throws NoSuchFieldException, IllegalAccessException {
sqlSessionFactory.getConfiguration().addInterceptor(new MybatisInterceptorUpdateSql());
}
}
xml 注册方式
<plugins>
<plugin interceptor="com.xiaolyuh.mybatis.EditPlugin">
<property name="args1" value="参数示例"/>
</plugin>
</plugins>