Mybatis学习笔记(六)
高级特性与插件
逻辑删除详解
简要描述:逻辑删除是一种软删除机制,通过标记字段来表示数据的删除状态,而不是物理删除数据记录。
核心概念:
- 软删除:标记删除状态,保留数据
- 硬删除:物理删除数据记录
- 删除标识:用于标记删除状态的字段
- 全局配置:统一的逻辑删除处理
- 查询过滤:自动过滤已删除数据
逻辑删除配置
/*** 逻辑删除配置*/
@Configuration
public class LogicDeleteConfig {/*** 逻辑删除插件配置*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加逻辑删除插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}/*** 全局逻辑删除配置*/@Bean@ConfigurationProperties(prefix = "mybatis-plus.global-config.db-config")public GlobalConfig.DbConfig dbConfig() {GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();// 逻辑删除字段名dbConfig.setLogicDeleteField("deleted");// 逻辑删除值(删除后的值)dbConfig.setLogicDeleteValue("1");// 逻辑未删除值(未删除的值)dbConfig.setLogicNotDeleteValue("0");return dbConfig;}
}/*** 配置文件方式配置逻辑删除*/
/*
mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局逻辑删除字段名logic-delete-value: 1 # 逻辑已删除值logic-not-delete-value: 0 # 逻辑未删除值
*/
实体类逻辑删除配置
/*** 用户实体类 - 逻辑删除配置*/
@Data
@TableName("sys_user")
public class User {@TableId(type = IdType.AUTO)private Long id;private String username;private String email;private String phone;/*** 逻辑删除字段* value: 未删除值* delval: 已删除值*/@TableLogic(value = "0", delval = "1")private Integer deleted;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 订单实体类 - 自定义逻辑删除字段*/
@Data
@TableName("t_order")
public class Order {@TableId(type = IdType.AUTO)private Long id;private String orderNo;private BigDecimal amount;private Long userId;/*** 自定义逻辑删除字段* 使用字符串类型*/@TableLogic(value = "N", delval = "Y")private String isDeleted;/*** 删除时间* 记录删除的具体时间*/private LocalDateTime deleteTime;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}/*** 商品实体类 - 时间戳逻辑删除*/
@Data
@TableName("t_product")
public class Product {@TableId(type = IdType.AUTO)private Long id;private String name;private BigDecimal price;private Integer stock;/*** 时间戳逻辑删除* 0表示未删除,删除时间戳表示已删除*/@TableLogic(value = "0")private Long deletedAt;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}
逻辑删除基本操作
/*** 逻辑删除基本操作*/
@Service
public class LogicDeleteService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate OrderMapper orderMapper;/*** 逻辑删除单个记录*/public boolean deleteUser(Long userId) {// 执行逻辑删除,会自动设置deleted字段为1int result = userMapper.deleteById(userId);return result > 0;}/*** 批量逻辑删除*/public boolean deleteUsers(List<Long> userIds) {// 批量逻辑删除int result = userMapper.deleteBatchIds(userIds);return result > 0;}/*** 条件逻辑删除*/public boolean deleteUsersByCondition(String department) {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getDepartment, department);// 条件逻辑删除int result = userMapper.delete(wrapper);return result > 0;}/*** 查询操作自动过滤已删除数据*/public List<User> getActiveUsers() {// 查询时自动过滤deleted=1的记录return userMapper.selectList(null);}/*** 条件查询自动过滤已删除数据*/public List<User> getUsersByDepartment(String department) {LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getDepartment, department);// 查询时自动添加deleted=0条件return userMapper.selectList(wrapper);}/*** 分页查询自动过滤已删除数据*/public IPage<User> getUsersPage(int current, int size) {Page<User> page = new Page<>(current, size);// 分页查询时自动过滤已删除数据return userMapper.selectPage(page, null);}
}
查询已删除数据
/*** 查询已删除数据*/
@Service
public class DeletedDataService {@Autowiredprivate UserMapper userMapper;/*** 查询所有数据(包括已删除)* 使用原生SQL或自定义方法*/public List<User> getAllUsersIncludeDeleted() {// 方式1:使用原生SQLreturn userMapper.selectAllIncludeDeleted();}/*** 查询已删除的数据*/public List<User> getDeletedUsers() {// 使用原生SQL查询已删除数据return userMapper.selectDeletedUsers();}/*** 根据删除时间范围查询已删除数据*/public List<User> getDeletedUsersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {return userMapper.selectDeletedUsersByTimeRange(startTime, endTime);}/*** 统计已删除数据数量*/public long countDeletedUsers() {return userMapper.countDeletedUsers();}
}/*** UserMapper - 自定义方法查询已删除数据*/
@Mapper
public interface UserMapper extends BaseMapper<User> {/*** 查询所有用户(包括已删除)*/@Select("SELECT * FROM sys_user")List<User> selectAllIncludeDeleted();/*** 查询已删除的用户*/@Select("SELECT * FROM sys_user WHERE deleted = 1")List<User> selectDeletedUsers();/*** 根据删除时间范围查询已删除用户*/@Select("SELECT * FROM sys_user WHERE deleted = 1 AND update_time BETWEEN #{startTime} AND #{endTime}")List<User> selectDeletedUsersByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);/*** 统计已删除用户数量*/@Select("SELECT COUNT(*) FROM sys_user WHERE deleted = 1")long countDeletedUsers();/*** 物理删除用户*/@Delete("DELETE FROM sys_user WHERE id = #{id}")int physicalDeleteById(@Param("id") Long id);/*** 恢复已删除用户*/@Update("UPDATE sys_user SET deleted = 0, update_time = NOW() WHERE id = #{id} AND deleted = 1")int restoreUser(@Param("id") Long id);
}
数据恢复功能
/*** 数据恢复服务*/
@Service
public class DataRecoveryService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate OrderMapper orderMapper;/*** 恢复单个用户*/public boolean restoreUser(Long userId) {// 使用自定义方法恢复int result = userMapper.restoreUser(userId);return result > 0;}/*** 批量恢复用户*/public boolean restoreUsers(List<Long> userIds) {int successCount = 0;for (Long userId : userIds) {if (restoreUser(userId)) {successCount++;}}return successCount == userIds.size();}/*** 条件恢复用户*/public boolean restoreUsersByCondition(String department) {return userMapper.restoreUsersByDepartment(department) > 0;}/*** 恢复指定时间范围内删除的用户*/public boolean restoreUsersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) {return userMapper.restoreUsersByTimeRange(startTime, endTime) > 0;}/*** 获取可恢复的用户列表*/public List<User> getRecoverableUsers(int days) {LocalDateTime cutoffTime = LocalDateTime.now().minusDays(days);return userMapper.selectDeletedUsersSince(cutoffTime);}
}/*** UserMapper - 数据恢复相关方法*/
@Mapper
public interface UserMapper extends BaseMapper<User> {/*** 恢复用户*/@Update("UPDATE sys_user SET deleted = 0, update_time = NOW() WHERE id = #{id} AND deleted = 1")int restoreUser(@Param("id") Long id);/*** 按部门恢复用户*/@Update("UPDATE sys_user SET deleted = 0, update_time = NOW() WHERE department = #{department} AND deleted = 1")int restoreUsersByDepartment(@Param("department") String department);/*** 按时间范围恢复用户*/@Update("UPDATE sys_user SET deleted = 0, update_time = NOW() WHERE deleted = 1 AND update_time BETWEEN #{startTime} AND #{endTime}")int restoreUsersByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);/*** 查询指定时间后删除的用户*/@Select("SELECT * FROM sys_user WHERE deleted = 1 AND update_time >= #{cutoffTime}")List<User> selectDeletedUsersSince(@Param("cutoffTime") LocalDateTime cutoffTime);
}
物理删除与清理
/*** 物理删除与数据清理服务*/
@Service
public class DataCleanupService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate OrderMapper orderMapper;/*** 物理删除单个记录*/public boolean physicalDeleteUser(Long userId) {// 使用自定义方法进行物理删除int result = userMapper.physicalDeleteById(userId);return result > 0;}/*** 清理过期的已删除数据* 删除超过指定天数的逻辑删除记录*/@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行public void cleanupExpiredDeletedData() {// 清理30天前删除的用户数据LocalDateTime cutoffTime = LocalDateTime.now().minusDays(30);int deletedCount = userMapper.physicalDeleteExpiredUsers(cutoffTime);log.info("清理过期用户数据,删除记录数:{}", deletedCount);// 清理60天前删除的订单数据LocalDateTime orderCutoffTime = LocalDateTime.now().minusDays(60);int deletedOrderCount = orderMapper.physicalDeleteExpiredOrders(orderCutoffTime);log.info("清理过期订单数据,删除记录数:{}", deletedOrderCount);}/*** 批量物理删除*/public boolean batchPhysicalDelete(List<Long> userIds) {int result = userMapper.batchPhysicalDelete(userIds);return result > 0;}/*** 按条件物理删除*/public boolean physicalDeleteByCondition(String department, LocalDateTime beforeTime) {int result = userMapper.physicalDeleteByCondition(department, beforeTime);return result > 0;}/*** 获取待清理数据统计*/public Map<String, Long> getCleanupStatistics() {Map<String, Long> statistics = new HashMap<>();LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);LocalDateTime sixtyDaysAgo = LocalDateTime.now().minusDays(60);// 统计各种待清理数据statistics.put("expiredUsers", userMapper.countExpiredDeletedUsers(thirtyDaysAgo));statistics.put("expiredOrders", orderMapper.countExpiredDeletedOrders(sixtyDaysAgo));statistics.put("totalDeletedUsers", userMapper.countDeletedUsers());statistics.put("totalDeletedOrders", orderMapper.countDeletedOrders());return statistics;}
}/*** UserMapper - 物理删除相关方法*/
@Mapper
public interface UserMapper extends BaseMapper<User> {/*** 物理删除用户*/@Delete("DELETE FROM sys_user WHERE id = #{id}")int physicalDeleteById(@Param("id") Long id);/*** 物理删除过期的已删除用户*/@Delete("DELETE FROM sys_user WHERE deleted = 1 AND update_time < #{cutoffTime}")int physicalDeleteExpiredUsers(@Param("cutoffTime") LocalDateTime cutoffTime);/*** 批量物理删除*/@Delete("<script>" +"DELETE FROM sys_user WHERE id IN " +"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +"#{id}" +"</foreach>" +"</script>")int batchPhysicalDelete(@Param("ids") List<Long> ids);/*** 按条件物理删除*/@Delete("DELETE FROM sys_user WHERE department = #{department} AND deleted = 1 AND update_time < #{beforeTime}")int physicalDeleteByCondition(@Param("department") String department, @Param("beforeTime") LocalDateTime beforeTime);/*** 统计过期的已删除用户数量*/@Select("SELECT COUNT(*) FROM sys_user WHERE deleted = 1 AND update_time < #{cutoffTime}")long countExpiredDeletedUsers(@Param("cutoffTime") LocalDateTime cutoffTime);
}
自动填充机制
简要描述:自动填充机制是MybatisPlus提供的一种在插入或更新数据时自动填充指定字段值的功能。
核心概念:
- 字段填充:在特定操作时自动设置字段值
- 填充策略:定义何时进行字段填充
- 填充处理器:实现具体的填充逻辑
- 全局配置:统一的填充规则
- 注解配置:基于注解的字段填充
自动填充配置
/*** 自动填充处理器*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {private static final Logger log = LoggerFactory.getLogger(MyMetaObjectHandler.class);/*** 插入时的填充策略*/@Overridepublic void insertFill(MetaObject metaObject) {log.info("开始插入填充...");// 填充创建时间this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// 填充更新时间this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 填充创建人(从当前登录用户获取)this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUser());// 填充更新人this.strictInsertFill(metaObject, "updateBy", String.class, getCurrentUser());// 填充版本号this.strictInsertFill(metaObject, "version", Integer.class, 1);// 填充删除标识this.strictInsertFill(metaObject, "deleted", Integer.class, 0);// 填充租户ID(多租户场景)this.strictInsertFill(metaObject, "tenantId", Long.class, getCurrentTenantId());}/*** 更新时的填充策略*/@Overridepublic void updateFill(MetaObject metaObject) {log.info("开始更新填充...");// 填充更新时间this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 填充更新人this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());}/*** 获取当前登录用户*/private String getCurrentUser() {// 从Spring Security或其他安全框架获取当前用户try {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.isAuthenticated()) {return authentication.getName();}} catch (Exception e) {log.warn("获取当前用户失败", e);}return "system";}/*** 获取当前租户ID*/private Long getCurrentTenantId() {// 从上下文获取租户IDtry {// 假设有一个TenantContext来管理租户信息return TenantContext.getCurrentTenantId();} catch (Exception e) {log.warn("获取当前租户ID失败", e);}return 1L; // 默认租户}
}/*** 高级自动填充处理器*/
@Component
public class AdvancedMetaObjectHandler implements MetaObjectHandler {/*** 插入时的填充策略*/@Overridepublic void insertFill(MetaObject metaObject) {// 条件填充:只有当字段为空时才填充this.fillStrategy(metaObject, "createTime", LocalDateTime.now());this.fillStrategy(metaObject, "updateTime", LocalDateTime.now());// 自定义填充逻辑if (metaObject.getOriginalObject() instanceof User) {User user = (User) metaObject.getOriginalObject();if (user.getStatus() == null) {this.strictInsertFill(metaObject, "status", Integer.class, 1);}}// 批量填充Map<String, Object> fillMap = new HashMap<>();fillMap.put("createBy", getCurrentUser());fillMap.put("updateBy", getCurrentUser());fillMap.put("version", 1);fillMap.put("deleted", 0);fillMap.forEach((fieldName, value) -> {if (metaObject.hasSetter(fieldName)) {this.setFieldValByName(fieldName, value, metaObject);}});}/*** 更新时的填充策略*/@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());}/*** 条件填充策略*/private void fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal) {// 只有当字段值为空时才填充if (getFieldValByName(fieldName, metaObject) == null) {this.setFieldValByName(fieldName, fieldVal, metaObject);}}private String getCurrentUser() {// 实现获取当前用户的逻辑return "admin";}
}
实体类填充配置
/*** 基础实体类*/
@Data
public abstract class BaseEntity {/*** 主键ID*/@TableId(type = IdType.AUTO)private Long id;/*** 创建时间* INSERT时自动填充*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间* INSERT和UPDATE时都自动填充*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;/*** 创建人* INSERT时自动填充*/@TableField(fill = FieldFill.INSERT)private String createBy;/*** 更新人* INSERT和UPDATE时都自动填充*/@TableField(fill = FieldFill.INSERT_UPDATE)private String updateBy;/*** 版本号(乐观锁)* INSERT时自动填充*/@Version@TableField(fill = FieldFill.INSERT)private Integer version;/*** 逻辑删除标识* INSERT时自动填充*/@TableLogic@TableField(fill = FieldFill.INSERT)private Integer deleted;
}/*** 用户实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseEntity {private String username;private String password;private String email;private String phone;/*** 用户状态* INSERT时自动填充*/@TableField(fill = FieldFill.INSERT)private Integer status;/*** 部门ID*/private Long departmentId;/*** 角色列表*/@TableField(exist = false)private List<Role> roles;
}/*** 订单实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_order")
public class Order extends BaseEntity {private String orderNo;private BigDecimal amount;private Long userId;/*** 订单状态* INSERT时自动填充默认状态*/@TableField(fill = FieldFill.INSERT)private Integer status;/*** 支付时间* 不自动填充,业务逻辑控制*/private LocalDateTime payTime;/*** 完成时间* 不自动填充,业务逻辑控制*/private LocalDateTime completeTime;
}/*** 多租户实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_tenant_data")
public class TenantData extends BaseEntity {private String name;private String description;/*** 租户ID* INSERT时自动填充*/@TableField(fill = FieldFill.INSERT)private Long tenantId;
}
填充策略详解
/*** 填充策略枚举详解*/
public enum FieldFill {/*** 默认不处理*/DEFAULT,/*** 插入时填充字段*/INSERT,/*** 更新时填充字段*/UPDATE,/*** 插入和更新时填充字段*/INSERT_UPDATE
}/*** 自定义填充策略示例*/
@Component
public class CustomFillStrategy implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {// 1. 严格填充(字段必须存在)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// 2. 非严格填充(字段可能不存在)this.fillStrategy(metaObject, "createTime", LocalDateTime.now());// 3. 条件填充if (metaObject.hasSetter("status")) {Object status = this.getFieldValByName("status", metaObject);if (status == null) {this.setFieldValByName("status", 1, metaObject);}}// 4. 基于实体类型的填充Object originalObject = metaObject.getOriginalObject();if (originalObject instanceof User) {fillUserSpecificFields(metaObject, (User) originalObject);} else if (originalObject instanceof Order) {fillOrderSpecificFields(metaObject, (Order) originalObject);}}@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());}/*** 填充用户特定字段*/private void fillUserSpecificFields(MetaObject metaObject, User user) {if (user.getStatus() == null) {this.setFieldValByName("status", 1, metaObject);}}/*** 填充订单特定字段*/private void fillOrderSpecificFields(MetaObject metaObject, Order order) {if (order.getStatus() == null) {this.setFieldValByName("status", 0, metaObject); // 订单初始状态}if (order.getOrderNo() == null) {this.setFieldValByName("orderNo", generateOrderNo(), metaObject);}}/*** 生成订单号*/private String generateOrderNo() {return "ORD" + System.currentTimeMillis();}private String getCurrentUser() {return "admin";}
}
高级填充功能
/*** 高级填充功能*/
@Component
public class AdvancedFillHandler implements MetaObjectHandler {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void insertFill(MetaObject metaObject) {// 1. 基于缓存的填充fillFromCache(metaObject);// 2. 基于数据库的填充fillFromDatabase(metaObject);// 3. 基于外部服务的填充fillFromExternalService(metaObject);// 4. 基于配置的填充fillFromConfiguration(metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());// 更新版本号Object version = this.getFieldValByName("version", metaObject);if (version instanceof Integer) {this.setFieldValByName("version", (Integer) version + 1, metaObject);}}/*** 基于缓存的填充*/private void fillFromCache(MetaObject metaObject) {try {String currentUser = (String) redisTemplate.opsForValue().get("current:user");if (currentUser != null) {this.fillStrategy(metaObject, "createBy", currentUser);this.fillStrategy(metaObject, "updateBy", currentUser);}} catch (Exception e) {// 缓存获取失败,使用默认值this.fillStrategy(metaObject, "createBy", "system");this.fillStrategy(metaObject, "updateBy", "system");}}/*** 基于数据库的填充*/private void fillFromDatabase(MetaObject metaObject) {// 例如:根据用户ID填充部门信息Object originalObject = metaObject.getOriginalObject();if (originalObject instanceof User) {User user = (User) originalObject;if (user.getDepartmentId() != null && user.getDepartmentName() == null) {// 从数据库查询部门名称并填充String departmentName = getDepartmentNameById(user.getDepartmentId());this.setFieldValByName("departmentName", departmentName, metaObject);}}}/*** 基于外部服务的填充*/private void fillFromExternalService(MetaObject metaObject) {// 例如:调用外部服务获取地理位置信息try {String clientIp = getClientIp();if (clientIp != null) {LocationInfo location = getLocationByIp(clientIp);this.fillStrategy(metaObject, "createLocation", location.getCity());}} catch (Exception e) {// 外部服务调用失败,使用默认值this.fillStrategy(metaObject, "createLocation", "Unknown");}}/*** 基于配置的填充*/private void fillFromConfiguration(MetaObject metaObject) {// 从配置文件或配置中心获取默认值Properties config = loadConfiguration();String defaultStatus = config.getProperty("default.user.status", "1");this.fillStrategy(metaObject, "status", Integer.valueOf(defaultStatus));String defaultTenantId = config.getProperty("default.tenant.id", "1");this.fillStrategy(metaObject, "tenantId", Long.valueOf(defaultTenantId));}// 辅助方法private String getCurrentUser() {return "admin";}private String getDepartmentNameById(Long departmentId) {// 模拟数据库查询return "IT部门";}private String getClientIp() {// 获取客户端IPreturn "127.0.0.1";}private LocationInfo getLocationByIp(String ip) {// 调用外部地理位置服务return new LocationInfo("北京", "中国");}private Properties loadConfiguration() {// 加载配置Properties props = new Properties();props.setProperty("default.user.status", "1");props.setProperty("default.tenant.id", "1");return props;}@Data@AllArgsConstructorprivate static class LocationInfo {private String city;private String country;}
}
自动填充最佳实践
/*** 自动填充最佳实践*/
@Component
public class BestPracticeFillHandler implements MetaObjectHandler {private static final Logger log = LoggerFactory.getLogger(BestPracticeFillHandler.class);@Overridepublic void insertFill(MetaObject metaObject) {log.debug("执行插入填充,实体类型:{}", metaObject.getOriginalObject().getClass().getSimpleName());try {// 1. 基础字段填充fillBasicFields(metaObject);// 2. 业务字段填充fillBusinessFields(metaObject);// 3. 审计字段填充fillAuditFields(metaObject);} catch (Exception e) {log.error("自动填充失败", e);// 填充失败不应该影响主业务流程}}@Overridepublic void updateFill(MetaObject metaObject) {log.debug("执行更新填充,实体类型:{}", metaObject.getOriginalObject().getClass().getSimpleName());try {// 更新时只填充必要字段this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUser());} catch (Exception e) {log.error("更新填充失败", e);}}/*** 填充基础字段*/private void fillBasicFields(MetaObject metaObject) {LocalDateTime now = LocalDateTime.now();String currentUser = getCurrentUser();this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);this.strictInsertFill(metaObject, "createBy", String.class, currentUser);this.strictInsertFill(metaObject, "updateBy", String.class, currentUser);}/*** 填充业务字段*/private void fillBusinessFields(MetaObject metaObject) {this.strictInsertFill(metaObject, "version", Integer.class, 1);this.strictInsertFill(metaObject, "deleted", Integer.class, 0);this.strictInsertFill(metaObject, "status", Integer.class, 1);}/*** 填充审计字段*/private void fillAuditFields(MetaObject metaObject) {// 填充操作IPString clientIp = getClientIp();this.fillStrategy(metaObject, "createIp", clientIp);// 填充租户IDLong tenantId = getCurrentTenantId();this.strictInsertFill(metaObject, "tenantId", Long.class, tenantId);}/*** 安全的字段填充*/private void fillStrategy(MetaObject metaObject, String fieldName, Object fieldVal) {if (metaObject.hasSetter(fieldName) && getFieldValByName(fieldName, metaObject) == null) {this.setFieldValByName(fieldName, fieldVal, metaObject);}}private String getCurrentUser() {try {// 从Spring Security获取当前用户Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.isAuthenticated()) {return authentication.getName();}} catch (Exception e) {log.warn("获取当前用户失败", e);}return "system";}private String getClientIp() {try {// 从请求上下文获取客户端IPHttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();return getIpAddress(request);} catch (Exception e) {return "unknown";}}private String getIpAddress(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}private Long getCurrentTenantId() {// 从上下文获取租户IDreturn 1L;}
}
乐观锁插件
简要描述:乐观锁插件是MybatisPlus提供的一种并发控制机制,通过版本号字段来防止数据的并发修改冲突。
核心概念:
- 版本控制:通过版本号字段控制并发更新
- CAS操作:Compare And Swap,比较并交换
- 并发安全:防止数据的并发修改冲突
- 自动版本管理:自动递增版本号
- 更新失败检测:检测并发更新冲突
乐观锁配置
/*** MybatisPlus配置类*/
@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {/*** 乐观锁插件配置*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());// 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}/*** 自定义乐观锁插件配置*/@Beanpublic OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {OptimisticLockerInnerInterceptor interceptor = new OptimisticLockerInnerInterceptor();// 设置乐观锁字段名(默认为version)// interceptor.setVersionField("version");return interceptor;}
}/*** 乐观锁配置属性*/
@ConfigurationProperties(prefix = "mybatis-plus.optimistic-lock")
@Data
public class OptimisticLockProperties {/*** 是否启用乐观锁*/private boolean enabled = true;/*** 版本字段名*/private String versionField = "version";/*** 是否在更新失败时抛出异常*/private boolean throwExceptionOnUpdateFail = false;/*** 最大重试次数*/private int maxRetryCount = 3;
}
实体类乐观锁配置
/*** 基础实体类(包含乐观锁)*/
@Data
public abstract class BaseEntity {@TableId(type = IdType.AUTO)private Long id;/*** 版本号(乐观锁字段)* 使用@Version注解标识*/@Version@TableField(fill = FieldFill.INSERT)private Integer version;@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;@TableField(fill = FieldFill.INSERT)private String createBy;@TableField(fill = FieldFill.INSERT_UPDATE)private String updateBy;
}/*** 用户实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseEntity {private String username;private String password;private String email;private String phone;private Integer status;private BigDecimal balance; // 余额字段,需要并发控制
}/*** 商品实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_product")
public class Product extends BaseEntity {private String name;private String description;private BigDecimal price;/*** 库存数量(需要并发控制的关键字段)*/private Integer stock;/*** 销售数量*/private Integer salesCount;
}/*** 订单实体类*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_order")
public class Order extends BaseEntity {private String orderNo;private Long userId;private BigDecimal amount;/*** 订单状态(需要状态控制)*/private Integer status;private LocalDateTime payTime;private LocalDateTime completeTime;
}
乐观锁基本使用
/*** 用户服务类*/
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;/*** 更新用户信息(自动乐观锁控制)*/public boolean updateUser(User user) {// MybatisPlus会自动在WHERE条件中加入version字段// 并且自动将version+1int result = userMapper.updateById(user);return result > 0;}/*** 更新用户余额(并发安全)*/public boolean updateUserBalance(Long userId, BigDecimal amount) {// 1. 先查询当前用户信息(包含version)User user = userMapper.selectById(userId);if (user == null) {throw new RuntimeException("用户不存在");}// 2. 检查余额是否足够if (user.getBalance().compareTo(amount) < 0) {throw new RuntimeException("余额不足");}// 3. 更新余额(乐观锁自动控制)user.setBalance(user.getBalance().subtract(amount));int result = userMapper.updateById(user);if (result == 0) {throw new RuntimeException("更新失败,可能存在并发冲突");}return true;}/*** 批量更新用户状态*/public void batchUpdateUserStatus(List<Long> userIds, Integer status) {for (Long userId : userIds) {User user = userMapper.selectById(userId);if (user != null) {user.setStatus(status);int result = userMapper.updateById(user);if (result == 0) {// 记录更新失败的用户System.out.println("用户 " + userId + " 更新失败,可能存在并发冲突");}}}}
}/*** 商品服务类*/
@Service
public class ProductService {@Autowiredprivate ProductMapper productMapper;/*** 减少商品库存(高并发场景)*/public boolean decreaseStock(Long productId, Integer quantity) {// 1. 查询商品信息Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException("商品不存在");}// 2. 检查库存是否足够if (product.getStock() < quantity) {throw new RuntimeException("库存不足");}// 3. 减少库存(乐观锁控制)product.setStock(product.getStock() - quantity);product.setSalesCount(product.getSalesCount() + quantity);int result = productMapper.updateById(product);return result > 0;}/*** 增加商品库存*/public boolean increaseStock(Long productId, Integer quantity) {Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException("商品不存在");}product.setStock(product.getStock() + quantity);int result = productMapper.updateById(product);return result > 0;}
}
乐观锁重试机制
/*** 乐观锁重试服务*/
@Service
public class OptimisticLockRetryService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate ProductMapper productMapper;/*** 带重试的用户余额更新*/public boolean updateUserBalanceWithRetry(Long userId, BigDecimal amount) {int maxRetries = 3;int retryCount = 0;while (retryCount < maxRetries) {try {User user = userMapper.selectById(userId);if (user == null) {throw new RuntimeException("用户不存在");}if (user.getBalance().compareTo(amount) < 0) {throw new RuntimeException("余额不足");}user.setBalance(user.getBalance().subtract(amount));int result = userMapper.updateById(user);if (result > 0) {return true; // 更新成功}// 更新失败,进行重试retryCount++;if (retryCount < maxRetries) {// 等待一段时间后重试Thread.sleep(50 + retryCount * 10);}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("重试被中断", e);}}throw new RuntimeException("更新失败,超过最大重试次数");}/*** 带重试的库存减少*/public boolean decreaseStockWithRetry(Long productId, Integer quantity) {return executeWithRetry(() -> {Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException("商品不存在");}if (product.getStock() < quantity) {throw new RuntimeException("库存不足");}product.setStock(product.getStock() - quantity);product.setSalesCount(product.getSalesCount() + quantity);return productMapper.updateById(product) > 0;}, 3, 50);}/*** 通用重试执行器*/private <T> T executeWithRetry(Supplier<T> operation, int maxRetries, long baseDelayMs) {int retryCount = 0;while (retryCount < maxRetries) {try {return operation.get();} catch (Exception e) {retryCount++;if (retryCount >= maxRetries) {throw new RuntimeException("操作失败,超过最大重试次数: " + maxRetries, e);}try {// 指数退避策略long delay = baseDelayMs * (1L << (retryCount - 1));Thread.sleep(delay);} catch (InterruptedException ie) {Thread.currentThread().interrupt();throw new RuntimeException("重试被中断", ie);}}}throw new RuntimeException("不应该到达这里");}
}/*** 基于注解的重试机制*/
@Component
public class RetryableOptimisticLockService {@Autowiredprivate UserMapper userMapper;/*** 使用Spring Retry的重试机制*/@Retryable(value = {OptimisticLockingFailureException.class},maxAttempts = 3,backoff = @Backoff(delay = 100, multiplier = 2))public boolean updateUserWithRetry(User user) {int result = userMapper.updateById(user);if (result == 0) {throw new OptimisticLockingFailureException("乐观锁更新失败");}return true;}/*** 重试失败后的恢复方法*/@Recoverpublic boolean recover(OptimisticLockingFailureException ex, User user) {// 记录失败日志System.err.println("用户更新最终失败: " + user.getId());return false;}
}/*** 自定义乐观锁异常*/
public class OptimisticLockingFailureException extends RuntimeException {public OptimisticLockingFailureException(String message) {super(message);}public OptimisticLockingFailureException(String message, Throwable cause) {super(message, cause);}
}
乐观锁高级应用
/*** 订单服务(复杂业务场景)*/
@Service
@Transactional
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate ProductMapper productMapper;/*** 创建订单(涉及多表乐观锁)*/public Order createOrder(Long userId, Long productId, Integer quantity) {// 1. 检查并减少商品库存Product product = productMapper.selectById(productId);if (product == null) {throw new RuntimeException("商品不存在");}if (product.getStock() < quantity) {throw new RuntimeException("库存不足");}// 减少库存(乐观锁控制)product.setStock(product.getStock() - quantity);int productUpdateResult = productMapper.updateById(product);if (productUpdateResult == 0) {throw new RuntimeException("库存更新失败,请重试");}// 2. 检查并扣减用户余额User user = userMapper.selectById(userId);if (user == null) {throw new RuntimeException("用户不存在");}BigDecimal totalAmount = product.getPrice().multiply(new BigDecimal(quantity));if (user.getBalance().compareTo(totalAmount) < 0) {throw new RuntimeException("余额不足");}// 扣减余额(乐观锁控制)user.setBalance(user.getBalance().subtract(totalAmount));int userUpdateResult = userMapper.updateById(user);if (userUpdateResult == 0) {throw new RuntimeException("余额扣减失败,请重试");}// 3. 创建订单Order order = new Order();order.setOrderNo(generateOrderNo());order.setUserId(userId);order.setAmount(totalAmount);order.setStatus(0); // 待支付orderMapper.insert(order);return order;}/*** 更新订单状态(状态机控制)*/public boolean updateOrderStatus(Long orderId, Integer fromStatus, Integer toStatus) {Order order = orderMapper.selectById(orderId);if (order == null) {throw new RuntimeException("订单不存在");}// 检查状态转换是否合法if (!order.getStatus().equals(fromStatus)) {throw new RuntimeException("订单状态不匹配,当前状态: " + order.getStatus());}// 更新状态(乐观锁控制)order.setStatus(toStatus);if (toStatus == 2) { // 已完成order.setCompleteTime(LocalDateTime.now());}int result = orderMapper.updateById(order);return result > 0;}/*** 取消订单(回滚库存和余额)*/public boolean cancelOrder(Long orderId) {Order order = orderMapper.selectById(orderId);if (order == null) {throw new RuntimeException("订单不存在");}if (order.getStatus() != 0) {throw new RuntimeException("只能取消待支付订单");}// 更新订单状态为已取消order.setStatus(-1);int orderUpdateResult = orderMapper.updateById(order);if (orderUpdateResult == 0) {throw new RuntimeException("订单状态更新失败");}// 回滚用户余额User user = userMapper.selectById(order.getUserId());user.setBalance(user.getBalance().add(order.getAmount()));int userUpdateResult = userMapper.updateById(user);if (userUpdateResult == 0) {throw new RuntimeException("余额回滚失败");}return true;}private String generateOrderNo() {return "ORD" + System.currentTimeMillis();}
}/*** 乐观锁监控和统计*/
@Component
public class OptimisticLockMonitor {private final AtomicLong successCount = new AtomicLong(0);private final AtomicLong failureCount = new AtomicLong(0);private final AtomicLong retryCount = new AtomicLong(0);/*** 记录成功更新*/public void recordSuccess() {successCount.incrementAndGet();}/*** 记录失败更新*/public void recordFailure() {failureCount.incrementAndGet();}/*** 记录重试*/public void recordRetry() {retryCount.incrementAndGet();}/*** 获取统计信息*/public Map<String, Object> getStatistics() {Map<String, Object> stats = new HashMap<>();long success = successCount.get();long failure = failureCount.get();long retry = retryCount.get();long total = success + failure;stats.put("successCount", success);stats.put("failureCount", failure);stats.put("retryCount", retry);stats.put("totalCount", total);stats.put("successRate", total > 0 ? (double) success / total : 0.0);stats.put("failureRate", total > 0 ? (double) failure / total : 0.0);stats.put("avgRetryPerOperation", total > 0 ? (double) retry / total : 0.0);return stats;}/*** 重置统计*/public void reset() {successCount.set(0);failureCount.set(0);retryCount.set(0);}
}
SQL性能分析插件
简要描述:SQL性能分析插件是MybatisPlus提供的一种SQL执行性能监控和分析工具,可以帮助开发者发现慢SQL和性能瓶颈。
核心概念:
- SQL监控:实时监控SQL执行情况
- 性能分析:分析SQL执行时间和性能指标
- 慢SQL检测:自动检测执行时间超过阈值的SQL
- 执行统计:统计SQL执行次数和平均耗时
- 性能报告:生成详细的性能分析报告
性能分析插件配置
/*** SQL性能分析插件配置*/
@Configuration
public class PerformanceAnalysisConfig {/*** 配置性能分析插件*/@Bean@Profile({"dev", "test"}) // 只在开发和测试环境启用public MybatisPlusInterceptor performanceInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加性能分析插件(注意:3.4.0版本后已移除,需要自定义实现)interceptor.addInnerInterceptor(new CustomPerformanceInnerInterceptor());return interceptor;}
}/*** 自定义性能分析拦截器*/
public class CustomPerformanceInnerInterceptor implements InnerInterceptor {private static final Logger log = LoggerFactory.getLogger(CustomPerformanceInnerInterceptor.class);/*** 慢SQL阈值(毫秒)*/private long slowSqlThreshold = 1000;/*** 是否格式化SQL*/private boolean format = true;/*** 是否写入日志文件*/private boolean writeInLog = true;@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 记录开始时间long startTime = System.currentTimeMillis();ThreadLocalUtil.set("start_time", startTime);}@Overridepublic void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) {// 记录开始时间long startTime = System.currentTimeMillis();ThreadLocalUtil.set("start_time", startTime);}@Overridepublic void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {// 可以在这里进行SQL预处理分析}@Overridepublic void afterQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {analyzePerformance(ms, boundSql, "SELECT");}@Overridepublic void afterUpdate(Executor executor, MappedStatement ms, Object parameter) {analyzePerformance(ms, null, "UPDATE");}/*** 分析SQL性能*/private void analyzePerformance(MappedStatement ms, BoundSql boundSql, String sqlType) {Long startTime = ThreadLocalUtil.get("start_time", Long.class);if (startTime == null) {return;}long endTime = System.currentTimeMillis();long executionTime = endTime - startTime;// 清理ThreadLocalThreadLocalUtil.remove("start_time");// 构建性能信息PerformanceInfo performanceInfo = PerformanceInfo.builder().sqlId(ms.getId()).sqlType(sqlType).executionTime(executionTime).sql(boundSql != null ? boundSql.getSql() : "").timestamp(new Date()).build();// 记录性能信息recordPerformance(performanceInfo);// 检查是否为慢SQLif (executionTime > slowSqlThreshold) {handleSlowSql(performanceInfo);}}/*** 记录性能信息*/private void recordPerformance(PerformanceInfo info) {if (writeInLog) {String formattedSql = format ? formatSql(info.getSql()) : info.getSql();log.info("SQL执行性能 - ID: {}, 类型: {}, 耗时: {}ms, SQL: {}", info.getSqlId(), info.getSqlType(), info.getExecutionTime(), formattedSql);}// 将性能信息存储到监控系统PerformanceMonitor.record(info);}/*** 处理慢SQL*/private void handleSlowSql(PerformanceInfo info) {log.warn("检测到慢SQL - ID: {}, 耗时: {}ms, SQL: {}", info.getSqlId(), info.getExecutionTime(), info.getSql());// 发送慢SQL告警SlowSqlAlertService.sendAlert(info);// 记录到慢SQL日志SlowSqlLogger.log(info);}/*** 格式化SQL*/private String formatSql(String sql) {if (sql == null || sql.trim().isEmpty()) {return sql;}try {return SQLUtils.formatMySql(sql);} catch (Exception e) {return sql;}}// Getter和Setter方法public void setSlowSqlThreshold(long slowSqlThreshold) {this.slowSqlThreshold = slowSqlThreshold;}public void setFormat(boolean format) {this.format = format;}public void setWriteInLog(boolean writeInLog) {this.writeInLog = writeInLog;}
}
性能监控数据模型
/*** 性能信息数据模型*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PerformanceInfo {/*** SQL ID*/private String sqlId;/*** SQL类型*/private String sqlType;/*** 执行时间(毫秒)*/private long executionTime;/*** SQL语句*/private String sql;/*** 执行时间戳*/private Date timestamp;/*** 参数信息*/private String parameters;/*** 影响行数*/private int affectedRows;/*** 数据库连接信息*/private String connectionInfo;/*** 线程信息*/private String threadInfo;
}/*** SQL统计信息*/
@Data
public class SqlStatistics {/*** SQL ID*/private String sqlId;/*** 执行次数*/private long executionCount;/*** 总执行时间*/private long totalExecutionTime;/*** 平均执行时间*/private double averageExecutionTime;/*** 最大执行时间*/private long maxExecutionTime;/*** 最小执行时间*/private long minExecutionTime;/*** 最后执行时间*/private Date lastExecutionTime;/*** 慢SQL次数*/private long slowSqlCount;/*** 错误次数*/private long errorCount;
}/*** 性能报告*/
@Data
public class PerformanceReport {/*** 报告生成时间*/private Date reportTime;/*** 统计时间范围*/private Date startTime;private Date endTime;/*** 总SQL执行次数*/private long totalSqlCount;/*** 慢SQL总数*/private long slowSqlCount;/*** 平均执行时间*/private double averageExecutionTime;/*** 最慢的SQL列表*/private List<PerformanceInfo> slowestSqls;/*** 执行最频繁的SQL列表*/private List<SqlStatistics> mostFrequentSqls;/*** 各类型SQL统计*/private Map<String, SqlStatistics> sqlTypeStatistics;
}
性能监控服务
/*** 性能监控服务*/
@Component
public class PerformanceMonitor {private static final Logger log = LoggerFactory.getLogger(PerformanceMonitor.class);/*** 性能数据存储(使用ConcurrentHashMap保证线程安全)*/private static final Map<String, SqlStatistics> SQL_STATISTICS = new ConcurrentHashMap<>();/*** 最近的性能记录(使用LRU缓存)*/private static final Map<String, PerformanceInfo> RECENT_PERFORMANCE = Collections.synchronizedMap(new LinkedHashMap<String, PerformanceInfo>(1000, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<String, PerformanceInfo> eldest) {return size() > 1000;}});/*** 慢SQL记录*/private static final List<PerformanceInfo> SLOW_SQL_RECORDS = Collections.synchronizedList(new ArrayList<>());/*** 记录性能信息*/public static void record(PerformanceInfo info) {// 更新SQL统计信息updateSqlStatistics(info);// 记录最近的性能信息String key = info.getSqlId() + "_" + System.currentTimeMillis();RECENT_PERFORMANCE.put(key, info);// 如果是慢SQL,记录到慢SQL列表if (info.getExecutionTime() > 1000) { // 1秒阈值SLOW_SQL_RECORDS.add(info);// 限制慢SQL记录数量if (SLOW_SQL_RECORDS.size() > 500) {SLOW_SQL_RECORDS.remove(0);}}}/*** 更新SQL统计信息*/private static void updateSqlStatistics(PerformanceInfo info) {SQL_STATISTICS.compute(info.getSqlId(), (key, statistics) -> {if (statistics == null) {statistics = new SqlStatistics();statistics.setSqlId(info.getSqlId());statistics.setExecutionCount(0);statistics.setTotalExecutionTime(0);statistics.setMaxExecutionTime(0);statistics.setMinExecutionTime(Long.MAX_VALUE);statistics.setSlowSqlCount(0);statistics.setErrorCount(0);}// 更新统计信息statistics.setExecutionCount(statistics.getExecutionCount() + 1);statistics.setTotalExecutionTime(statistics.getTotalExecutionTime() + info.getExecutionTime());statistics.setAverageExecutionTime((double) statistics.getTotalExecutionTime() / statistics.getExecutionCount());statistics.setMaxExecutionTime(Math.max(statistics.getMaxExecutionTime(), info.getExecutionTime()));statistics.setMinExecutionTime(Math.min(statistics.getMinExecutionTime(), info.getExecutionTime()));statistics.setLastExecutionTime(info.getTimestamp());if (info.getExecutionTime() > 1000) {statistics.setSlowSqlCount(statistics.getSlowSqlCount() + 1);}return statistics;});}/*** 获取SQL统计信息*/public static SqlStatistics getSqlStatistics(String sqlId) {return SQL_STATISTICS.get(sqlId);}/*** 获取所有SQL统计信息*/public static Map<String, SqlStatistics> getAllSqlStatistics() {return new HashMap<>(SQL_STATISTICS);}/*** 获取慢SQL记录*/public static List<PerformanceInfo> getSlowSqlRecords() {return new ArrayList<>(SLOW_SQL_RECORDS);}/*** 获取最近的性能记录*/public static List<PerformanceInfo> getRecentPerformance() {return new ArrayList<>(RECENT_PERFORMANCE.values());}/*** 生成性能报告*/public static PerformanceReport generateReport(Date startTime, Date endTime) {PerformanceReport report = new PerformanceReport();report.setReportTime(new Date());report.setStartTime(startTime);report.setEndTime(endTime);// 过滤时间范围内的数据List<PerformanceInfo> filteredRecords = RECENT_PERFORMANCE.values().stream().filter(info -> info.getTimestamp().after(startTime) && info.getTimestamp().before(endTime)).collect(Collectors.toList());// 计算总体统计report.setTotalSqlCount(filteredRecords.size());report.setSlowSqlCount(filteredRecords.stream().filter(info -> info.getExecutionTime() > 1000).count());report.setAverageExecutionTime(filteredRecords.stream().mapToLong(PerformanceInfo::getExecutionTime).average().orElse(0.0));// 获取最慢的SQL(前10个)report.setSlowestSqls(filteredRecords.stream().sorted((a, b) -> Long.compare(b.getExecutionTime(), a.getExecutionTime())).limit(10).collect(Collectors.toList()));// 获取执行最频繁的SQL(前10个)report.setMostFrequentSqls(SQL_STATISTICS.values().stream().sorted((a, b) -> Long.compare(b.getExecutionCount(), a.getExecutionCount())).limit(10).collect(Collectors.toList()));// 按SQL类型统计Map<String, SqlStatistics> typeStatistics = filteredRecords.stream().collect(Collectors.groupingBy(PerformanceInfo::getSqlType,Collectors.collectingAndThen(Collectors.toList(),list -> {SqlStatistics stats = new SqlStatistics();stats.setExecutionCount(list.size());stats.setTotalExecutionTime(list.stream().mapToLong(PerformanceInfo::getExecutionTime).sum());stats.setAverageExecutionTime(stats.getTotalExecutionTime() / (double) stats.getExecutionCount());stats.setMaxExecutionTime(list.stream().mapToLong(PerformanceInfo::getExecutionTime).max().orElse(0));stats.setMinExecutionTime(list.stream().mapToLong(PerformanceInfo::getExecutionTime).min().orElse(0));stats.setSlowSqlCount(list.stream().filter(info -> info.getExecutionTime() > 1000).count());return stats;})));report.setSqlTypeStatistics(typeStatistics);return report;}/*** 清理统计数据*/public static void clearStatistics() {SQL_STATISTICS.clear();RECENT_PERFORMANCE.clear();SLOW_SQL_RECORDS.clear();}/*** 清理过期数据*/public static void cleanupExpiredData(long retentionPeriodMs) {long cutoffTime = System.currentTimeMillis() - retentionPeriodMs;// 清理过期的慢SQL记录SLOW_SQL_RECORDS.removeIf(info -> info.getTimestamp().getTime() < cutoffTime);// 清理过期的最近性能记录RECENT_PERFORMANCE.entrySet().removeIf(entry -> entry.getValue().getTimestamp().getTime() < cutoffTime);}
}
慢SQL告警服务
/*** 慢SQL告警服务*/
@Service
public class SlowSqlAlertService {private static final Logger log = LoggerFactory.getLogger(SlowSqlAlertService.class);@Autowiredprivate NotificationService notificationService;@Value("${performance.alert.enabled:true}")private boolean alertEnabled;@Value("${performance.alert.threshold:2000}")private long alertThreshold;@Value("${performance.alert.interval:300000}") // 5分钟private long alertInterval;/*** 最后告警时间缓存*/private final Map<String, Long> lastAlertTime = new ConcurrentHashMap<>();/*** 发送慢SQL告警*/public void sendAlert(PerformanceInfo info) {if (!alertEnabled || info.getExecutionTime() < alertThreshold) {return;}String sqlId = info.getSqlId();long currentTime = System.currentTimeMillis();// 检查告警间隔Long lastAlert = lastAlertTime.get(sqlId);if (lastAlert != null && (currentTime - lastAlert) < alertInterval) {return; // 在告警间隔内,不重复发送}// 更新最后告警时间lastAlertTime.put(sqlId, currentTime);// 构建告警消息String alertMessage = buildAlertMessage(info);// 发送告警try {notificationService.sendAlert("慢SQL告警", alertMessage);log.info("慢SQL告警已发送: {}", sqlId);} catch (Exception e) {log.error("发送慢SQL告警失败", e);}}/*** 构建告警消息*/private String buildAlertMessage(PerformanceInfo info) {StringBuilder message = new StringBuilder();message.append("检测到慢SQL执行\n");message.append("SQL ID: ").append(info.getSqlId()).append("\n");message.append("执行时间: ").append(info.getExecutionTime()).append("ms\n");message.append("执行时间: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(info.getTimestamp())).append("\n");message.append("SQL语句: ").append(info.getSql()).append("\n");// 添加统计信息SqlStatistics statistics = PerformanceMonitor.getSqlStatistics(info.getSqlId());if (statistics != null) {message.append("历史统计:\n");message.append(" 执行次数: ").append(statistics.getExecutionCount()).append("\n");message.append(" 平均耗时: ").append(String.format("%.2f", statistics.getAverageExecutionTime())).append("ms\n");message.append(" 最大耗时: ").append(statistics.getMaxExecutionTime()).append("ms\n");message.append(" 慢SQL次数: ").append(statistics.getSlowSqlCount()).append("\n");}return message.toString();}
}/*** 慢SQL日志记录器*/
@Component
public class SlowSqlLogger {private static final Logger slowSqlLog = LoggerFactory.getLogger("SLOW_SQL");/*** 记录慢SQL日志*/public static void log(PerformanceInfo info) {try {String logMessage = String.format("SlowSQL - ID: %s, Time: %dms, SQL: %s, Timestamp: %s",info.getSqlId(),info.getExecutionTime(),info.getSql(),new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(info.getTimestamp()));slowSqlLog.warn(logMessage);} catch (Exception e) {LoggerFactory.getLogger(SlowSqlLogger.class).error("记录慢SQL日志失败", e);}}
}/*** 通知服务接口*/
public interface NotificationService {/*** 发送告警通知*/void sendAlert(String title, String message);
}/*** 邮件通知服务实现*/
@Service
public class EmailNotificationService implements NotificationService {@Autowiredprivate JavaMailSender mailSender;@Value("${performance.alert.email.to}")private String alertEmail;@Value("${performance.alert.email.from}")private String fromEmail;@Overridepublic void sendAlert(String title, String message) {try {SimpleMailMessage mailMessage = new SimpleMailMessage();mailMessage.setFrom(fromEmail);mailMessage.setTo(alertEmail);mailMessage.setSubject(title);mailMessage.setText(message);mailSender.send(mailMessage);} catch (Exception e) {throw new RuntimeException("发送邮件告警失败", e);}}
}
多租户插件
简要描述:多租户插件是MybatisPlus提供的一种数据隔离解决方案,支持在同一个数据库中为不同租户提供数据隔离,确保租户间数据的安全性和独立性。
核心概念:
- 租户隔离:通过租户ID实现数据隔离
- 透明过滤:自动在SQL中添加租户条件
- 多种策略:支持字段级、表级、库级隔离
- 动态切换:支持运行时动态切换租户
- 权限控制:结合权限系统实现细粒度控制
多租户插件配置
/*** 多租户插件配置*/
@Configuration
public class TenantConfig {/*** 配置多租户插件*/@Beanpublic MybatisPlusInterceptor tenantInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加多租户插件TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();tenantLineInnerInterceptor.setTenantLineHandler(new CustomTenantLineHandler());interceptor.addInnerInterceptor(tenantLineInnerInterceptor);return interceptor;}
}/*** 自定义租户处理器*/
public class CustomTenantLineHandler implements TenantLineHandler {/*** 租户字段名*/private static final String TENANT_ID_COLUMN = "tenant_id";/*** 需要排除多租户的表*/private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList("sys_user", // 系统用户表"sys_role", // 系统角色表"sys_permission", // 系统权限表"sys_config", // 系统配置表"sys_dict", // 数据字典表"sys_log", // 系统日志表"tenant_info" // 租户信息表));@Overridepublic Expression getTenantId() {// 从当前上下文获取租户IDString tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {throw new RuntimeException("当前上下文中未找到租户ID");}return new LongValue(tenantId);}@Overridepublic String getTenantIdColumn() {return TENANT_ID_COLUMN;}@Overridepublic boolean ignoreTable(String tableName) {// 检查是否需要忽略该表的多租户处理return IGNORE_TABLES.contains(tableName.toLowerCase());}@Overridepublic boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {// 检查插入操作是否需要忽略多租户处理return TenantContextHolder.isIgnoreTenant();}
}/*** 租户上下文持有者*/
public class TenantContextHolder {private static final ThreadLocal<String> TENANT_ID_HOLDER = new ThreadLocal<>();private static final ThreadLocal<Boolean> IGNORE_TENANT_HOLDER = new ThreadLocal<>();/*** 设置租户ID*/public static void setTenantId(String tenantId) {TENANT_ID_HOLDER.set(tenantId);}/*** 获取租户ID*/public static String getTenantId() {return TENANT_ID_HOLDER.get();}/*** 清除租户ID*/public static void clear() {TENANT_ID_HOLDER.remove();IGNORE_TENANT_HOLDER.remove();}/*** 设置忽略租户*/public static void setIgnoreTenant(boolean ignore) {IGNORE_TENANT_HOLDER.set(ignore);}/*** 是否忽略租户*/public static boolean isIgnoreTenant() {Boolean ignore = IGNORE_TENANT_HOLDER.get();return ignore != null && ignore;}/*** 在忽略租户的上下文中执行*/public static <T> T executeIgnoreTenant(Supplier<T> supplier) {boolean originalIgnore = isIgnoreTenant();try {setIgnoreTenant(true);return supplier.get();} finally {setIgnoreTenant(originalIgnore);}}/*** 在指定租户的上下文中执行*/public static <T> T executeWithTenant(String tenantId, Supplier<T> supplier) {String originalTenantId = getTenantId();try {setTenantId(tenantId);return supplier.get();} finally {if (originalTenantId != null) {setTenantId(originalTenantId);} else {clear();}}}
}
租户拦截器
/*** 租户拦截器 - 自动设置租户上下文*/
@Component
public class TenantInterceptor implements HandlerInterceptor {private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class);@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {try {// 从请求头获取租户IDString tenantId = extractTenantId(request);if (tenantId != null) {// 验证租户ID的有效性if (isValidTenant(tenantId)) {TenantContextHolder.setTenantId(tenantId);log.debug("设置租户上下文: {}", tenantId);} else {log.warn("无效的租户ID: {}", tenantId);response.setStatus(HttpStatus.FORBIDDEN.value());return false;}} else {// 对于某些不需要租户的接口,可以跳过if (requiresTenant(request)) {log.warn("请求缺少租户ID: {}", request.getRequestURI());response.setStatus(HttpStatus.BAD_REQUEST.value());return false;}}return true;} catch (Exception e) {log.error("处理租户拦截器时发生错误", e);response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());return false;}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {// 清理租户上下文TenantContextHolder.clear();}/*** 从请求中提取租户ID*/private String extractTenantId(HttpServletRequest request) {// 优先从请求头获取String tenantId = request.getHeader("X-Tenant-Id");if (tenantId == null) {// 从请求参数获取tenantId = request.getParameter("tenantId");}if (tenantId == null) {// 从JWT Token中获取tenantId = extractTenantFromToken(request);}return tenantId;}/*** 从JWT Token中提取租户ID*/private String extractTenantFromToken(HttpServletRequest request) {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {try {// 解析JWT Token获取租户IDString jwt = token.substring(7);// 这里需要根据实际的JWT解析逻辑来实现return JwtUtils.extractTenantId(jwt);} catch (Exception e) {log.warn("解析JWT Token失败", e);}}return null;}/*** 验证租户ID是否有效*/private boolean isValidTenant(String tenantId) {// 这里可以查询数据库验证租户是否存在且有效return TenantService.isValidTenant(tenantId);}/*** 判断请求是否需要租户*/private boolean requiresTenant(HttpServletRequest request) {String uri = request.getRequestURI();// 系统管理接口不需要租户if (uri.startsWith("/admin/") || uri.startsWith("/system/")) {return false;}// 公共接口不需要租户if (uri.startsWith("/public/") || uri.startsWith("/health")) {return false;}// 其他接口需要租户return true;}
}/*** 租户服务*/
@Service
public class TenantService {@Autowiredprivate TenantInfoMapper tenantInfoMapper;private static final Map<String, TenantInfo> TENANT_CACHE = new ConcurrentHashMap<>();/*** 验证租户是否有效*/public static boolean isValidTenant(String tenantId) {if (tenantId == null || tenantId.trim().isEmpty()) {return false;}// 先从缓存获取TenantInfo tenantInfo = TENANT_CACHE.get(tenantId);if (tenantInfo == null) {// 从数据库查询(忽略租户过滤)tenantInfo = TenantContextHolder.executeIgnoreTenant(() -> {return SpringContextHolder.getBean(TenantInfoMapper.class).selectOne(new QueryWrapper<TenantInfo>().eq("tenant_id", tenantId).eq("status", 1)); // 1表示启用状态});if (tenantInfo != null) {TENANT_CACHE.put(tenantId, tenantInfo);}}return tenantInfo != null && tenantInfo.getStatus() == 1;}/*** 获取租户信息*/public TenantInfo getTenantInfo(String tenantId) {return TenantContextHolder.executeIgnoreTenant(() -> {return tenantInfoMapper.selectOne(new QueryWrapper<TenantInfo>().eq("tenant_id", tenantId));});}/*** 创建租户*/@Transactionalpublic void createTenant(TenantInfo tenantInfo) {TenantContextHolder.executeIgnoreTenant(() -> {tenantInfoMapper.insert(tenantInfo);return null;});// 清除缓存TENANT_CACHE.remove(tenantInfo.getTenantId());}/*** 更新租户状态*/@Transactionalpublic void updateTenantStatus(String tenantId, Integer status) {TenantContextHolder.executeIgnoreTenant(() -> {tenantInfoMapper.update(null, new UpdateWrapper<TenantInfo>().eq("tenant_id", tenantId).set("status", status));return null;});// 清除缓存TENANT_CACHE.remove(tenantId);}
}
租户实体类
/*** 租户信息实体*/
@Data
@TableName("tenant_info")
public class TenantInfo {@TableId(type = IdType.AUTO)private Long id;/*** 租户ID*/@TableField("tenant_id")private String tenantId;/*** 租户名称*/@TableField("tenant_name")private String tenantName;/*** 租户类型*/@TableField("tenant_type")private String tenantType;/*** 联系人*/@TableField("contact_person")private String contactPerson;/*** 联系电话*/@TableField("contact_phone")private String contactPhone;/*** 联系邮箱*/@TableField("contact_email")private String contactEmail;/*** 状态:1-启用,0-禁用*/@TableField("status")private Integer status;/*** 创建时间*/@TableField(value = "create_time", fill = FieldFill.INSERT)private Date createTime;/*** 更新时间*/@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)private Date updateTime;/*** 过期时间*/@TableField("expire_time")private Date expireTime;/*** 备注*/@TableField("remark")private String remark;
}/*** 基础租户实体类*/
@Data
public abstract class BaseTenantEntity {/*** 租户ID*/@TableField("tenant_id")private String tenantId;/*** 创建时间*/@TableField(value = "create_time", fill = FieldFill.INSERT)private Date createTime;/*** 更新时间*/@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)private Date updateTime;/*** 创建人*/@TableField(value = "create_by", fill = FieldFill.INSERT)private String createBy;/*** 更新人*/@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)private String updateBy;
}/*** 用户实体(支持多租户)*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseTenantEntity {@TableId(type = IdType.AUTO)private Long id;/*** 用户名*/@TableField("username")private String username;/*** 密码*/@TableField("password")private String password;/*** 真实姓名*/@TableField("real_name")private String realName;/*** 邮箱*/@TableField("email")private String email;/*** 手机号*/@TableField("phone")private String phone;/*** 状态:1-启用,0-禁用*/@TableField("status")private Integer status;/*** 逻辑删除*/@TableLogic@TableField("deleted")private Integer deleted;
}/*** 订单实体(支持多租户)*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_order")
public class Order extends BaseTenantEntity {@TableId(type = IdType.AUTO)private Long id;/*** 订单号*/@TableField("order_no")private String orderNo;/*** 用户ID*/@TableField("user_id")private Long userId;/*** 订单金额*/@TableField("amount")private BigDecimal amount;/*** 订单状态*/@TableField("status")private Integer status;/*** 逻辑删除*/@TableLogic@TableField("deleted")private Integer deleted;
}
数据权限插件
简要描述:数据权限插件是MybatisPlus提供的一种细粒度数据访问控制解决方案,可以根据用户角色、部门、数据范围等条件自动过滤查询结果,确保用户只能访问有权限的数据。
核心概念:
- 数据范围控制:基于用户权限限制数据访问范围
- 动态SQL过滤:自动在SQL中添加权限过滤条件
- 多维度权限:支持部门、角色、用户等多种权限维度
- 灵活配置:支持注解和配置文件两种配置方式
- 权限缓存:提供权限信息缓存机制提升性能
数据权限插件配置
/*** 数据权限插件配置*/
@Configuration
public class DataPermissionConfig {/*** 配置数据权限插件*/@Beanpublic MybatisPlusInterceptor dataPermissionInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加数据权限插件DataPermissionInnerInterceptor dataPermissionInnerInterceptor = new DataPermissionInnerInterceptor();dataPermissionInnerInterceptor.setDataPermissionHandler(new CustomDataPermissionHandler());interceptor.addInnerInterceptor(dataPermissionInnerInterceptor);return interceptor;}
}/*** 自定义数据权限处理器*/
public class CustomDataPermissionHandler implements DataPermissionHandler {private static final Logger log = LoggerFactory.getLogger(CustomDataPermissionHandler.class);@Overridepublic Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {try {// 获取当前用户信息UserInfo currentUser = SecurityContextHolder.getCurrentUser();if (currentUser == null) {return where;}// 获取表名String tableName = table.getName();// 检查是否需要数据权限过滤if (!needDataPermission(tableName, mappedStatementId)) {return where;}// 构建数据权限条件Expression dataPermissionCondition = buildDataPermissionCondition(tableName, currentUser);if (dataPermissionCondition != null) {if (where != null) {return new AndExpression(where, dataPermissionCondition);} else {return dataPermissionCondition;}}return where;} catch (Exception e) {log.error("处理数据权限时发生错误", e);return where;}}/*** 检查是否需要数据权限过滤*/private boolean needDataPermission(String tableName, String mappedStatementId) {// 系统表不需要数据权限过滤if (tableName.startsWith("sys_")) {return false;}// 检查方法是否有@DataPermission注解return DataPermissionAnnotationHelper.hasDataPermissionAnnotation(mappedStatementId);}/*** 构建数据权限条件*/private Expression buildDataPermissionCondition(String tableName, UserInfo userInfo) {List<Expression> conditions = new ArrayList<>();// 根据用户的数据权限范围构建条件DataScope dataScope = userInfo.getDataScope();switch (dataScope) {case ALL:// 全部数据权限,不添加任何条件return null;case CUSTOM:// 自定义数据权限Expression customCondition = buildCustomDataPermission(tableName, userInfo);if (customCondition != null) {conditions.add(customCondition);}break;case DEPT:// 部门数据权限Expression deptCondition = buildDeptDataPermission(tableName, userInfo);if (deptCondition != null) {conditions.add(deptCondition);}break;case DEPT_AND_CHILD:// 部门及以下数据权限Expression deptAndChildCondition = buildDeptAndChildDataPermission(tableName, userInfo);if (deptAndChildCondition != null) {conditions.add(deptAndChildCondition);}break;case SELF:// 仅本人数据权限Expression selfCondition = buildSelfDataPermission(tableName, userInfo);if (selfCondition != null) {conditions.add(selfCondition);}break;default:// 默认无权限return new EqualsTo(new LongValue(1), new LongValue(0));}// 合并所有条件if (conditions.isEmpty()) {return null;} else if (conditions.size() == 1) {return conditions.get(0);} else {Expression result = conditions.get(0);for (int i = 1; i < conditions.size(); i++) {result = new OrExpression(result, conditions.get(i));}return result;}}/*** 构建自定义数据权限条件*/private Expression buildCustomDataPermission(String tableName, UserInfo userInfo) {// 获取用户的自定义数据权限配置List<DataPermissionRule> rules = DataPermissionService.getCustomRules(userInfo.getUserId(), tableName);if (rules.isEmpty()) {return null;}List<Expression> ruleConditions = new ArrayList<>();for (DataPermissionRule rule : rules) {Expression ruleCondition = buildRuleCondition(rule);if (ruleCondition != null) {ruleConditions.add(ruleCondition);}}if (ruleConditions.isEmpty()) {return null;} else if (ruleConditions.size() == 1) {return ruleConditions.get(0);} else {Expression result = ruleConditions.get(0);for (int i = 1; i < ruleConditions.size(); i++) {result = new OrExpression(result, ruleConditions.get(i));}return result;}}/*** 构建部门数据权限条件*/private Expression buildDeptDataPermission(String tableName, UserInfo userInfo) {// 检查表是否有部门字段String deptColumn = getDeptColumn(tableName);if (deptColumn == null) {return null;}return new EqualsTo(new Column(deptColumn),new LongValue(userInfo.getDeptId()));}/*** 构建部门及子部门数据权限条件*/private Expression buildDeptAndChildDataPermission(String tableName, UserInfo userInfo) {// 检查表是否有部门字段String deptColumn = getDeptColumn(tableName);if (deptColumn == null) {return null;}// 获取当前部门及所有子部门IDList<Long> deptIds = DeptService.getDeptAndChildIds(userInfo.getDeptId());if (deptIds.isEmpty()) {return null;}// 构建IN条件InExpression inExpression = new InExpression();inExpression.setLeftExpression(new Column(deptColumn));ExpressionList expressionList = new ExpressionList();List<Expression> expressions = deptIds.stream().map(LongValue::new).collect(Collectors.toList());expressionList.setExpressions(expressions);inExpression.setRightItemsList(expressionList);return inExpression;}/*** 构建个人数据权限条件*/private Expression buildSelfDataPermission(String tableName, UserInfo userInfo) {// 检查表是否有创建人字段String createByColumn = getCreateByColumn(tableName);if (createByColumn == null) {return null;}return new EqualsTo(new Column(createByColumn),new LongValue(userInfo.getUserId()));}/*** 构建规则条件*/private Expression buildRuleCondition(DataPermissionRule rule) {String column = rule.getColumn();String operator = rule.getOperator();String value = rule.getValue();Column leftColumn = new Column(column);switch (operator.toUpperCase()) {case "=":case "EQ":return new EqualsTo(leftColumn, new StringValue(value));case "!=":case "NE":return new NotEqualsTo(leftColumn, new StringValue(value));case ">":case "GT":return new GreaterThan(leftColumn, new StringValue(value));case ">=":case "GE":return new GreaterThanEquals(leftColumn, new StringValue(value));case "<":case "LT":return new MinorThan(leftColumn, new StringValue(value));case "<=":case "LE":return new MinorThanEquals(leftColumn, new StringValue(value));case "IN":return buildInCondition(leftColumn, value);case "NOT IN":InExpression inExpression = (InExpression) buildInCondition(leftColumn, value);inExpression.setNot(true);return inExpression;case "LIKE":return new LikeExpression(leftColumn, new StringValue("%" + value + "%"));case "NOT LIKE":LikeExpression likeExpression = new LikeExpression(leftColumn, new StringValue("%" + value + "%"));likeExpression.setNot(true);return likeExpression;default:log.warn("不支持的操作符: {}", operator);return null;}}/*** 构建IN条件*/private Expression buildInCondition(Column column, String value) {String[] values = value.split(",");InExpression inExpression = new InExpression();inExpression.setLeftExpression(column);ExpressionList expressionList = new ExpressionList();List<Expression> expressions = Arrays.stream(values).map(String::trim).map(StringValue::new).collect(Collectors.toList());expressionList.setExpressions(expressions);inExpression.setRightItemsList(expressionList);return inExpression;}/*** 获取部门字段名*/private String getDeptColumn(String tableName) {// 可以从配置文件或数据库中获取表的部门字段映射Map<String, String> deptColumnMapping = DataPermissionConfig.getDeptColumnMapping();return deptColumnMapping.getOrDefault(tableName, "dept_id");}/*** 获取创建人字段名*/private String getCreateByColumn(String tableName) {// 可以从配置文件或数据库中获取表的创建人字段映射Map<String, String> createByColumnMapping = DataPermissionConfig.getCreateByColumnMapping();return createByColumnMapping.getOrDefault(tableName, "create_by");}
}
数据权限注解
/*** 数据权限注解*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {/*** 数据权限范围*/DataScope value() default DataScope.DEFAULT;/*** 部门字段名*/String deptAlias() default "";/*** 用户字段名*/String userAlias() default "";/*** 是否启用数据权限*/boolean enabled() default true;
}/*** 数据权限范围枚举*/
public enum DataScope {/*** 全部数据权限*/ALL("1", "全部数据权限"),/*** 自定义数据权限*/CUSTOM("2", "自定义数据权限"),/*** 部门数据权限*/DEPT("3", "部门数据权限"),/*** 部门及以下数据权限*/DEPT_AND_CHILD("4", "部门及以下数据权限"),/*** 仅本人数据权限*/SELF("5", "仅本人数据权限"),/*** 默认数据权限(根据用户角色确定)*/DEFAULT("0", "默认数据权限");private final String code;private final String description;DataScope(String code, String description) {this.code = code;this.description = description;}public String getCode() {return code;}public String getDescription() {return description;}
}/*** 数据权限注解助手*/
public class DataPermissionAnnotationHelper {private static final Map<String, Boolean> ANNOTATION_CACHE = new ConcurrentHashMap<>();/*** 检查方法是否有数据权限注解*/public static boolean hasDataPermissionAnnotation(String mappedStatementId) {return ANNOTATION_CACHE.computeIfAbsent(mappedStatementId, key -> {try {// 解析mappedStatementId获取类名和方法名int lastDotIndex = key.lastIndexOf('.');if (lastDotIndex == -1) {return false;}String className = key.substring(0, lastDotIndex);String methodName = key.substring(lastDotIndex + 1);// 获取类Class<?> clazz = Class.forName(className);// 检查类级别的注解if (clazz.isAnnotationPresent(DataPermission.class)) {DataPermission classAnnotation = clazz.getAnnotation(DataPermission.class);if (classAnnotation.enabled()) {return true;}}// 检查方法级别的注解Method[] methods = clazz.getDeclaredMethods();for (Method method : methods) {if (method.getName().equals(methodName) && method.isAnnotationPresent(DataPermission.class)) {DataPermission methodAnnotation = method.getAnnotation(DataPermission.class);return methodAnnotation.enabled();}}return false;} catch (Exception e) {return false;}});}/*** 获取数据权限注解*/public static DataPermission getDataPermissionAnnotation(String mappedStatementId) {try {// 解析mappedStatementId获取类名和方法名int lastDotIndex = mappedStatementId.lastIndexOf('.');if (lastDotIndex == -1) {return null;}String className = mappedStatementId.substring(0, lastDotIndex);String methodName = mappedStatementId.substring(lastDotIndex + 1);// 获取类Class<?> clazz = Class.forName(className);// 先检查方法级别的注解Method[] methods = clazz.getDeclaredMethods();for (Method method : methods) {if (method.getName().equals(methodName) && method.isAnnotationPresent(DataPermission.class)) {return method.getAnnotation(DataPermission.class);}}// 再检查类级别的注解if (clazz.isAnnotationPresent(DataPermission.class)) {return clazz.getAnnotation(DataPermission.class);}return null;} catch (Exception e) {return null;}}
}
数据权限实体类
/*** 用户信息*/
@Data
public class UserInfo {/*** 用户ID*/private Long userId;/*** 用户名*/private String username;/*** 部门ID*/private Long deptId;/*** 角色列表*/private List<RoleInfo> roles;/*** 数据权限范围*/private DataScope dataScope;/*** 权限列表*/private Set<String> permissions;
}/*** 角色信息*/
@Data
public class RoleInfo {/*** 角色ID*/private Long roleId;/*** 角色名称*/private String roleName;/*** 角色编码*/private String roleCode;/*** 数据权限范围*/private DataScope dataScope;
}/*** 数据权限规则*/
@Data
public class DataPermissionRule {/*** 规则ID*/private Long ruleId;/*** 表名*/private String tableName;/*** 字段名*/private String column;/*** 操作符*/private String operator;/*** 值*/private String value;/*** 规则类型*/private String ruleType;/*** 是否启用*/private Boolean enabled;
}/*** 部门信息*/
@Data
public class DeptInfo {/*** 部门ID*/private Long deptId;/*** 部门名称*/private String deptName;/*** 父部门ID*/private Long parentId;/*** 部门路径*/private String deptPath;/*** 排序*/private Integer orderNum;/*** 状态*/private Integer status;
}
数据权限服务层
/*** 数据权限服务*/
@Service
public class DataPermissionService {@Autowiredprivate DataPermissionRuleMapper dataPermissionRuleMapper;@Autowiredprivate DeptMapper deptMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String PERMISSION_CACHE_KEY = "data:permission:";private static final long CACHE_EXPIRE_TIME = 30; // 30分钟/*** 获取用户自定义数据权限规则*/public List<DataPermissionRule> getCustomRules(Long userId, String tableName) {String cacheKey = PERMISSION_CACHE_KEY + "custom:" + userId + ":" + tableName;// 先从缓存获取List<DataPermissionRule> cachedRules = (List<DataPermissionRule>) redisTemplate.opsForValue().get(cacheKey);if (cachedRules != null) {return cachedRules;}// 从数据库查询QueryWrapper<DataPermissionRule> queryWrapper = new QueryWrapper<>();queryWrapper.eq("user_id", userId).eq("table_name", tableName).eq("enabled", true).orderByAsc("rule_id");List<DataPermissionRule> rules = dataPermissionRuleMapper.selectList(queryWrapper);// 缓存结果redisTemplate.opsForValue().set(cacheKey, rules, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);return rules;}/*** 获取部门及所有子部门ID*/public List<Long> getDeptAndChildIds(Long deptId) {String cacheKey = PERMISSION_CACHE_KEY + "dept:" + deptId;// 先从缓存获取List<Long> cachedDeptIds = (List<Long>) redisTemplate.opsForValue().get(cacheKey);if (cachedDeptIds != null) {return cachedDeptIds;}// 从数据库查询List<Long> deptIds = new ArrayList<>();deptIds.add(deptId);// 递归查询所有子部门List<Long> childDeptIds = getChildDeptIds(deptId);deptIds.addAll(childDeptIds);// 缓存结果redisTemplate.opsForValue().set(cacheKey, deptIds, CACHE_EXPIRE_TIME, TimeUnit.MINUTES);return deptIds;}/*** 递归获取子部门ID*/private List<Long> getChildDeptIds(Long parentId) {List<Long> childIds = new ArrayList<>();QueryWrapper<DeptInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("parent_id", parentId).eq("status", 1); // 只查询启用的部门List<DeptInfo> childDepts = deptMapper.selectList(queryWrapper);for (DeptInfo dept : childDepts) {childIds.add(dept.getDeptId());// 递归查询子部门的子部门childIds.addAll(getChildDeptIds(dept.getDeptId()));}return childIds;}/*** 清除用户数据权限缓存*/public void clearUserPermissionCache(Long userId) {String pattern = PERMISSION_CACHE_KEY + "*:" + userId + ":*";Set<String> keys = redisTemplate.keys(pattern);if (!keys.isEmpty()) {redisTemplate.delete(keys);}}/*** 清除部门权限缓存*/public void clearDeptPermissionCache(Long deptId) {String cacheKey = PERMISSION_CACHE_KEY + "dept:" + deptId;redisTemplate.delete(cacheKey);// 同时清除父部门的缓存(因为父部门的子部门列表可能发生变化)DeptInfo dept = deptMapper.selectById(deptId);if (dept != null && dept.getParentId() != null) {clearDeptPermissionCache(dept.getParentId());}}
}/*** 安全上下文持有者*/
public class SecurityContextHolder {private static final ThreadLocal<UserInfo> USER_CONTEXT = new ThreadLocal<>();/*** 设置当前用户*/public static void setCurrentUser(UserInfo userInfo) {USER_CONTEXT.set(userInfo);}/*** 获取当前用户*/public static UserInfo getCurrentUser() {return USER_CONTEXT.get();}/*** 清除当前用户*/public static void clearCurrentUser() {USER_CONTEXT.remove();}/*** 获取当前用户ID*/public static Long getCurrentUserId() {UserInfo userInfo = getCurrentUser();return userInfo != null ? userInfo.getUserId() : null;}/*** 获取当前用户部门ID*/public static Long getCurrentDeptId() {UserInfo userInfo = getCurrentUser();return userInfo != null ? userInfo.getDeptId() : null;}/*** 获取当前用户数据权限范围*/public static DataScope getCurrentDataScope() {UserInfo userInfo = getCurrentUser();return userInfo != null ? userInfo.getDataScope() : DataScope.SELF;}
}/*** 数据权限配置*/
@Component
public class DataPermissionConfig {private static Map<String, String> deptColumnMapping = new HashMap<>();private static Map<String, String> createByColumnMapping = new HashMap<>();static {// 初始化部门字段映射deptColumnMapping.put("sys_user", "dept_id");deptColumnMapping.put("sys_dept", "dept_id");deptColumnMapping.put("sys_role", "dept_id");deptColumnMapping.put("business_order", "dept_id");deptColumnMapping.put("business_product", "dept_id");// 初始化创建人字段映射createByColumnMapping.put("sys_user", "create_by");createByColumnMapping.put("sys_dept", "create_by");createByColumnMapping.put("sys_role", "create_by");createByColumnMapping.put("business_order", "create_by");createByColumnMapping.put("business_product", "create_by");}/*** 获取部门字段映射*/public static Map<String, String> getDeptColumnMapping() {return deptColumnMapping;}/*** 获取创建人字段映射*/public static Map<String, String> getCreateByColumnMapping() {return createByColumnMapping;}/*** 添加部门字段映射*/public static void addDeptColumnMapping(String tableName, String columnName) {deptColumnMapping.put(tableName, columnName);}/*** 添加创建人字段映射*/public static void addCreateByColumnMapping(String tableName, String columnName) {createByColumnMapping.put(tableName, columnName);}
}
数据权限使用示例
/*** 用户服务层*/
@Service
@DataPermission(DataScope.DEPT_AND_CHILD) // 类级别数据权限
public class UserService extends ServiceImpl<UserMapper, User> {/*** 查询用户列表(自动应用数据权限)*/public List<User> getUserList(UserQueryDTO queryDTO) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();// 添加业务查询条件if (StringUtils.isNotBlank(queryDTO.getUsername())) {queryWrapper.like("username", queryDTO.getUsername());}if (StringUtils.isNotBlank(queryDTO.getNickname())) {queryWrapper.like("nickname", queryDTO.getNickname());}if (queryDTO.getStatus() != null) {queryWrapper.eq("status", queryDTO.getStatus());}// 数据权限会自动应用,无需手动添加权限条件return this.list(queryWrapper);}/*** 分页查询用户(自动应用数据权限)*/public IPage<User> getUserPage(Page<User> page, UserQueryDTO queryDTO) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();// 添加业务查询条件if (StringUtils.isNotBlank(queryDTO.getUsername())) {queryWrapper.like("username", queryDTO.getUsername());}if (StringUtils.isNotBlank(queryDTO.getNickname())) {queryWrapper.like("nickname", queryDTO.getNickname());}// 数据权限会自动应用return this.page(page, queryWrapper);}/*** 查询用户详情(仅本人数据权限)*/@DataPermission(DataScope.SELF)public User getUserDetail(Long userId) {return this.getById(userId);}/*** 查询所有用户(全部数据权限,通常只有管理员可以调用)*/@DataPermission(DataScope.ALL)public List<User> getAllUsers() {return this.list();}/*** 不应用数据权限的查询(通过enabled=false禁用)*/@DataPermission(enabled = false)public User getUserByUsername(String username) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username", username);return this.getOne(queryWrapper);}
}/*** 订单服务层*/
@Service
@DataPermission(DataScope.DEPT) // 部门数据权限
public class OrderService extends ServiceImpl<OrderMapper, Order> {/*** 查询订单列表*/public List<Order> getOrderList(OrderQueryDTO queryDTO) {QueryWrapper<Order> queryWrapper = new QueryWrapper<>();// 添加业务查询条件if (StringUtils.isNotBlank(queryDTO.getOrderNo())) {queryWrapper.like("order_no", queryDTO.getOrderNo());}if (queryDTO.getStatus() != null) {queryWrapper.eq("status", queryDTO.getStatus());}if (queryDTO.getStartDate() != null) {queryWrapper.ge("create_time", queryDTO.getStartDate());}if (queryDTO.getEndDate() != null) {queryWrapper.le("create_time", queryDTO.getEndDate());}return this.list(queryWrapper);}/*** 查询我的订单(仅本人数据权限)*/@DataPermission(DataScope.SELF)public List<Order> getMyOrders() {return this.list();}/*** 管理员查询所有订单(全部数据权限)*/@DataPermission(DataScope.ALL)public List<Order> getAllOrdersForAdmin() {return this.list();}
}/*** 控制器层*/
@RestController
@RequestMapping("/api/user")
public class UserController {@Autowiredprivate UserService userService;/*** 查询用户列表*/@GetMapping("/list")public Result<List<User>> getUserList(UserQueryDTO queryDTO) {List<User> users = userService.getUserList(queryDTO);return Result.success(users);}/*** 分页查询用户*/@GetMapping("/page")public Result<IPage<User>> getUserPage(@RequestParam(defaultValue = "1") Integer current,@RequestParam(defaultValue = "10") Integer size,UserQueryDTO queryDTO) {Page<User> page = new Page<>(current, size);IPage<User> userPage = userService.getUserPage(page, queryDTO);return Result.success(userPage);}/*** 查询用户详情*/@GetMapping("/{id}")public Result<User> getUserDetail(@PathVariable Long id) {User user = userService.getUserDetail(id);return Result.success(user);}
}
数据权限拦截器
/*** 数据权限拦截器*/
@Component
public class DataPermissionInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从请求中获取用户信息(通常从JWT token或session中获取)String token = request.getHeader("Authorization");if (StringUtils.isNotBlank(token)) {// 解析token获取用户信息UserInfo userInfo = parseUserFromToken(token);if (userInfo != null) {// 设置到安全上下文SecurityContextHolder.setCurrentUser(userInfo);}}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 清除安全上下文SecurityContextHolder.clearCurrentUser();}/*** 从token解析用户信息*/private UserInfo parseUserFromToken(String token) {try {// 这里实现具体的token解析逻辑// 示例代码,实际需要根据具体的认证方案实现// 去掉Bearer前缀if (token.startsWith("Bearer ")) {token = token.substring(7);}// 解析JWT token(示例)Claims claims = Jwts.parser().setSigningKey("your-secret-key").parseClaimsJws(token).getBody();Long userId = Long.valueOf(claims.getSubject());// 从数据库或缓存中获取完整的用户信息User user = userService.getById(userId);if (user != null) {UserInfo userInfo = new UserInfo();userInfo.setUserId(user.getId());userInfo.setUsername(user.getUsername());userInfo.setDeptId(user.getDeptId());// 获取用户的数据权限范围(从角色或用户配置中获取)DataScope dataScope = getUserDataScope(userId);userInfo.setDataScope(dataScope);return userInfo;}} catch (Exception e) {// token解析失败,返回null}return null;}/*** 获取用户数据权限范围*/private DataScope getUserDataScope(Long userId) {// 这里实现获取用户数据权限范围的逻辑// 可以从用户角色、权限配置等地方获取// 示例:从用户角色中获取最高权限List<RoleInfo> roles = userService.getUserRoles(userId);DataScope maxDataScope = DataScope.SELF; // 默认最小权限for (RoleInfo role : roles) {if (role.getDataScope().ordinal() < maxDataScope.ordinal()) {maxDataScope = role.getDataScope();}}return maxDataScope;}
}/*** Web配置*/
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate DataPermissionInterceptor dataPermissionInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(dataPermissionInterceptor).addPathPatterns("/api/**") // 拦截所有API请求.excludePathPatterns("/api/auth/**"); // 排除认证相关接口}
}
数据权限最佳实践
/*** 数据权限最佳实践*/
public class DataPermissionBestPractices {/*** 1. 性能优化建议*/public void performanceOptimization() {// a) 使用缓存减少数据库查询// b) 合理设置缓存过期时间// c) 避免在循环中查询权限信息// d) 使用批量查询减少数据库访问次数}/*** 2. 安全性考虑*/public void securityConsiderations() {// a) 默认拒绝策略:没有明确权限时拒绝访问// b) 权限检查前置:在业务逻辑执行前进行权限检查// c) 敏感操作审计:记录重要的数据访问操作// d) 权限变更通知:权限变更时及时清除相关缓存}/*** 3. 可维护性建议*/public void maintainabilityAdvice() {// a) 统一权限配置:使用配置文件或数据库统一管理权限规则// b) 权限测试:编写单元测试验证权限逻辑// c) 文档完善:详细记录权限设计和使用方法// d) 监控告警:监控权限异常和性能问题}/*** 4. 扩展性设计*/public void extensibilityDesign() {// a) 插件化设计:支持自定义权限处理器// b) 多维度权限:支持按地区、项目等多种维度控制// c) 动态权限:支持运行时动态调整权限规则// d) 权限继承:支持权限的继承和覆盖机制}
}
自定义插件开发
简要描述:MyBatis-Plus支持自定义插件开发,通过实现Interceptor接口可以在SQL执行的各个阶段进行拦截和处理。
核心概念:
- 拦截器机制:基于MyBatis的插件机制实现
- 执行阶段:可以拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler
- 责任链模式:多个插件按顺序执行
插件开发步骤:
// 1. 实现Interceptor接口
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class CustomInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取执行参数Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];// 前置处理System.out.println("执行SQL: " + ms.getId());long startTime = System.currentTimeMillis();try {// 执行原方法Object result = invocation.proceed();// 后置处理long endTime = System.currentTimeMillis();System.out.println("执行耗时: " + (endTime - startTime) + "ms");return result;} catch (Exception e) {// 异常处理System.err.println("SQL执行异常: " + e.getMessage());throw e;}}@Overridepublic Object plugin(Object target) {// 判断是否需要拦截if (target instanceof Executor) {return Plugin.wrap(target, this);}return target;}@Overridepublic void setProperties(Properties properties) {// 设置插件属性String logLevel = properties.getProperty("logLevel", "INFO");System.out.println("插件日志级别: " + logLevel);}
}
常用插件类型:
SQL性能监控插件
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PerformanceInterceptor implements Interceptor {private long maxTime = 1000; // 最大执行时间(毫秒)@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());// 获取SQL语句BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");String sql = boundSql.getSql();long startTime = System.currentTimeMillis();Object result = invocation.proceed();long endTime = System.currentTimeMillis();long executeTime = endTime - startTime;if (executeTime > maxTime) {System.err.println("慢SQL警告: 执行时间 " + executeTime + "ms");System.err.println("SQL语句: " + sql);}return result;}
}
数据权限插件
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataPermissionInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0];Object parameter = args[1];// 获取当前用户信息UserContext userContext = getCurrentUser();if (userContext == null) {return invocation.proceed();}// 修改SQL添加数据权限条件BoundSql boundSql = ms.getBoundSql(parameter);String originalSql = boundSql.getSql();String modifiedSql = addDataPermissionCondition(originalSql, userContext);// 创建新的BoundSqlBoundSql newBoundSql = new BoundSql(ms.getConfiguration(), modifiedSql, boundSql.getParameterMappings(), parameter);// 替换原有的BoundSqlMetaObject msObject = MetaObject.forObject(ms, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());msObject.setValue("sqlSource.boundSql", newBoundSql);return invocation.proceed();}private String addDataPermissionCondition(String sql, UserContext userContext) {// 根据用户权限添加WHERE条件if (userContext.getRole().equals("ADMIN")) {return sql; // 管理员可以查看所有数据} else {// 普通用户只能查看自己的数据return sql + " AND user_id = " + userContext.getUserId();}}
}
敏感数据脱敏插件
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DataMaskingInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 执行原方法获取结果Object result = invocation.proceed();// 对结果进行脱敏处理if (result instanceof List) {List<?> list = (List<?>) result;for (Object item : list) {maskSensitiveData(item);}} else if (result != null) {maskSensitiveData(result);}return result;}private void maskSensitiveData(Object obj) {if (obj == null) return;Class<?> clazz = obj.getClass();Field[] fields = clazz.getDeclaredFields();for (Field field : fields) {// 检查是否有脱敏注解if (field.isAnnotationPresent(SensitiveData.class)) {try {field.setAccessible(true);Object value = field.get(obj);if (value instanceof String) {String maskedValue = maskString((String) value, field.getAnnotation(SensitiveData.class).type());field.set(obj, maskedValue);}} catch (IllegalAccessException e) {e.printStackTrace();}}}}private String maskString(String value, SensitiveType type) {if (value == null || value.length() == 0) {return value;}switch (type) {case PHONE:return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");case EMAIL:return value.replaceAll("(\\w+)@(\\w+)", "***@$2");case ID_CARD:return value.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");default:return "***";}}
}// 脱敏注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {SensitiveType type();
}public enum SensitiveType {PHONE, EMAIL, ID_CARD, NAME
}
插件注册方式:
// 1. 配置类注册
@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加自定义插件interceptor.addInnerInterceptor(new CustomInterceptor());interceptor.addInnerInterceptor(new PerformanceInterceptor());interceptor.addInnerInterceptor(new DataPermissionInterceptor());return interceptor;}
}// 2. XML配置注册
// 在mybatis-config.xml中配置
<plugins><plugin interceptor="com.example.interceptor.CustomInterceptor"><property name="logLevel" value="DEBUG"/></plugin>
</plugins>
插件开发最佳实践:
- 性能考虑:避免在插件中执行耗时操作
- 异常处理:妥善处理插件中的异常,避免影响主流程
- 配置灵活:通过Properties支持插件配置
- 职责单一:每个插件只负责一个特定功能
- 测试充分:编写完整的单元测试和集成测试
多数据源配置
简要描述:在实际项目中,经常需要连接多个数据库或实现读写分离,MyBatis-Plus提供了灵活的多数据源配置方案。
核心概念:
- 数据源路由:根据业务逻辑动态选择数据源
- 读写分离:查询操作使用从库,写操作使用主库
- 分库分表:将数据分散到多个数据库和表中
- 事务一致性:保证跨数据源操作的事务一致性
动态数据源配置
简要描述:动态数据源允许在运行时根据业务需求切换不同的数据库连接,常用于多租户系统或分库场景。
核心概念:
- 数据源路由器:负责选择合适的数据源
- 线程本地存储:使用ThreadLocal存储当前数据源标识
- AOP切面:通过注解自动切换数据源
基础配置:
# application.yml
spring:datasource:dynamic:primary: master # 设置默认的数据源或者数据源组strict: false # 严格匹配数据源,默认falsedatasource:master:url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8username: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driverslave_1:url: jdbc:mysql://localhost:3306/slave1_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8username: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driverslave_2:url: jdbc:mysql://localhost:3306/slave2_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8username: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driver
Java配置方式:
@Configuration
public class DataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.slave")public DataSource slaveDataSource() {return DruidDataSourceBuilder.create().build();}@Bean@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("master", masterDataSource);targetDataSources.put("slave", slaveDataSource);DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource);return dynamicDataSource;}
}// 自定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSource();}
}// 数据源上下文持有者
public class DataSourceContextHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setDataSource(String dataSource) {contextHolder.set(dataSource);}public static String getDataSource() {return contextHolder.get();}public static void clearDataSource() {contextHolder.remove();}
}
注解方式切换数据源:
// 数据源切换注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String value() default "master";
}// AOP切面处理
@Aspect
@Component
@Order(1) // 确保在事务注解之前执行
public class DataSourceAspect {@Pointcut("@annotation(com.example.annotation.DataSource)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class);if (dataSource != null) {DataSourceContextHolder.setDataSource(dataSource.value());}try {return point.proceed();} finally {DataSourceContextHolder.clearDataSource();}}
}
使用示例:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 使用主库@DataSource("master")public void saveUser(User user) {userMapper.insert(user);}// 使用从库@DataSource("slave")public List<User> findUsers() {return userMapper.selectList(null);}// 使用默认数据源public User findById(Long id) {return userMapper.selectById(id);}
}
读写分离实现
简要描述:读写分离是一种常见的数据库优化策略,将读操作分配到从库,写操作分配到主库,提高系统性能和可用性。
核心概念:
- 主从复制:主库负责写操作,从库同步主库数据
- 负载均衡:多个从库之间的负载均衡
- 延迟处理:处理主从同步延迟问题
自动读写分离配置:
@Component
@Order(0)
public class ReadWriteSplitAspect {@Pointcut("execution(* com.example.service.*.select*(..)) || "+ "execution(* com.example.service.*.find*(..)) || "+ "execution(* com.example.service.*.get*(..)) || "+ "execution(* com.example.service.*.query*(..))")public void readOperation() {}@Pointcut("execution(* com.example.service.*.insert*(..)) || "+ "execution(* com.example.service.*.save*(..)) || "+ "execution(* com.example.service.*.update*(..)) || "+ "execution(* com.example.service.*.delete*(..))")public void writeOperation() {}@Before("readOperation()")public void setReadDataSource() {// 随机选择一个从库String[] slaves = {"slave_1", "slave_2"};String slave = slaves[new Random().nextInt(slaves.length)];DataSourceContextHolder.setDataSource(slave);}@Before("writeOperation()")public void setWriteDataSource() {DataSourceContextHolder.setDataSource("master");}
}
强制主库注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Master {
}@Aspect
@Component
public class MasterAspect {@Pointcut("@annotation(com.example.annotation.Master)")public void masterPointCut() {}@Before("masterPointCut()")public void setMasterDataSource() {DataSourceContextHolder.setDataSource("master");}
}// 使用示例
@Service
public class OrderService {// 强制使用主库查询,避免主从延迟问题@Masterpublic Order findOrderAfterCreate(Long orderId) {return orderMapper.selectById(orderId);}
}
负载均衡策略:
public interface LoadBalancer {String select(List<String> servers);
}// 轮询策略
@Component
public class RoundRobinLoadBalancer implements LoadBalancer {private final AtomicInteger counter = new AtomicInteger(0);@Overridepublic String select(List<String> servers) {if (servers.isEmpty()) {return null;}int index = counter.getAndIncrement() % servers.size();return servers.get(index);}
}// 随机策略
@Component
public class RandomLoadBalancer implements LoadBalancer {private final Random random = new Random();@Overridepublic String select(List<String> servers) {if (servers.isEmpty()) {return null;}return servers.get(random.nextInt(servers.size()));}
}// 权重策略
@Component
public class WeightedLoadBalancer implements LoadBalancer {@Overridepublic String select(List<String> servers) {// 根据服务器权重选择Map<String, Integer> weights = getServerWeights();int totalWeight = weights.values().stream().mapToInt(Integer::intValue).sum();int randomWeight = new Random().nextInt(totalWeight);int currentWeight = 0;for (Map.Entry<String, Integer> entry : weights.entrySet()) {currentWeight += entry.getValue();if (randomWeight < currentWeight) {return entry.getKey();}}return servers.get(0); // 默认返回第一个}private Map<String, Integer> getServerWeights() {Map<String, Integer> weights = new HashMap<>();weights.put("slave_1", 3); // 权重3weights.put("slave_2", 1); // 权重1return weights;}
}
分库分表策略
简要描述:当单个数据库无法承载大量数据时,需要将数据分散到多个数据库和表中,这就是分库分表策略。
核心概念:
- 水平分库:按照某种规则将数据分散到不同数据库
- 水平分表:将同一个表的数据分散到多个表中
- 垂直分库:按业务模块分离到不同数据库
- 分片键:用于确定数据存储位置的字段
分库分表配置:
// 分片策略接口
public interface ShardingStrategy {String determineDataSource(Object shardingValue);String determineTable(Object shardingValue);
}// 用户ID分片策略
@Component
public class UserShardingStrategy implements ShardingStrategy {private static final int DB_COUNT = 4; // 4个数据库private static final int TABLE_COUNT = 8; // 每个库8张表@Overridepublic String determineDataSource(Object shardingValue) {Long userId = (Long) shardingValue;int dbIndex = (int) (userId % DB_COUNT);return "db_" + dbIndex;}@Overridepublic String determineTable(Object shardingValue) {Long userId = (Long) shardingValue;int tableIndex = (int) (userId % TABLE_COUNT);return "user_" + tableIndex;}
}// 分片注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sharding {String strategy() default "";String shardingKey() default "";
}// 分片切面
@Aspect
@Component
public class ShardingAspect {@Autowiredprivate Map<String, ShardingStrategy> strategies;@Around("@annotation(sharding)")public Object around(ProceedingJoinPoint point, Sharding sharding) throws Throwable {// 获取分片键值Object shardingValue = getShardingValue(point, sharding.shardingKey());// 获取分片策略ShardingStrategy strategy = strategies.get(sharding.strategy());if (strategy != null && shardingValue != null) {// 设置数据源String dataSource = strategy.determineDataSource(shardingValue);DataSourceContextHolder.setDataSource(dataSource);// 设置表名(需要配合动态表名插件)String tableName = strategy.determineTable(shardingValue);TableNameContextHolder.setTableName(tableName);}try {return point.proceed();} finally {DataSourceContextHolder.clearDataSource();TableNameContextHolder.clearTableName();}}private Object getShardingValue(ProceedingJoinPoint point, String shardingKey) {// 从方法参数中提取分片键值Object[] args = point.getArgs();MethodSignature signature = (MethodSignature) point.getSignature();String[] paramNames = signature.getParameterNames();for (int i = 0; i < paramNames.length; i++) {if (shardingKey.equals(paramNames[i])) {return args[i];}}// 如果参数是对象,尝试从对象属性中获取for (Object arg : args) {if (arg != null) {try {Field field = arg.getClass().getDeclaredField(shardingKey);field.setAccessible(true);return field.get(arg);} catch (Exception e) {// 忽略异常,继续查找}}}return null;}
}
动态表名插件:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DynamicTableNameInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = MetaObject.forObject(statementHandler,SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");String sql = boundSql.getSql();// 替换表名String tableName = TableNameContextHolder.getTableName();if (tableName != null) {sql = sql.replaceAll("user", tableName);metaObject.setValue("delegate.boundSql.sql", sql);}return invocation.proceed();}
}// 表名上下文持有者
public class TableNameContextHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setTableName(String tableName) {contextHolder.set(tableName);}public static String getTableName() {return contextHolder.get();}public static void clearTableName() {contextHolder.remove();}
}
使用示例:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Sharding(strategy = "userShardingStrategy", shardingKey = "userId")public User findUserById(Long userId) {return userMapper.selectById(userId);}@Sharding(strategy = "userShardingStrategy", shardingKey = "id")public void saveUser(User user) {userMapper.insert(user);}
}
事务管理
简要描述:在多数据源环境下,事务管理变得复杂,需要考虑分布式事务、跨数据源事务等问题。
核心概念:
- 本地事务:单个数据源内的事务
- 分布式事务:跨多个数据源的事务
- 两阶段提交:分布式事务的经典解决方案
- 最终一致性:通过补偿机制保证数据最终一致
多数据源事务配置:
@Configuration
@EnableTransactionManagement
public class TransactionConfig {@Beanpublic PlatformTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}@Beanpublic PlatformTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}// JTA事务管理器(用于分布式事务)@Beanpublic JtaTransactionManager jtaTransactionManager() {JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();jtaTransactionManager.setTransactionManager(atomikosTransactionManager());jtaTransactionManager.setUserTransaction(atomikosUserTransaction());return jtaTransactionManager;}@Beanpublic TransactionManager atomikosTransactionManager() {UserTransactionManager userTransactionManager = new UserTransactionManager();userTransactionManager.setForceShutdown(false);return userTransactionManager;}@Beanpublic UserTransaction atomikosUserTransaction() {UserTransactionImp userTransactionImp = new UserTransactionImp();userTransactionImp.setTransactionTimeout(300);return userTransactionImp;}
}
事务传播策略:
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate PaymentService paymentService;// 主事务@Transactional(transactionManager = "masterTransactionManager")public void createOrder(Order order) {// 保存订单orderMapper.insert(order);// 调用支付服务(新事务)paymentService.processPayment(order.getId(), order.getAmount());}
}@Service
public class PaymentService {@Autowiredprivate PaymentMapper paymentMapper;// 独立事务,即使主事务回滚,支付记录也会保存@Transactional(transactionManager = "slaveTransactionManager", propagation = Propagation.REQUIRES_NEW)public void processPayment(Long orderId, BigDecimal amount) {Payment payment = new Payment();payment.setOrderId(orderId);payment.setAmount(amount);payment.setStatus("PROCESSING");paymentMapper.insert(payment);}
}
分布式事务解决方案:
// 使用Seata分布式事务
@Service
public class BusinessService {@Autowiredprivate OrderService orderService;@Autowiredprivate InventoryService inventoryService;@Autowiredprivate AccountService accountService;@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)public void createOrder(OrderRequest request) {// 创建订单orderService.create(request);// 扣减库存inventoryService.decrease(request.getProductId(), request.getCount());// 扣减账户余额accountService.decrease(request.getUserId(), request.getMoney());}
}
最终一致性方案:
// 基于消息队列的最终一致性
@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate MessageProducer messageProducer;@Transactionalpublic void createOrder(Order order) {try {// 1. 保存订单orderMapper.insert(order);// 2. 发送消息(本地消息表模式)OutboxEvent event = new OutboxEvent();event.setEventType("ORDER_CREATED");event.setPayload(JSON.toJSONString(order));event.setStatus("PENDING");outboxEventMapper.insert(event);} catch (Exception e) {// 事务回滚throw new RuntimeException("订单创建失败", e);}}// 异步发送消息@Async@EventListenerpublic void handleOrderCreated(OrderCreatedEvent event) {try {// 发送到消息队列messageProducer.send("order.created", event.getOrder());// 更新消息状态outboxEventMapper.updateStatus(event.getEventId(), "SENT");} catch (Exception e) {// 标记为失败,后续重试outboxEventMapper.updateStatus(event.getEventId(), "FAILED");}}
}