引文
本文主要介绍如何使用mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录、在更新数据时准确记录更新字段
核心:mybatis插件(拦截器)、mybatis-Plus实体规范、数据对比
1、相关技术简介
mybatis插件:
mybatis插件实际上就是官方针对4层数据操作处理预留的拦截器,使用者可以根据不同的需求进行操作拦截并处理。这边笔者不做详细描述,详细介绍请到官网了解,这里笔者就复用官网介绍。
插件(plugins)
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为如果在试图修改或重写已有方法的行为的时候,你很可能在破坏 MyBatis 的核心模块。 这些都是更低层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
// ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象。
提示 覆盖配置类
除了用插件来修改 MyBatis 核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会严重影响 MyBatis 的行为,务请慎之又慎。
重点讲下4层处理,MyBatis两级缓存就是在其中两层中实现
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- 所有数据库操作到达底层后都由该执行器进行任务分发,主要有update(插入、更新、删除),query(查询),提交,回滚,关闭链接等
- ParameterHandler (getParameterObject, setParameters)
- 参数处理器(获取参数,设置参数)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- 结果集处理器(结果集,输出参数)
- StatementHandler (prepare, parameterize, batch, update, query)
- 声明处理器、准备链接jdbc前处理,prepare(预处理):生成sql语句,准备链接数据库进行操作
以上4层执行顺序为顺序执行
- Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
- ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
- ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。
- StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
MyBatis-Plus:
MyBatis增强器,主要规范了数据实体,在底层实现了简单的增删查改,使用者不再需要开发基础操作接口,小编认为是最强大、最方便易用的,没有之一,不接受任何反驳。详细介绍请看官网。
数据实体的规范让底层操作更加便捷,本例主要实体规范中的表名以及主键获取,下面上实体规范demo
@Data
@TableName("tb_demo")
@EqualsAndHashCode(callSuper = true)
public class Demo extends Model<Demo> {
private static final long serialVersionUID = 1L; /**
*
*/
@TableId
private Integer id;
/**
* 名称
*/
private String name; }
2、实现
一、实现拦截器
DataUpdateInterceptor,根据官网demo实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。
package com.erp4cloud.rerp.common.data.log; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.AllArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.scheduling.annotation.Async; import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties; /**
* 数据更新拦截器
*
* @author Tophua
* @date 2019/8/2
*/
@AllArgsConstructor
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class DataUpdateInterceptor implements Interceptor { private final DataSource dataSource;
private final DataLogHandler dataLogHandler; @Override
public Object intercept(Invocation invocation) {
Object result = null;
try {
this.dealData(invocation);
result = invocation.proceed();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
} @Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
} @Override
public void setProperties(Properties properties) { } /**
* 对数据库操作传入参数进行处理
*
* @param invocation
* @return void
* @author Tophua
* @date 2019/8/3
*/
public void dealData(Invocation invocation) {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
// 参数
Object et = args[1];
if (et instanceof Model) {
this.doLog(mappedStatement, et);
} else if (et instanceof Map) {
String key = "et";
String listKey = "collection";
if (((Map) et).containsKey(key) && ((Map) et).get(key) instanceof Model) {
this.doLog(mappedStatement, ((Map) et).get(key));
} else if (((Map) et).containsKey(listKey) && ((Map) et).get(listKey) instanceof Collection) {
List<Object> list = (List<Object>) ((Map) et).get(listKey);
for (Object obj : list) {
if (obj instanceof Model) {
this.doLog(mappedStatement, obj);
}
}
}
}
} /**
* 根据不同参数及操作进行不同的日志记录
*
* @param mappedStatement
* @param et
* @return void
* @author Tophua
* @date 2019/8/3
*/
public void doLog(MappedStatement mappedStatement, Object et) {
// 反射获取实体类
Class<?> clazz = et.getClass();
// 不含有表名的实体就默认通过
if (!clazz.isAnnotationPresent(TableName.class)) {
return;
}
// 获取表名
TableName tableName = clazz.getAnnotation(TableName.class);
String tbName = tableName.value();
if (StringUtils.isBlank(tbName)) {
return;
}
String pkName = null;
String pkValue = null;
// 获取实体所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 设置些属性是可以访问的
field.setAccessible(true);
if (field.isAnnotationPresent(TableId.class)) {
// 获取主键
pkName = field.getName();
try {
// 获取主键值
pkValue = field.get(et).toString();
} catch (Exception e) {
pkValue = null;
} }
}
BasicInfo basicInfo = new BasicInfo(dataSource, (Model) et, tbName, pkName, pkValue); // 插入
if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
InsertInfo insertInfo = new InsertInfo(basicInfo, et);
dataLogHandler.insertHandler(insertInfo);
}
// 更新
if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) {
Object oldObj = this.queryData(pkName, pkValue, (Model) et);
if (oldObj != null) {
UpdateInfo updateInfo = new UpdateInfo(basicInfo, oldObj, et);
// 调用自定义处理方法
dataLogHandler.updateHandler(updateInfo);
}
}
// 删除
if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) {
Object delObj = this.queryData(pkName, pkValue, (Model) et);
if (delObj != null) {
DeleteInfo deleteInfo = new DeleteInfo(basicInfo, delObj);
// 调用自定义处理方法
dataLogHandler.deleteHandler(deleteInfo);
}
}
} /**
* 根据主键和主键值查询数据
*
* @param pkName
* @param pkValue
* @param clazz
* @return java.lang.Object
* @author Tophua
* @date 2019/8/5
*/
private Object queryData(String pkName, String pkValue, Model clazz) {
// 查询更新前数据
return clazz.selectOne(Wrappers.query().eq(pkName, pkValue));
}
}
二、配置
package com.erp4cloud.rerp.common.data.log; import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /**
* 数据更新日志处理配置(实现按需加载)
*
* @author Tophua
* @date 2019/8/2
*/
@Configuration
@AllArgsConstructor
@ConditionalOnBean({DataSource.class, DataLogHandler.class})
public class DataLogConfig { private final DataLogHandler dataLogHandler;
private final DataSource dataSource; @Bean
@ConditionalOnMissingBean
public DataUpdateInterceptor dataUpdateInterceptor() {
return new DataUpdateInterceptor(dataSource, dataLogHandler);
}
}
提示:公共模块中需要在spring.factories(src/main/resources/META-INF/)中进行配置让Spring自动进行装配,小笔使用如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.erp4cloud.rerp.common.data.log.DataLogConfig
三、在其它模块实现自定义处理接口
在接口中会根据不同操作传入不同参数,各位可以根据具体方法取出数据进行所需操作。
本例仅测试使用,未实现具体操作,在具体使用中请自行编写具体逻辑。
DataLogHandler 自定义处理接口,各模块实现,注意需要将实现类作为ServiceBean使用,否则该功能无法生效。
package com.erp4cloud.rerp.common.data.log; /**
* 数据日志处理
*
* @author Tophua
* @date 2019/8/2
*/
public interface DataLogHandler { /**
* 插入处理
*
* @param insertInfo 插入数据信息
* @return void
* @author Tophua
* @date 2019/8/2
*/
void insertHandler(InsertInfo insertInfo); /**
* 更新处理
*
* @param updateInfo 更新数据信息
* @return void
* @author Tophua
* @date 2019/8/2
*/
void updateHandler(UpdateInfo updateInfo); /**
* 删除处理
*
* @param deleteInfo 删除数据信息
* @return void
* @author Tophua
* @date 2019/8/3
*/
void deleteHandler(DeleteInfo deleteInfo);
}
实现demo
package com.erp4cloud.rerp.building.log; import com.erp4cloud.rerp.common.data.log.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service; import java.util.List; /**
* describe
*
* @author Tophua
* @date 2019/8/3
*/
@Service
@AllArgsConstructor
public class DataLogDeal implements DataLogHandler { ObjectMapper objectMapper = new ObjectMapper(); @SneakyThrows
@Override
public void insertHandler(InsertInfo insertInfo) {
System.out.println("插入:" + objectMapper.writeValueAsString(insertInfo.getInsertObj()));
} @SneakyThrows
@Override
public void updateHandler(UpdateInfo updateInfo) {
List<CompareResult> cr = updateInfo.getCompareResult();
StringBuilder sb = new StringBuilder();
sb.append("更新\"");
sb.append(updateInfo.getBasicInfo().getTbName());
sb.append("\" 表 ");
cr.forEach(r -> {
String s = "把《" + r.getFieldComment() + "》从<" + r.getOldValue() + ">改成<" + r.getNewValue() + ">";
sb.append(s);
});
System.out.println(sb.toString());
} @SneakyThrows
@Override
public void deleteHandler(DeleteInfo deleteInfo) {
System.out.println("删除:" + objectMapper.writeValueAsString(deleteInfo.getDeleteObj()));
}
}
四、其它代码
BaseDataLogHandler 基础处理抽象类,提供底层数据对比方法。
package com.erp4cloud.rerp.common.data.log; import lombok.AllArgsConstructor;
import lombok.Getter; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; /**
* 数据日志基础信息及处理
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
@AllArgsConstructor
public abstract class BaseDataLogHandler { /**
* 数据基础信息
*/
private BasicInfo basicInfo; /**
* 对比两个对象
*
* @param oldObj 旧对象
* @param newObj 新对象
* @return java.util.List<com.erp4cloud.rerp.common.data.log.CompareResult>
* @author Tophua
* @date 2019/8/5
*/
protected List<CompareResult> compareTowObject(Object oldObj, Object newObj) throws IllegalAccessException {
List<CompareResult> list = new ArrayList<>();
//获取对象的class
Class<?> clazz1 = oldObj.getClass();
Class<?> clazz2 = newObj.getClass();
//获取对象的属性列表
Field[] field1 = clazz1.getDeclaredFields();
Field[] field2 = clazz2.getDeclaredFields();
//遍历属性列表field1
for (int i = 0; i < field1.length; i++) {
//遍历属性列表field2
for (int j = 0; j < field2.length; j++) {
//如果field1[i]属性名与field2[j]属性名内容相同
if (field1[i].getName().equals(field2[j].getName())) {
field1[i].setAccessible(true);
field2[j].setAccessible(true);
if (field2[j].get(newObj) == null) {
continue;
}
//如果field1[i]属性值与field2[j]属性值内容不相同
if (!compareTwo(field1[i].get(oldObj), field2[j].get(newObj))) {
CompareResult r = new CompareResult();
r.setFieldName(field1[i].getName());
r.setOldValue(field1[i].get(oldObj));
r.setNewValue(field2[j].get(newObj)); // 匹配字段注释
Optional o = this.basicInfo.getFieldInfos().stream()
.filter(f -> r.getFieldName().equals(f.getJFieldName())).findFirst();
if (o.isPresent()) {
r.setFieldComment(((FieldInfo) o.get()).getComment());
}
list.add(r);
}
break;
}
}
}
return list;
} /**
* 对比两个数据是否内容相同
*
* @param object1,object2
* @return boolean类型
*/
private boolean compareTwo(Object object1, Object object2) { if (object1 == null && object2 == null) {
return true;
}
if (object1 == null && object2 != null) {
return false;
}
if (object1.equals(object2)) {
return true;
}
return false;
} }
BasicInfo 基础信息,数据源,本表字段信息等。
package com.erp4cloud.rerp.common.data.log; import cn.hutool.db.Db;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Getter;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils; import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; /**
* 基础信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class BasicInfo {
private static ConcurrentHashMap<String, List<FieldInfo>> fields = new ConcurrentHashMap<>(); /**
* 数据源
*/
private DataSource dataSource;
/**
* mybatis数据底层
*/
private Model model;
/**
* 表名
*/
private String tbName;
/**
* 主键名称
*/
private String pkName;
/**
* 主键值
*/
private String pkValue; /**
* 表字段注释
*/
private List<FieldInfo> fieldInfos; public BasicInfo(DataSource dataSource, Model model, String tbName, String pkName, String pkValue) {
this.dataSource = dataSource;
this.model = model;
this.tbName = tbName;
this.pkName = pkName;
this.pkValue = pkValue;
} public List<FieldInfo> getFieldInfos() {
if (!fields.containsKey(this.tbName)) {
String query = "select column_name fieldName, column_comment comment from information_schema.columns" +
" where table_name = \"" + this.tbName + "\" and table_schema = (select database())";
try {
this.fieldInfos = Db.use(dataSource).query(query, FieldInfo.class);
} catch (SQLException e) {
this.fieldInfos = new ArrayList<>();
}
this.fieldInfos.forEach(f -> {
String caseName = this.columnToJava(f.getFieldName());
f.setJFieldName(StringUtils.uncapitalize(caseName));
});
fields.put(this.tbName, this.fieldInfos);
}
return fields.get(this.tbName);
} /**
* 列名转换成Java属性名
*/
private String columnToJava(String columnName) {
return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
}
}
FieldInfo 字段信息
package com.erp4cloud.rerp.common.data.log; import lombok.Data; /**
* 字段信息
*
* @author Tophua
* @date 2019/8/5
*/
@Data
public class FieldInfo { /**
* 字段名
*/
private String fieldName;
/**
* java字段名
*/
private String jFieldName;
/**
* 注释
*/
private String comment;
}
CompareResult 字段对比结果
package com.erp4cloud.rerp.common.data.log; import lombok.Data; /**
* 对比两个对象结果
*
* @author Tophua
* @date 2019/8/5
*/
@Data
public class CompareResult { /**
* 字段名
*/
private String fieldName;
/**
* 字段注释
*/
private String fieldComment;
/**
* 字段旧值
*/
private Object oldValue;
/**
* 字段新值
*/
private Object newValue;
}
InsertInfo 插入信息
package com.erp4cloud.rerp.common.data.log; import lombok.Getter; /**
* 数据插入信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class InsertInfo extends BaseDataLogHandler { /**
* 插入对象
*/
private Object insertObj; public InsertInfo(BasicInfo basicInfo, Object insertObj) {
super(basicInfo);
this.insertObj = insertObj;
} }
UpdateInfo 更新信息
package com.erp4cloud.rerp.common.data.log; import lombok.Getter; import java.util.List; /**
* 数据更新信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class UpdateInfo extends BaseDataLogHandler { /**
* 更新前对象
*/
private Object oldObj;
/**
* 更新对象
*/
private Object newObj; public UpdateInfo(BasicInfo basicInfo, Object oldObj, Object newObj) {
super(basicInfo);
this.oldObj = oldObj;
this.newObj = newObj;
} public List<CompareResult> getCompareResult() throws IllegalAccessException {
return compareTowObject(this.oldObj, this.newObj);
}
}
DeleteInfo 删除信息
package com.erp4cloud.rerp.common.data.log; import lombok.Getter; /**
* 数据删除信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class DeleteInfo extends BaseDataLogHandler { /**
* 删除对象
*/
private Object deleteObj; public DeleteInfo(BasicInfo basicInfo, Object deleteObj) {
super(basicInfo);
this.deleteObj = deleteObj;
}
}
四、结果
更新时控制台打印:
更新"customer_resource_base_info" 表 把《姓名》从<测试>改成<测试dsffgggg>把《身份证》从<2222222>改成<3333333333333>
由于是测试所以未进行数据库保存,大家自行保存。
3、总结
本例主要解决多实体数据更新前后对比记录,当然也可使用AOP实现数据对比,但经笔者实现感觉还是此方法实现起来相对简单。笔者更推荐使用底层技术直接进行拦截处理,这样能保证任何数据操作都毫无遗漏,不放过任何操作。
目前本例暂未实现数据无主键更新记录,但业务中经常会出现无主键根据其它条件更新,所以本例还可进行优化提升,在此笔者就先放一段了,等后续再进行升级更新。
欢迎各位大神交流意见。。。。。。