关于Mybatis拦截器的使用
上一篇记录了Mybatis拦截器的说明,对于其原理功能,有了初步的了解,本次记录一下Mybatis在日常中的应用场景
1 Mybatis拦截器的使用
上一篇给到了Mybatis官方对于拦截器的使用方法, 而在日常项目中使用拦截器,只需要分成两步.
- 1 自定义拦截器
- 2 注册拦截器
1 自定义拦截器
根据上一篇说明可知,自定义拦截器需要实现org.apache.ibatis.plugin.Interceptor接口, 并在接口上添加@Intercepts注解.
1 Interceptor接口
public interface Interceptor {
/**
* 拦截要执行的方法, 在这个方法中自定义的逻辑
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理
* 1 拦截 当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法 -- Plugin.wrap(target, this)
* 2 不拦截 当返回的是当前对象的时候 就不会调用intercept方法
*/
Object plugin(Object target);
/**
* 用于指定属性,注册当前拦截器的时候可以设置一些属性
*/
void setProperties(Properties properties);
}
2 @Intercepts注解
@Intercepts注解是通过一个@Signature注解(拦截点),来指定拦截那个对象里面的某个方法
Intercepts注解的参数列表是Signature注解数组
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
// Signature注解数组
Signature[] value();
}
3 @Signature注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
/**
* 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
*/
Class<?> type();
/**
* 在定义拦截类的基础之上,在定义拦截的方法
*/
String method();
/**
* 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
* 因方法里面可能重载,不指定参数列表,不能确定是对应拦截的方法
*/
Class<?>[] args();
}
2 注册拦截器
注册拦截器,就是一个普通的Bean对象注册并交由Spring管理.
/**
* mybatis拦截器配置
*/
@Configuration
public class MybatisConfiguration {
/**
* 注册拦截器
*/
@Bean
public MybatisInterceptor mybatisInterceptor() {
MybatisInterceptor mybatisInterceptor = new MybatisInterceptor();
Properties properties = new Properties();
// 可以调用properties.setProperty方法来给拦截器设置一些自定义参数
mybatisInterceptor.setProperties(properties);
return mybatisInterceptor;
}
}
3 拦截器使用案列
1 日志打印
在项目基本增删改查功能完成的基础上, 需要添加日志打印或日志记录入库的需求,此时可使用Mybatis拦截器.
自定义拦截器
/**
* 记录打印日志
*/
@Slf4j
@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}
),
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class Loginterceptor implements Interceptor {
// 是否开启记录日志
private Boolean enable;
public Loginterceptor(Boolean enable) {
this.enable = enable;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object param = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(param);
long logStartTime = System.currentTimeMillis();
if (enable == null || !enable) {
return invocation.proceed();
}
// 执行sql语句
Object result = invocation.proceed();
long logEndTime = System.currentTimeMillis();
// 组装sql语句
String sql = formatSql(boundSql, mappedStatement.getConfiguration()).concat(";");
// 构造日志对象
LogStat sqlStat = LogStat.builder()
.id(mappedStatement.getId())
.sqlCostTime(String.valueOf(logEndTime - logStartTime).concat(" ms"))
.sql(sql)
.build();
// 打印拦截sql日志, 也可以根据不同的要求保存到数据
log.info("Mybatis拦截器执行了, 拦截的数据为: {}",JSON.toJSONString(sqlStat));
return result;
}
/**
* 拼接sql
*/
private String formatSql(BoundSql boundSql, Configuration configuration) {
String sql = boundSql.getSql();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
Object parameterObject = boundSql.getParameterObject();
if (StringUtils.isBlank(sql)) {
return "";
}
if (configuration == null) {
return "";
}
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
sql = beautifySql(sql);
if (parameterMappings != null) {
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
String paramValueStr = "";
if (value instanceof String) {
paramValueStr = "'" + value + "'";
} else if (value instanceof Date) {
paramValueStr = "'" + this.dateToStrTimeMill((Date) value) + "'";
} else {
paramValueStr = value + "";
}
sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValueStr));
}
}
}
return sql;
}
private static String formatStr = "yyyy-MM-dd HH:mm:ss:SSS";
/**
* 时间转换
*/
private String dateToStrTimeMill(Date currentDate) {
if (currentDate == null || formatStr == null) {
return null;
} else {
SimpleDateFormat sdf = new SimpleDateFormat(formatStr);
return sdf.format(currentDate);
}
}
/**
* 格式化sql
*/
private String beautifySql(String sql) {
sql = sql.replaceAll("[\\s\n ]+", " ");
return sql;
}
/**
* 拦截对象属于Executor实例
*/
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
/**
* 可设置属性
*/
@Override
public void setProperties(Properties properties) {
}
/**
* 定义日志记录内部类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
static class LogStat {
// sql语句
private String sql;
// 执行耗时
private String sqlCostTime;
// 全限定名
private String id;
}
}
注册拦截器
@Configuration
public class SqlConfig {
@Autowired
private SqlSessionFactory sqlSessionFactory;
// 是否开启日志记录 默认开启
@Value("${sqlPrint:true}")
private String enable;
@PostConstruct
public void init(){
sqlSessionFactory.getConfiguration().addInterceptor(new Loginterceptor(Boolean.parseBoolean(enable)));
}
}
2 数据隔离
在某些多租户数据隔离项目中, 一些数据需要根据不同的租户,展示对应的数据,在一些查询中,为了安全和方便,可以将这一部分功能放到Mybatis拦截中执行.
自定义拦截器
/**
* 多租户查询拦截
*/
@Intercepts({
@Signature(method = "prepare",
type = StatementHandler.class,
args = {Connection.class, Integer.class}
),
@Signature(method = "query",
type = Executor.class,
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class}
)
})
public class TenantInterceptor implements Interceptor {
// 跳过拦截的mapper
private final Set<String> ignore = new HashSet<>();
public TenantInterceptor( String... ignore) {
if (ignore != null && ignore.length > 0) {
this.ignore.addAll(Arrays.asList(ignore));
}
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 由于mappedStatement中有我们需要的方法id,但却是protected的,所以要通过反射获取
MetaObject statementHandler = SystemMetaObject.forObject(handler);
MappedStatement mappedStatement = (MappedStatement) statementHandler
.getValue("delegate.mappedStatement");
// 不是查询类型,直接放过
if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
return invocation.proceed();
}
String namespace = mappedStatement.getId();
// 忽视拦截对象,直接放过
if (ignore.contains(namespace)) {
return invocation.proceed();
}
String className = namespace.substring(0, namespace.lastIndexOf("."));
String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
Class<?> clazz = Class.forName(className);
Tenant tenant = null;
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (methodName.equals(method.getName())) {
tenant = method.getAnnotation(Tenant.class);
break;
}
}
// 方法没有租户注解,直接放过
if (tenant == null) {
return invocation.proceed();
}
BoundSql boundSql = handler.getBoundSql();
// 获取sql
String sql = boundSql.getSql();
StringBuilder whereSql = new StringBuilder();
CCJSqlParserManager parserManager = new CCJSqlParserManager();
Select select = (Select) parserManager.parse(new StringReader(sql));
PlainSelect plain = (PlainSelect) select.getSelectBody();
// 获取当前查询条件
Expression where = plain.getWhere();
// 租户数据隔离
// 从用户上下文域中获取用户id和租户id等信息
if (tenant != null) {
whereSql.append("( 1 = 0 OR ");
whereSql.append(addAlias(plain, tenant.userField())).append(" = '")
.append(BaseContextHolder.getUserId()).append("'");
whereSql.append(" AND ");
whereSql.append(addAlias(plain, tenant.tenantField())).append(" = '")
.append(BaseContextHolder.getTenantId()).append("' )");
}
if (where == null) {
if (tenant != null) {
whereSql.append(")");
}
if (whereSql.length() > 0) {
Expression whereExpression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
plain.setWhere(whereExpression);
}
} else {
if (whereSql.length() > 0) {
whereSql.append(" and ( ").append(where.toString()).append(" )");
} else {
whereSql.append(where.toString());
}
if (tenant != null) {
whereSql.append(")");
}
Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
plain.setWhere(expression);
}
statementHandler.setValue("delegate.boundSql.sql", select.toString());
return invocation.proceed();
}
/**
* 拦截StatementHandler实例对象
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
/**
* 添加别名
* @param plain
* @param field
* @return
*/
private String addAlias(PlainSelect plain, String field) {
if (plain.getFromItem().getAlias() != null) {
return plain.getFromItem().getAlias() + "." + field;
} else {
return field;
}
}
}
额外使用到租户注解来进行开关控制. 租户注解用在需要添加租户隔离的查询方法上或类上.
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface Tenant {
/**
* 租户属性
*/
String tenantField() default "tenant_id";
/**
* 用户信息
*/
String userField() default "crt_user";
}
注册拦截器
@Configuration
public class TenantConfig {
/**
* 注册拦截器
*/
@Bean
public TenantInterceptor tenantInterceptor() {
TenantInterceptor tenantInterceptor = new TenantInterceptor();
return tenantInterceptor;
}
}
3 结果翻译替换
@Intercepts({
@Signature(method = "handleResultSets",
type = ResultSetHandler.class,
args = {Statement.class}
)
})
public class TranslateInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable{
Object result = invocation.proceed();
if(ObjectUtils.isEmpty(result)){
return result;
}
// 翻译数据
return translate(reuslt);
}
// 模拟翻译接口
public Object translate(Object object){
// xxx
}
}