基于MyBatis攔截器實(shí)現(xiàn)數(shù)據(jù)變更記錄的技術(shù)方案
一、背景與需求
在日常應(yīng)用開發(fā)中,數(shù)據(jù)變更記錄是一個(gè)常見需求。我們需要記錄業(yè)務(wù)數(shù)據(jù)的每一次變更,包括:
- 字段級(jí)別的變更記錄
- 變更前后的值
- 變更人、變更時(shí)間
- 支持INSERT/UPDATE/DELETE操作
- 支持單條和批量操作
- 確保數(shù)據(jù)一致性和準(zhǔn)確性
二、整體架構(gòu)設(shè)計(jì)
2.1 核心組件
┌─────────────────────────────────────┐
│ Controller/Service層 │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Mapper接口(帶注解) │
│ @DataChangeLog(businessType) │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ MyBatis Executor.update() │
│ 攔截器攔截點(diǎn) │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ ImprovedDataChangeInterceptor │
│ ┌─────────────────────────────┐ │
│ │ 1. 獲取注解/判斷是否需要記錄 │ │
│ │ 2. 查詢?cè)紨?shù)據(jù) │ │
│ │ 3. 執(zhí)行SQL操作 │ │
│ │ 4. 事務(wù)提交后查詢新數(shù)據(jù) │ │
│ │ 5. 對(duì)比差異并保存記錄 │ │
│ └─────────────────────────────┘ │
└───────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ sys_data_change_log │
│ 數(shù)據(jù)變更記錄表 │
└─────────────────────────────────────┘2.2 數(shù)據(jù)表設(shè)計(jì)
CREATE TABLE sys_data_change_log (
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
business_type varchar(50) NOT NULL COMMENT '業(yè)務(wù)類型',
business_id varchar(50) NOT NULL COMMENT '業(yè)務(wù)數(shù)據(jù)ID',
field_name varchar(50) NOT NULL COMMENT '字段名稱',
field_comment varchar(100) DEFAULT NULL COMMENT '字段注釋',
old_value text COMMENT '變更前值',
new_value text COMMENT '變更后值',
change_type varchar(20) DEFAULT 'UPDATE' COMMENT '變更類型',
change_by varchar(50) NOT NULL COMMENT '變更人',
change_time datetime NOT NULL COMMENT '變更時(shí)間',
PRIMARY KEY (id),
KEY idx_business (business_type, business_id),
KEY idx_change_time (change_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='數(shù)據(jù)變更記錄表';三、核心代碼實(shí)現(xiàn)
3.1 自定義注解
package com.demo.framework.aspectj.lang.annotation;
import java.lang.annotation.*;
/**
* 數(shù)據(jù)變更記錄注解
* 標(biāo)記在Mapper方法上,表示該方法需要記錄數(shù)據(jù)變更
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataChangeLog {
/**
* 業(yè)務(wù)類型
*/
String businessType() default "";
/**
* 是否記錄詳情(字段級(jí)別)
*/
boolean detail() default true;
/**
* 忽略的字段
*/
String[] ignoreFields() default {"createTime", "updateTime", "createBy", "updateBy"};
}3.2 數(shù)據(jù)變更記錄實(shí)體
package com.demo.system.domain;
import com.demo.common.annotation.Excel;
import java.util.Date;
/**
* 數(shù)據(jù)變更記錄對(duì)象 sys_data_change_log
*/
public class SysDataChangeLog {
private static final long serialVersionUID = 1L;
/** 主鍵ID */
private Long id;
/** 業(yè)務(wù)類型 */
@Excel(name = "業(yè)務(wù)類型")
private String businessType;
/** 業(yè)務(wù)數(shù)據(jù)ID */
@Excel(name = "業(yè)務(wù)數(shù)據(jù)ID")
private String businessId;
/** 字段名稱 */
@Excel(name = "字段名稱")
private String fieldName;
/** 字段注釋 */
@Excel(name = "字段注釋")
private String fieldComment;
/** 變更前值 */
@Excel(name = "變更前值")
private String oldValue;
/** 變更后值 */
@Excel(name = "變更后值")
private String newValue;
/** 變更類型 */
@Excel(name = "變更類型")
private String changeType;
/** 變更人 */
@Excel(name = "變更人")
private String changeBy;
/** 變更時(shí)間 */
@Excel(name = "變更時(shí)間", dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date changeTime;
// getters and setters...
}
四、數(shù)據(jù)提交后再記錄日志的方案
4.1 事務(wù)同步機(jī)制
使用Spring的TransactionSynchronization實(shí)現(xiàn)事務(wù)提交后執(zhí)行記錄操作:
/**
* 注冊(cè)事務(wù)提交后的任務(wù)
* 核心:確保數(shù)據(jù)真正寫入數(shù)據(jù)庫后才記錄變更日志
*/
private void registerAfterCommitTask(Runnable task) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
// 事務(wù)中:注冊(cè)事務(wù)同步器,事務(wù)提交后執(zhí)行
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
task.run();
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
log.debug("事務(wù)回滾,不記錄變更");
}
}
}
);
} else {
// 無事務(wù):直接執(zhí)行
log.debug("無事務(wù)環(huán)境,直接執(zhí)行");
task.run();
}
}
4.2 更新操作處理流程
/**
* 處理更新操作
* 三步走策略:
* 1. 更新前查詢?cè)紨?shù)據(jù)
* 2. 執(zhí)行更新操作
* 3. 事務(wù)提交后查詢新數(shù)據(jù)并對(duì)比記錄
*/
private Object handleUpdate(Invocation invocation, MappedStatement ms,
Object parameter, DataChangeLog dataChangeLog) throws Throwable {
log.debug("處理更新操作,業(yè)務(wù)類型:{}", dataChangeLog.businessType());
// 1. 更新前查詢?cè)紨?shù)據(jù)
Object beforeData = queryDataById(parameter, ms);
log.debug("更新前數(shù)據(jù):{}", JSON.toJSONString(beforeData));
// 2. 緩存操作信息
OperationInfo opInfo = new OperationInfo();
opInfo.setBusinessType(dataChangeLog.businessType());
opInfo.setIgnoreFields(dataChangeLog.ignoreFields());
opInfo.setParameter(parameter);
opInfo.setSqlCommandType(SqlCommandType.UPDATE);
opInfo.setMapperId(ms.getId());
opInfo.setBeforeData(beforeData);
OPERATION_INFO_THREAD_LOCAL.set(opInfo);
// 3. 執(zhí)行更新操作
Object result = invocation.proceed();
// 4. 如果更新成功,在事務(wù)提交后處理變更記錄
if (Integer.parseInt(result.toString()) > 0) {
registerAfterCommitTask(() -> processUpdateAfterCommit(opInfo));
}
return result;
}
/**
* 事務(wù)提交后處理更新操作
*/
private void processUpdateAfterCommit(OperationInfo opInfo) {
try {
// 1. 更新后查詢最新數(shù)據(jù)
Object afterData = queryDataById(opInfo.getParameter(), opInfo.getMapperId());
log.debug("更新后數(shù)據(jù):{}", JSON.toJSONString(afterData));
if (opInfo.getBeforeData() != null && afterData != null) {
// 2. 對(duì)比數(shù)據(jù)差異
List<SysDataChangeLog> changeLogs = compareData(
opInfo.getBeforeData(),
afterData,
opInfo.getBusinessType(),
opInfo.getIgnoreFields()
);
// 3. 批量保存變更記錄
if (!changeLogs.isEmpty()) {
dataChangeLogService.insertBatch(changeLogs);
log.info("保存{}條更新變更記錄", changeLogs.size());
}
}
} catch (Exception e) {
log.error("處理更新后變更記錄失敗", e);
}
}
4.3 數(shù)據(jù)對(duì)比工具方法
/**
* 對(duì)比數(shù)據(jù)差異
* 支持日期、數(shù)字類型的精確比較
*/
private List<SysDataChangeLog> compareData(Object beforeData, Object afterData,
String businessType, String[] ignoreFields) {
List<SysDataChangeLog> changeLogs = new ArrayList<>();
try {
Map<String, Object> beforeMap = convertToMap(beforeData);
Map<String, Object> afterMap = convertToMap(afterData);
String businessId = extractBusinessId(afterMap);
// 遍歷更新后的數(shù)據(jù)
for (Map.Entry<String, Object> entry : afterMap.entrySet()) {
String fieldName = entry.getKey();
// 忽略指定字段
if (shouldIgnore(fieldName, ignoreFields)) {
continue;
}
Object beforeValue = beforeMap.get(fieldName);
Object afterValue = entry.getValue();
// 比較值是否變化
if (!isValueEquals(beforeValue, afterValue)) {
SysDataChangeLog changeLog = createChangeLog(
businessType, businessId, fieldName,
getFieldComment(afterData, fieldName),
beforeValue, afterValue, "UPDATE"
);
changeLogs.add(changeLog);
}
}
} catch (Exception e) {
log.error("對(duì)比數(shù)據(jù)差異失敗", e);
}
return changeLogs;
}
/**
* 判斷兩個(gè)值是否相等(處理各種數(shù)據(jù)類型)
*/
private boolean isValueEquals(Object v1, Object v2) {
if (v1 == null && v2 == null) return true;
if (v1 == null || v2 == null) return false;
// 處理日期類型
if (v1 instanceof Date && v2 instanceof Date) {
return ((Date) v1).getTime() == ((Date) v2).getTime();
}
// 處理數(shù)字類型(避免精度問題)
if (v1 instanceof Number && v2 instanceof Number) {
return new BigDecimal(v1.toString())
.compareTo(new BigDecimal(v2.toString())) == 0;
}
return v1.equals(v2);
}
五、攔截器未注冊(cè)到Spring容器的解決方案
5.1 問題分析
在實(shí)際項(xiàng)目中,經(jīng)常遇到攔截器配置了@Component注解,但MyBatis并沒有自動(dòng)注冊(cè)攔截器的情況。主要原因:
- MyBatis自動(dòng)配置順序問題:攔截器注冊(cè)時(shí)機(jī)過早
- 多數(shù)據(jù)源配置:需要為每個(gè)SqlSessionFactory手動(dòng)注冊(cè)
- 自定義MyBatis配置:覆蓋了默認(rèn)配置
5.2 解決方案一:手動(dòng)注冊(cè)攔截器(推薦)
package com.demo.framework.config;
import com.demo.framework.interceptor.ImprovedDataChangeInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* 攔截器手動(dòng)注冊(cè)配置
* 解決攔截器未自動(dòng)注冊(cè)到MyBatis的問題
*/
@Configuration
public class InterceptorManualConfig {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(InterceptorManualConfig.class);
@Autowired
private List<SqlSessionFactory> sqlSessionFactories;
@Autowired
@Lazy // 避免循環(huán)依賴
private ImprovedDataChangeInterceptor dataChangeInterceptor;
@PostConstruct
public void manualRegisterInterceptor() {
log.info("========== 開始手動(dòng)注冊(cè)數(shù)據(jù)變更攔截器 ==========");
log.info("發(fā)現(xiàn) {} 個(gè)SqlSessionFactory", sqlSessionFactories.size());
for (int i = 0; i < sqlSessionFactories.size(); i++) {
SqlSessionFactory sqlSessionFactory = sqlSessionFactories.get(i);
org.apache.ibatis.session.Configuration configuration =
sqlSessionFactory.getConfiguration();
// 檢查是否已經(jīng)注冊(cè)
boolean alreadyRegistered = false;
for (Interceptor interceptor : configuration.getInterceptors()) {
if (interceptor instanceof ImprovedDataChangeInterceptor) {
alreadyRegistered = true;
log.info("SqlSessionFactory[{}] 攔截器已存在,跳過注冊(cè)", i);
break;
}
}
if (!alreadyRegistered) {
log.info("注冊(cè)攔截器到 SqlSessionFactory[{}]: {}",
i, sqlSessionFactory);
configuration.addInterceptor(dataChangeInterceptor);
}
}
// 驗(yàn)證注冊(cè)結(jié)果
verifyInterceptorRegistration();
log.info("========== 攔截器手動(dòng)注冊(cè)完成 ==========");
}
/**
* 驗(yàn)證攔截器注冊(cè)結(jié)果
*/
private void verifyInterceptorRegistration() {
for (int i = 0; i < sqlSessionFactories.size(); i++) {
SqlSessionFactory sqlSessionFactory = sqlSessionFactories.get(i);
List<Interceptor> interceptors =
sqlSessionFactory.getConfiguration().getInterceptors();
log.info("SqlSessionFactory[{}] 已注冊(cè)攔截器數(shù)量: {}", i, interceptors.size());
for (Interceptor interceptor : interceptors) {
log.info(" - {}", interceptor.getClass().getSimpleName());
}
}
}
}
5.3 解決方案二:使用MyBatis配置類
package com.demo.framework.config;
import com.demo.framework.interceptor.ImprovedDataChangeInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* 通過MyBatis配置類注冊(cè)攔截器
*/
@Configuration
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class MyBatisInterceptorConfig {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private ImprovedDataChangeInterceptor dataChangeInterceptor;
@PostConstruct
public void init() {
// 獲取所有SqlSessionFactory
String[] sqlSessionFactoryNames =
applicationContext.getBeanNamesForType(SqlSessionFactory.class);
for (String name : sqlSessionFactoryNames) {
SqlSessionFactory sqlSessionFactory =
applicationContext.getBean(name, SqlSessionFactory.class);
// 獲取當(dāng)前的攔截器列表
org.apache.ibatis.session.Configuration configuration =
sqlSessionFactory.getConfiguration();
// 添加攔截器(如果不存在)
boolean exists = false;
for (Interceptor interceptor : configuration.getInterceptors()) {
if (interceptor instanceof ImprovedDataChangeInterceptor) {
exists = true;
break;
}
}
if (!exists) {
configuration.addInterceptor(dataChangeInterceptor);
}
}
}
}
5.4 解決方案三:驗(yàn)證攔截器是否注冊(cè)
package com.demo.framework.config;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 啟動(dòng)后驗(yàn)證攔截器注冊(cè)情況
*/
@Component
public class InterceptorVerifyRunner implements CommandLineRunner {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(InterceptorVerifyRunner.class);
@Autowired
private List<SqlSessionFactory> sqlSessionFactories;
@Override
public void run(String... args) throws Exception {
log.info("========== 數(shù)據(jù)變更攔截器注冊(cè)驗(yàn)證 ==========");
for (int i = 0; i < sqlSessionFactories.size(); i++) {
SqlSessionFactory factory = sqlSessionFactories.get(i);
List<Interceptor> interceptors = factory.getConfiguration().getInterceptors();
boolean hasInterceptor = false;
for (Interceptor interceptor : interceptors) {
String className = interceptor.getClass().getName();
if (className.contains("DataChangeInterceptor")) {
hasInterceptor = true;
log.info("? SqlSessionFactory[{}] 已注冊(cè)攔截器: {}",
i, className);
}
}
if (!hasInterceptor) {
log.error("? SqlSessionFactory[{}] 未找到數(shù)據(jù)變更攔截器!", i);
}
}
log.info("==========================================");
}
}
5.5 調(diào)試用REST接口
package com.demo.web.controller.monitor;
import com.demo.common.core.controller.BaseController;
import com.demo.common.core.domain.AjaxResult;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 攔截器調(diào)試控制器
*/
@RestController
@RequestMapping("/monitor/interceptor")
public class InterceptorDebugController extends BaseController {
@Autowired
private List<SqlSessionFactory> sqlSessionFactories;
/**
* 查看攔截器注冊(cè)狀態(tài)
*/
@GetMapping("/status")
public AjaxResult status() {
List<Map<String, Object>> result = new ArrayList<>();
for (int i = 0; i < sqlSessionFactories.size(); i++) {
SqlSessionFactory factory = sqlSessionFactories.get(i);
Map<String, Object> factoryInfo = new HashMap<>();
factoryInfo.put("factoryIndex", i);
factoryInfo.put("factoryName", factory.getClass().getSimpleName());
List<Interceptor> interceptors = factory.getConfiguration().getInterceptors();
List<String> interceptorNames = new ArrayList<>();
for (Interceptor interceptor : interceptors) {
interceptorNames.add(interceptor.getClass().getSimpleName());
}
factoryInfo.put("interceptorCount", interceptors.size());
factoryInfo.put("interceptors", interceptorNames);
factoryInfo.put("hasDataChangeInterceptor",
interceptorNames.stream().anyMatch(name -> name.contains("DataChange")));
result.add(factoryInfo);
}
return success(result);
}
/**
* 測(cè)試攔截器是否工作
*/
@GetMapping("/test")
public AjaxResult test() {
// 返回提示信息,實(shí)際測(cè)試需要調(diào)用Mapper方法
return success("請(qǐng)調(diào)用實(shí)際的Mapper方法測(cè)試攔截器,如:/system/user/update");
}
}
六、使用示例
6.1 Mapper接口配置
package com.demo.system.mapper;
import com.demo.framework.aspectj.lang.annotation.DataChangeLog;
import com.demo.system.domain.SysUser;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface SysUserMapper {
/**
* 修改用戶信息
*/
@DataChangeLog(
businessType = "user.update",
ignoreFields = {"createTime", "updateTime", "password"}
)
public int updateUser(SysUser user);
/**
* 新增用戶
*/
@DataChangeLog(businessType = "user.insert")
public int insertUser(SysUser user);
/**
* 刪除用戶
*/
@DataChangeLog(businessType = "user.delete")
public int deleteUserById(Long userId);
/**
* 批量刪除用戶
*/
@DataChangeLog(businessType = "user.batchDelete")
public int deleteUserByIds(@Param("userIds") List<Long> userIds);
/**
* 查詢用戶(用于獲取變更前后的數(shù)據(jù))
*/
public SysUser selectUserById(Long userId);
}
6.2 Service層調(diào)用
@Service
public class SysUserServiceImpl implements ISysUserService {
@Autowired
private SysUserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public int updateUser(SysUser user) {
// 直接調(diào)用Mapper,攔截器會(huì)自動(dòng)處理數(shù)據(jù)變更記錄
// 記錄會(huì)在事務(wù)提交后自動(dòng)寫入
return userMapper.updateUser(user);
}
}
七、常見問題與解決方案
7.1 攔截器未生效排查清單
| 問題 | 檢查點(diǎn) | 解決方案 |
|---|---|---|
| 攔截器類未掃描 | @Component注解是否存在 | 添加注解或手動(dòng)注冊(cè)Bean |
| MyBatis未注冊(cè) | 查看configuration.getInterceptors() | 手動(dòng)注冊(cè)攔截器 |
| 多數(shù)據(jù)源問題 | 所有SqlSessionFactory都檢查 | 為每個(gè)數(shù)據(jù)源注冊(cè)攔截器 |
| 事務(wù)問題 | 事務(wù)提交前查詢不到新數(shù)據(jù) | 使用afterCommit回調(diào) |
| 注解未識(shí)別 | Mapper方法上是否有@DataChangeLog | 檢查注解是否存在 |
7.2 性能優(yōu)化建議
/**
* 批量操作優(yōu)化
*/
private static final int BATCH_THRESHOLD = 100; // 批量閾值
// 分批保存變更記錄
if (allChangeLogs.size() > BATCH_THRESHOLD) {
List<List<SysDataChangeLog>> partitions = partitionList(allChangeLogs, BATCH_THRESHOLD);
for (List<SysDataChangeLog> partition : partitions) {
dataChangeLogService.insertBatch(partition);
}
} else {
dataChangeLogService.insertBatch(allChangeLogs);
}
7.3 日志配置
# application.yml
logging:
level:
com.demo.framework.interceptor: DEBUG
org.apache.ibatis: INFO八、總結(jié)
8.1 方案優(yōu)勢(shì)
- 數(shù)據(jù)一致性:事務(wù)提交后才記錄日志,確保數(shù)據(jù)準(zhǔn)確性
- 性能優(yōu)化:異步處理變更記錄,不影響主業(yè)務(wù)流程
- 字段級(jí)對(duì)比:精確記錄每個(gè)字段的變化
- 易于集成:通過注解配置,對(duì)業(yè)務(wù)代碼無侵入
- 支持批量:完善的批量操作處理機(jī)制
8.2 注意事項(xiàng)
- 查詢方法必須存在:需要提供根據(jù)ID查詢數(shù)據(jù)的方法
- 事務(wù)管理:確保在事務(wù)環(huán)境中使用
- 大字段處理:考慮對(duì)BLOB等大字段的特殊處理
- 數(shù)據(jù)清理:定期清理歷史變更記錄
8.3 擴(kuò)展建議
- 支持異步記錄(使用消息隊(duì)列)
- 支持變更記錄查詢和導(dǎo)出
- 支持自定義字段轉(zhuǎn)換器
- 支持敏感數(shù)據(jù)脫敏
以上就是基于MyBatis攔截器實(shí)現(xiàn)數(shù)據(jù)變更記錄的技術(shù)方案的詳細(xì)內(nèi)容,更多關(guān)于MyBatis攔截器數(shù)據(jù)變更記錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Mybatis 的 Interceptor(攔截器) 與 JSqlparser 結(jié)合解析SQL 使SpringBoot項(xiàng)目多數(shù)據(jù)庫兼容的嘗試(推薦)
- Mybatis攔截器如何實(shí)現(xiàn)數(shù)據(jù)權(quán)限過濾
- mybatis攔截器實(shí)現(xiàn)數(shù)據(jù)庫數(shù)據(jù)權(quán)限隔離方式
- MybatisPlus攔截器如何實(shí)現(xiàn)數(shù)據(jù)表分表
- SpringBoot基于Mybatis攔截器和JSqlParser實(shí)現(xiàn)數(shù)據(jù)隔離
相關(guān)文章
深度分析MybatisPlus查詢結(jié)果映射失敗@TableField失效解決辦法
本文針對(duì)SpringBoot使用MybatisPlus框架時(shí)出現(xiàn)的查詢結(jié)果映射失敗問題進(jìn)行深入分析,通過源碼解析,闡述了@TableField注解失效的原因,并提出了四種有效的解決方案,感興趣的朋友一起看看吧2025-07-07
Java并發(fā)編程之synchronized底層實(shí)現(xiàn)原理分析
這篇文章主要介紹了Java并發(fā)編程之synchronized底層實(shí)現(xiàn)原理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02
使用Mybatis的Batch?Insert?Support?實(shí)現(xiàn)批量插入
這篇文章主要介紹了使用Mybatis的Batch?Insert?Support?實(shí)現(xiàn)批量插入。具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
java實(shí)現(xiàn)百度云文字識(shí)別接口代碼
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)百度云文字識(shí)別的接口代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11
Intellij IDEA 2019 最新亂碼問題及解決必殺技(必看篇)
大家在使用Intellij IDEA 的時(shí)候會(huì)經(jīng)常遇到各種亂碼問題,今天小編給大家分享一些關(guān)于Intellij IDEA 2019 最新亂碼問題及解決必殺技,感興趣的朋友跟隨小編一起看看吧2020-04-04
SpringBoot+Redis海量重復(fù)提交問題解決
在實(shí)際的開發(fā)項(xiàng)目中,一個(gè)對(duì)外暴露的接口往往會(huì)面臨很多次請(qǐng)求,所以本文介紹一下SpringBoot+Redis海量重復(fù)提交問題解決,感興趣的可以了解一下2023-12-12

