引文html
本文主要介紹如何使用mybatis插件實現攔截數據庫操做並根據不一樣需求進行數據對比分析,主要適用於系統中須要對數據操做進行記錄、在更新數據時準確記錄更新字段java
核心:mybatis插件(攔截器)、mybatis-Plus實體規範、數據對比spring
mybatis插件:sql
mybatis插件實際上就是官方針對4層數據操做處理預留的攔截器,使用者能夠根據不一樣的需求進行操做攔截並處理。這邊筆者不作詳細描述,詳細介紹請到官網瞭解,這裏筆者就複用官網介紹。數據庫
MyBatis 容許你在已映射語句執行過程當中的某一點進行攔截調用。默認狀況下,MyBatis 容許使用插件來攔截的方法調用包括:apache
這些類中方法的細節能夠經過查看每一個方法的簽名來發現,或者直接查看 MyBatis 發行包中的源代碼。 若是你想作的不只僅是監控方法的調用,那麼你最好至關了解要重寫的方法的行爲。 由於若是在試圖修改或重寫已有方法的行爲的時候,你極可能在破壞 MyBatis 的核心模塊。 這些都是更低層的類和方法,因此使用插件的時候要特別小心。api
經過 MyBatis 提供的強大機制,使用插件是很是簡單的,只需實現 Interceptor 接口,並指定想要攔截的方法簽名便可。緩存
1 // ExamplePlugin.java 2 @Intercepts({@Signature( 3 type= Executor.class, 4 method = "update", 5 args = {MappedStatement.class,Object.class})}) 6 public class ExamplePlugin implements Interceptor { 7 private Properties properties = new Properties(); 8 public Object intercept(Invocation invocation) throws Throwable { 9 // implement pre processing if need 10 Object returnObject = invocation.proceed(); 11 // implement post processing if need 12 return returnObject; 13 } 14 public void setProperties(Properties properties) { 15 this.properties = properties; 16 } 17 } 18 <!-- mybatis-config.xml --> 19 <plugins> 20 <plugin interceptor="org.mybatis.example.ExamplePlugin"> 21 <property name="someProperty" value="100"/> 22 </plugin> 23 </plugins>
上面的插件將會攔截在 Executor 實例中全部的 「update」 方法調用, 這裏的 Executor 是負責執行低層映射語句的內部對象。mybatis
提示 覆蓋配置類app
除了用插件來修改 MyBatis 核心行爲以外,還能夠經過徹底覆蓋配置類來達到目的。只需繼承後覆蓋其中的每一個方法,再把它傳遞到 SqlSessionFactoryBuilder.build(myConfig) 方法便可。再次重申,這可能會嚴重影響 MyBatis 的行爲,務請慎之又慎。
重點講下4層處理,MyBatis兩級緩存就是在其中兩層中實現
以上4層執行順序爲順序執行
MyBatis-Plus:
MyBatis加強器,主要規範了數據實體,在底層實現了簡單的增刪查改,使用者再也不須要開發基礎操做接口,小編認爲是最強大、最方便易用的,沒有之一,不接受任何反駁。詳細介紹請看官網。
數據實體的規範讓底層操做更加便捷,本例主要實體規範中的表名以及主鍵獲取,下面上實體規範demo
1 @Data 2 @TableName("tb_demo") 3 @EqualsAndHashCode(callSuper = true) 4 public class Demo extends Model<Demo> { 5 private static final long serialVersionUID = 1L; 6 7 /** 8 * 9 */ 10 @TableId 11 private Integer id; 12 /** 13 * 名稱 14 */ 15 private String name; 16 17 }
DataUpdateInterceptor,根據官網demo實現攔截器,在攔截器中根據增、刪、改操做去調用各個模塊中自定義實現的處理方法來達到不一樣的操做處理。
1 package com.erp4cloud.rerp.common.data.log; 2 3 import com.baomidou.mybatisplus.annotation.TableId; 4 import com.baomidou.mybatisplus.annotation.TableName; 5 import com.baomidou.mybatisplus.core.toolkit.Wrappers; 6 import com.baomidou.mybatisplus.extension.activerecord.Model; 7 import lombok.AllArgsConstructor; 8 import org.apache.commons.lang.StringUtils; 9 import org.apache.ibatis.executor.Executor; 10 import org.apache.ibatis.mapping.MappedStatement; 11 import org.apache.ibatis.mapping.SqlCommandType; 12 import org.apache.ibatis.plugin.*; 13 import org.springframework.scheduling.annotation.Async; 14 15 import javax.sql.DataSource; 16 import java.lang.reflect.Field; 17 import java.lang.reflect.InvocationTargetException; 18 import java.util.Collection; 19 import java.util.List; 20 import java.util.Map; 21 import java.util.Properties; 22 23 /** 24 * 數據更新攔截器 25 * 26 * @author Tophua 27 * @date 2019/8/2 28 */ 29 @AllArgsConstructor 30 @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) 31 public class DataUpdateInterceptor implements Interceptor { 32 33 private final DataSource dataSource; 34 private final DataLogHandler dataLogHandler; 35 36 @Override 37 public Object intercept(Invocation invocation) { 38 Object result = null; 39 try { 40 this.dealData(invocation); 41 result = invocation.proceed(); 42 } catch (InvocationTargetException e) { 43 e.printStackTrace(); 44 } catch (IllegalAccessException e) { 45 e.printStackTrace(); 46 } 47 return result; 48 } 49 50 @Override 51 public Object plugin(Object target) { 52 if (target instanceof Executor) { 53 return Plugin.wrap(target, this); 54 } 55 return target; 56 } 57 58 @Override 59 public void setProperties(Properties properties) { 60 61 } 62 63 /** 64 * 對數據庫操做傳入參數進行處理 65 * 66 * @param invocation 67 * @return void 68 * @author Tophua 69 * @date 2019/8/3 70 */ 71 public void dealData(Invocation invocation) { 72 Object[] args = invocation.getArgs(); 73 MappedStatement mappedStatement = (MappedStatement) args[0]; 74 // 參數 75 Object et = args[1]; 76 if (et instanceof Model) { 77 this.doLog(mappedStatement, et); 78 } else if (et instanceof Map) { 79 String key = "et"; 80 String listKey = "collection"; 81 if (((Map) et).containsKey(key) && ((Map) et).get(key) instanceof Model) { 82 this.doLog(mappedStatement, ((Map) et).get(key)); 83 } else if (((Map) et).containsKey(listKey) && ((Map) et).get(listKey) instanceof Collection) { 84 List<Object> list = (List<Object>) ((Map) et).get(listKey); 85 for (Object obj : list) { 86 if (obj instanceof Model) { 87 this.doLog(mappedStatement, obj); 88 } 89 } 90 } 91 } 92 } 93 94 /** 95 * 根據不一樣參數及操做進行不一樣的日誌記錄 96 * 97 * @param mappedStatement 98 * @param et 99 * @return void 100 * @author Tophua 101 * @date 2019/8/3 102 */ 103 public void doLog(MappedStatement mappedStatement, Object et) { 104 // 反射獲取實體類 105 Class<?> clazz = et.getClass(); 106 // 不含有表名的實體就默認經過 107 if (!clazz.isAnnotationPresent(TableName.class)) { 108 return; 109 } 110 // 獲取表名 111 TableName tableName = clazz.getAnnotation(TableName.class); 112 String tbName = tableName.value(); 113 if (StringUtils.isBlank(tbName)) { 114 return; 115 } 116 String pkName = null; 117 String pkValue = null; 118 // 獲取實體全部字段 119 Field[] fields = clazz.getDeclaredFields(); 120 for (Field field : fields) { 121 // 設置些屬性是能夠訪問的 122 field.setAccessible(true); 123 if (field.isAnnotationPresent(TableId.class)) { 124 // 獲取主鍵 125 pkName = field.getName(); 126 try { 127 // 獲取主鍵值 128 pkValue = field.get(et).toString(); 129 } catch (Exception e) { 130 pkValue = null; 131 } 132 133 } 134 } 135 BasicInfo basicInfo = new BasicInfo(dataSource, (Model) et, tbName, pkName, pkValue); 136 137 // 插入 138 if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) { 139 InsertInfo insertInfo = new InsertInfo(basicInfo, et); 140 dataLogHandler.insertHandler(insertInfo); 141 } 142 // 更新 143 if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) { 144 Object oldObj = this.queryData(pkName, pkValue, (Model) et); 145 if (oldObj != null) { 146 UpdateInfo updateInfo = new UpdateInfo(basicInfo, oldObj, et); 147 // 調用自定義處理方法 148 dataLogHandler.updateHandler(updateInfo); 149 } 150 } 151 // 刪除 152 if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) { 153 Object delObj = this.queryData(pkName, pkValue, (Model) et); 154 if (delObj != null) { 155 DeleteInfo deleteInfo = new DeleteInfo(basicInfo, delObj); 156 // 調用自定義處理方法 157 dataLogHandler.deleteHandler(deleteInfo); 158 } 159 } 160 } 161 162 /** 163 * 根據主鍵和主鍵值查詢數據 164 * 165 * @param pkName 166 * @param pkValue 167 * @param clazz 168 * @return java.lang.Object 169 * @author Tophua 170 * @date 2019/8/5 171 */ 172 private Object queryData(String pkName, String pkValue, Model clazz) { 173 // 查詢更新前數據 174 return clazz.selectOne(Wrappers.query().eq(pkName, pkValue)); 175 } 176 }
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.AllArgsConstructor; 4 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 5 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 6 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Configuration; 8 9 import javax.sql.DataSource; 10 11 /** 12 * 數據更新日誌處理配置(實現按需加載) 13 * 14 * @author Tophua 15 * @date 2019/8/2 16 */ 17 @Configuration 18 @AllArgsConstructor 19 @ConditionalOnBean({DataSource.class, DataLogHandler.class}) 20 public class DataLogConfig { 21 22 private final DataLogHandler dataLogHandler; 23 private final DataSource dataSource; 24 25 @Bean 26 @ConditionalOnMissingBean 27 public DataUpdateInterceptor dataUpdateInterceptor() { 28 return new DataUpdateInterceptor(dataSource, dataLogHandler); 29 } 30 }
提示:公共模塊中須要在spring.factories(src/main/resources/META-INF/)中進行配置讓Spring自動進行裝配,小筆使用以下
1 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 com.erp4cloud.rerp.common.data.log.DataLogConfig
在接口中會根據不一樣操做傳入不一樣參數,各位能夠根據具體方法取出數據進行所需操做。
本例僅測試使用,未實現具體操做,在具體使用中請自行編寫具體邏輯。
DataLogHandler 自定義處理接口,各模塊實現,注意須要將實現類做爲ServiceBean使用,不然該功能沒法生效。
1 package com.erp4cloud.rerp.common.data.log; 2 3 /** 4 * 數據日誌處理 5 * 6 * @author Tophua 7 * @date 2019/8/2 8 */ 9 public interface DataLogHandler { 10 11 /** 12 * 插入處理 13 * 14 * @param insertInfo 插入數據信息 15 * @return void 16 * @author Tophua 17 * @date 2019/8/2 18 */ 19 void insertHandler(InsertInfo insertInfo); 20 21 /** 22 * 更新處理 23 * 24 * @param updateInfo 更新數據信息 25 * @return void 26 * @author Tophua 27 * @date 2019/8/2 28 */ 29 void updateHandler(UpdateInfo updateInfo); 30 31 /** 32 * 刪除處理 33 * 34 * @param deleteInfo 刪除數據信息 35 * @return void 36 * @author Tophua 37 * @date 2019/8/3 38 */ 39 void deleteHandler(DeleteInfo deleteInfo); 40 }
實現demo
1 package com.erp4cloud.rerp.building.log; 2 3 import com.erp4cloud.rerp.common.data.log.*; 4 import com.fasterxml.jackson.databind.ObjectMapper; 5 import lombok.AllArgsConstructor; 6 import lombok.SneakyThrows; 7 import org.springframework.stereotype.Service; 8 9 import java.util.List; 10 11 /** 12 * describe 13 * 14 * @author Tophua 15 * @date 2019/8/3 16 */ 17 @Service 18 @AllArgsConstructor 19 public class DataLogDeal implements DataLogHandler { 20 21 ObjectMapper objectMapper = new ObjectMapper(); 22 23 @SneakyThrows 24 @Override 25 public void insertHandler(InsertInfo insertInfo) { 26 System.out.println("插入:" + objectMapper.writeValueAsString(insertInfo.getInsertObj())); 27 } 28 29 @SneakyThrows 30 @Override 31 public void updateHandler(UpdateInfo updateInfo) { 32 List<CompareResult> cr = updateInfo.getCompareResult(); 33 StringBuilder sb = new StringBuilder(); 34 sb.append("更新\""); 35 sb.append(updateInfo.getBasicInfo().getTbName()); 36 sb.append("\" 表 "); 37 cr.forEach(r -> { 38 String s = "把《" + r.getFieldComment() + "》從<" + r.getOldValue() + ">改爲<" + r.getNewValue() + ">"; 39 sb.append(s); 40 }); 41 System.out.println(sb.toString()); 42 } 43 44 @SneakyThrows 45 @Override 46 public void deleteHandler(DeleteInfo deleteInfo) { 47 System.out.println("刪除:" + objectMapper.writeValueAsString(deleteInfo.getDeleteObj())); 48 } 49 }
BaseDataLogHandler 基礎處理抽象類,提供底層數據對比方法。
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.AllArgsConstructor; 4 import lombok.Getter; 5 6 import java.lang.reflect.Field; 7 import java.util.ArrayList; 8 import java.util.List; 9 import java.util.Optional; 10 11 /** 12 * 數據日誌基礎信息及處理 13 * 14 * @author Tophua 15 * @date 2019/8/5 16 */ 17 @Getter 18 @AllArgsConstructor 19 public abstract class BaseDataLogHandler { 20 21 /** 22 * 數據基礎信息 23 */ 24 private BasicInfo basicInfo; 25 26 /** 27 * 對比兩個對象 28 * 29 * @param oldObj 舊對象 30 * @param newObj 新對象 31 * @return java.util.List<com.erp4cloud.rerp.common.data.log.CompareResult> 32 * @author Tophua 33 * @date 2019/8/5 34 */ 35 protected List<CompareResult> compareTowObject(Object oldObj, Object newObj) throws IllegalAccessException { 36 List<CompareResult> list = new ArrayList<>(); 37 //獲取對象的class 38 Class<?> clazz1 = oldObj.getClass(); 39 Class<?> clazz2 = newObj.getClass(); 40 //獲取對象的屬性列表 41 Field[] field1 = clazz1.getDeclaredFields(); 42 Field[] field2 = clazz2.getDeclaredFields(); 43 //遍歷屬性列表field1 44 for (int i = 0; i < field1.length; i++) { 45 //遍歷屬性列表field2 46 for (int j = 0; j < field2.length; j++) { 47 //若是field1[i]屬性名與field2[j]屬性名內容相同 48 if (field1[i].getName().equals(field2[j].getName())) { 49 field1[i].setAccessible(true); 50 field2[j].setAccessible(true); 51 if (field2[j].get(newObj) == null) { 52 continue; 53 } 54 //若是field1[i]屬性值與field2[j]屬性值內容不相同 55 if (!compareTwo(field1[i].get(oldObj), field2[j].get(newObj))) { 56 CompareResult r = new CompareResult(); 57 r.setFieldName(field1[i].getName()); 58 r.setOldValue(field1[i].get(oldObj)); 59 r.setNewValue(field2[j].get(newObj)); 60 61 // 匹配字段註釋 62 Optional o = this.basicInfo.getFieldInfos().stream() 63 .filter(f -> r.getFieldName().equals(f.getJFieldName())).findFirst(); 64 if (o.isPresent()) { 65 r.setFieldComment(((FieldInfo) o.get()).getComment()); 66 } 67 list.add(r); 68 } 69 break; 70 } 71 } 72 } 73 return list; 74 } 75 76 /** 77 * 對比兩個數據是否內容相同 78 * 79 * @param object1,object2 80 * @return boolean類型 81 */ 82 private boolean compareTwo(Object object1, Object object2) { 83 84 if (object1 == null && object2 == null) { 85 return true; 86 } 87 if (object1 == null && object2 != null) { 88 return false; 89 } 90 if (object1.equals(object2)) { 91 return true; 92 } 93 return false; 94 } 95 96 }
BasicInfo 基礎信息,數據源,本表字段信息等。
1 package com.erp4cloud.rerp.common.data.log; 2 3 import cn.hutool.db.Db; 4 import com.baomidou.mybatisplus.extension.activerecord.Model; 5 import lombok.Getter; 6 import org.apache.commons.lang.StringUtils; 7 import org.apache.commons.lang.WordUtils; 8 9 import javax.sql.DataSource; 10 import java.sql.SQLException; 11 import java.util.ArrayList; 12 import java.util.List; 13 import java.util.concurrent.ConcurrentHashMap; 14 15 /** 16 * 基礎信息 17 * 18 * @author Tophua 19 * @date 2019/8/5 20 */ 21 @Getter 22 public class BasicInfo { 23 private static ConcurrentHashMap<String, List<FieldInfo>> fields = new ConcurrentHashMap<>(); 24 25 /** 26 * 數據源 27 */ 28 private DataSource dataSource; 29 /** 30 * mybatis數據底層 31 */ 32 private Model model; 33 /** 34 * 表名 35 */ 36 private String tbName; 37 /** 38 * 主鍵名稱 39 */ 40 private String pkName; 41 /** 42 * 主鍵值 43 */ 44 private String pkValue; 45 46 /** 47 * 表字段註釋 48 */ 49 private List<FieldInfo> fieldInfos; 50 51 public BasicInfo(DataSource dataSource, Model model, String tbName, String pkName, String pkValue) { 52 this.dataSource = dataSource; 53 this.model = model; 54 this.tbName = tbName; 55 this.pkName = pkName; 56 this.pkValue = pkValue; 57 } 58 59 public List<FieldInfo> getFieldInfos() { 60 if (!fields.containsKey(this.tbName)) { 61 String query = "select column_name fieldName, column_comment comment from information_schema.columns" + 62 " where table_name = \"" + this.tbName + "\" and table_schema = (select database())"; 63 try { 64 this.fieldInfos = Db.use(dataSource).query(query, FieldInfo.class); 65 } catch (SQLException e) { 66 this.fieldInfos = new ArrayList<>(); 67 } 68 this.fieldInfos.forEach(f -> { 69 String caseName = this.columnToJava(f.getFieldName()); 70 f.setJFieldName(StringUtils.uncapitalize(caseName)); 71 }); 72 fields.put(this.tbName, this.fieldInfos); 73 } 74 return fields.get(this.tbName); 75 } 76 77 /** 78 * 列名轉換成Java屬性名 79 */ 80 private String columnToJava(String columnName) { 81 return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", ""); 82 } 83 }
FieldInfo 字段信息
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.Data; 4 5 /** 6 * 字段信息 7 * 8 * @author Tophua 9 * @date 2019/8/5 10 */ 11 @Data 12 public class FieldInfo { 13 14 /** 15 * 字段名 16 */ 17 private String fieldName; 18 /** 19 * java字段名 20 */ 21 private String jFieldName; 22 /** 23 * 註釋 24 */ 25 private String comment; 26 }
CompareResult 字段對比結果
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.Data; 4 5 /** 6 * 對比兩個對象結果 7 * 8 * @author Tophua 9 * @date 2019/8/5 10 */ 11 @Data 12 public class CompareResult { 13 14 /** 15 * 字段名 16 */ 17 private String fieldName; 18 /** 19 * 字段註釋 20 */ 21 private String fieldComment; 22 /** 23 * 字段舊值 24 */ 25 private Object oldValue; 26 /** 27 * 字段新值 28 */ 29 private Object newValue; 30 }
InsertInfo 插入信息
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.Getter; 4 5 /** 6 * 數據插入信息 7 * 8 * @author Tophua 9 * @date 2019/8/5 10 */ 11 @Getter 12 public class InsertInfo extends BaseDataLogHandler { 13 14 /** 15 * 插入對象 16 */ 17 private Object insertObj; 18 19 public InsertInfo(BasicInfo basicInfo, Object insertObj) { 20 super(basicInfo); 21 this.insertObj = insertObj; 22 } 23 24 }
UpdateInfo 更新信息
1 package com.erp4cloud.rerp.common.data.log; 2 3 import lombok.Getter; 4 5 import java.util.List; 6 7 /** 8 * 數據更新信息 9 * 10 * @author Tophua 11 * @date 2019/8/5 12 */ 13 @Getter 14 public class UpdateInfo extends BaseDataLogHandler { 15 16 /** 17 * 更新前對象 18 */ 19 private Object oldObj; 20 /** 21 * 更新對象 22 */ 23 private Object newObj; 24 25 public UpdateInfo(BasicInfo basicInfo, Object oldObj, Object newObj) { 26 super(basicInfo); 27 this.oldObj = oldObj; 28 this.newObj = newObj; 29 } 30 31 public List<CompareResult> getCompareResult() throws IllegalAccessException { 32 return compareTowObject(this.oldObj, this.newObj); 33 } 34 }
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; } }
更新時控制檯打印:
1 更新"customer_resource_base_info" 表 把《姓名》從<測試>改爲<測試dsffgggg>把《身份證》從<2222222>改爲<3333333333333>
因爲是測試因此未進行數據庫保存,你們自行保存。
本例主要解決多實體數據更新先後對比記錄,固然也可以使用AOP實現數據對比,但經筆者實現感受仍是此方法實現起來相對簡單。筆者更推薦使用底層技術直接進行攔截處理,這樣能保證任何數據操做都毫無遺漏,不放過任何操做。
目前本例暫未實現數據無主鍵更新記錄,但業務中常常會出現無主鍵根據其它條件更新,因此本例還可進行優化提高,在此筆者就先放一段了,等後續再進行升級更新。
歡迎各位大神交流意見。。。。。。