Created
June 5, 2025 06:55
-
-
Save pengxiaochao/5b09f3d978e4ba950687fab693c048a3 to your computer and use it in GitHub Desktop.
Mybatis 对象自动缓存插件
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.au92.grpcapi.infrastructure.interceptor; | |
| import com.au92.common.util.exception.ShortClassNameUtil; | |
| import com.au92.common.util.json.JsonUtils; | |
| import com.baomidou.mybatisplus.annotation.TableId; | |
| import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; | |
| import java.lang.reflect.Field; | |
| import java.lang.reflect.ParameterizedType; | |
| import java.lang.reflect.Type; | |
| import java.util.ArrayList; | |
| import java.util.Collection; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Properties; | |
| import java.util.Random; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ThreadLocalRandom; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.function.Function; | |
| import java.util.regex.Pattern; | |
| import javax.annotation.Resource; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.apache.commons.lang3.StringUtils; | |
| import org.apache.ibatis.executor.Executor; | |
| import org.apache.ibatis.mapping.MappedStatement; | |
| import org.apache.ibatis.plugin.Interceptor; | |
| import org.apache.ibatis.plugin.Intercepts; | |
| import org.apache.ibatis.plugin.Invocation; | |
| import org.apache.ibatis.plugin.Plugin; | |
| import org.apache.ibatis.plugin.Signature; | |
| import org.apache.ibatis.session.ResultHandler; | |
| import org.apache.ibatis.session.RowBounds; | |
| import org.springframework.data.redis.core.RedisTemplate; | |
| /** | |
| * Redis缓存拦截器 | |
| * | |
| * <p>基于MyBatis拦截器机制实现的二级缓存,使用Redis作为缓存存储。 | |
| * 支持以下功能:</p> | |
| * <ul> | |
| * <li>单条记录的主键查询缓存</li> | |
| * <li>批量查询的mget优化(兼容Redis Cluster)</li> | |
| * <li>更新操作的缓存失效</li> | |
| * <li>空值缓存防穿透</li> | |
| * <li>双删策略保证数据一致性</li> | |
| * </ul> | |
| * | |
| * <p>支持的查询类型:</p> | |
| * <ul> | |
| * <li>简单主键等值查询:baseMapper.selectById(id)</li> | |
| * <li>LambdaQueryWrapper单主键查询:lambdaQuery().eq(Entity::getId, id)</li> | |
| * <li>批量主键查询:baseMapper.selectBatchIds(ids)</li> | |
| * </ul> | |
| * | |
| * <p>缓存策略:</p> | |
| * <ul> | |
| * <li>缓存键格式:{完整类名}:主键值(使用Hash Tag确保Cluster兼容性,避免同名类冲突)</li> | |
| * <li>正常数据缓存时间:1天(带随机浮动±2.4小时,防止缓存雪崩)</li> | |
| * <li>空值缓存时间:1分钟(带随机浮动±6秒,防止缓存穿透)</li> | |
| * <li>浮动过期时间:在基础过期时间上增加±10%的随机时间,避免大量缓存同时过期</li> | |
| * </ul> | |
| * | |
| * <p>Redis Cluster兼容性:</p> | |
| * <ul> | |
| * <li>使用Hash Tag语法确保同一实体类的key在同一槽位</li> | |
| * <li>批量操作优先使用multiGet,失败时自动回退到逐个获取</li> | |
| * <li>完全兼容单机Redis和Redis Cluster部署</li> | |
| * </ul> | |
| * <li>更新时采用双删策略:操作前删除 -> 执行数据库操作 -> 操作后再删除</li> | |
| * </ul> | |
| * | |
| * @author p_x_c | |
| * @version 1.0 | |
| * @since 2024-01-01 | |
| */ | |
| @Intercepts({ | |
| @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), | |
| @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) | |
| }) | |
| @Slf4j | |
| public class RedisCacheInterceptor implements Interceptor { | |
| /** | |
| * 默认缓存过期时间(秒) - 1天 | |
| */ | |
| private static final long DEFAULT_CACHE_EXPIRE_TIME = 86400; | |
| /** | |
| * 空值缓存时间(秒) - 1分钟,防止缓存穿透 | |
| */ | |
| private static final long NULL_CACHE_EXPIRE_TIME = 60; | |
| /** | |
| * 空值标识符,用于标识数据库中不存在的记录 | |
| */ | |
| private static final String NULL_VALUE_PLACEHOLDER = "NULL_VALUE"; | |
| /** | |
| * 随机数生成器,用于生成浮动过期时间,防止缓存雪崩 | |
| */ | |
| private static final Random RANDOM = ThreadLocalRandom.current(); | |
| /** | |
| * 性能优化:缓存Field查找结果,避免重复反射 | |
| */ | |
| private static final ConcurrentHashMap<Class<?>, Field> ID_FIELD_CACHE = new ConcurrentHashMap<>(); | |
| /** | |
| * 性能优化:缓存实体类推断结果,避免重复类加载和反射 | |
| */ | |
| private static final ConcurrentHashMap<String, Class<?>> ENTITY_CLASS_CACHE = new ConcurrentHashMap<>(); | |
| /** | |
| * 性能优化:缓存Field访问器,避免重复设置setAccessible | |
| */ | |
| private static final ConcurrentHashMap<Field, Function<Object, Object>> FIELD_ACCESSOR_CACHE = new ConcurrentHashMap<>(); | |
| /** | |
| * 将驼峰命名转换为下划线命名 性能优化:避免重复编译正则表达式 | |
| */ | |
| private static final Pattern CAMEL_TO_UNDERSCORE_PATTERN = Pattern.compile("([a-z])([A-Z])"); | |
| @Resource | |
| private RedisTemplate<String, Object> redisTemplate; | |
| public RedisCacheInterceptor() { | |
| } | |
| /** | |
| * MyBatis拦截器核心方法 | |
| * | |
| * @param invocation 方法调用信息,包含目标方法、参数等 | |
| * @return 方法执行结果 | |
| * @throws Throwable 执行过程中的异常 | |
| */ | |
| @Override | |
| public Object intercept(Invocation invocation) throws Throwable { | |
| Object[] args = invocation.getArgs(); | |
| MappedStatement ms = (MappedStatement) args[0]; | |
| Object parameter = args[1]; | |
| String methodName = invocation.getMethod() | |
| .getName(); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Intercepted method: {}, SQL ID: {}", methodName, ms.getId()); | |
| } | |
| // 根据方法类型分发处理 | |
| if ("query".equals(methodName)) { | |
| return handleQuery(invocation, ms, parameter); | |
| } else if ("update".equals(methodName)) { | |
| // 处理新增、更新、删除操作 | |
| return handleUpdate(invocation, ms, parameter); | |
| } | |
| // 其他操作直接执行,不进行缓存处理 | |
| return invocation.proceed(); | |
| } | |
| /** | |
| * 插件包装方法 | |
| * | |
| * @param target 目标对象 | |
| * @return 代理对象 | |
| */ | |
| @Override | |
| public Object plugin(Object target) { | |
| return Plugin.wrap(target, this); | |
| } | |
| /** | |
| * 设置拦截器属性(当前实现为空) | |
| * | |
| * @param properties 属性配置 | |
| */ | |
| @Override | |
| public void setProperties(Properties properties) { | |
| // 当前无需特殊配置 | |
| } | |
| /** | |
| * 构建可能的实体类路径 | |
| */ | |
| private String[] buildPossibleEntityPaths(String packagePrefix, String entityName) { | |
| return new String[]{packagePrefix + ".domain.model." + entityName, | |
| packagePrefix + ".entity." + entityName, | |
| packagePrefix + ".model." + entityName, | |
| packagePrefix + ".domain." + entityName, | |
| packagePrefix + ".pojo." + entityName}; | |
| } | |
| /** | |
| * 缓存数据库查询结果 | |
| * | |
| * @param dbResultList 数据库查询结果列表 | |
| * @param modelClass 实体类Class对象 | |
| * @param finalResults 最终所有数据结果列表 | |
| * @param resultMap 结果映射表,存储ID到实体对象的映射关系 | |
| */ | |
| private void cacheDatabaseResults(List<?> dbResultList, Class<?> modelClass, List<Object> finalResults, Map<Object, Object> resultMap) { | |
| for (Object dbResult : dbResultList) { | |
| if (dbResult != null) { | |
| Object id = getIdValueFromEntity(modelClass, dbResult); | |
| if (id != null) { | |
| cacheQueryResult(getRedisKey(modelClass, id), dbResult); | |
| finalResults.add(dbResult); | |
| resultMap.put(id, dbResult); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * 缓存空值结果(防止缓存穿透) | |
| * | |
| * @param missedIds 批量查询中未命中的ID列表 | |
| * @param resultMap 缓存结果id->对象映射表 | |
| * @param modelClass 实体类Class对象 | |
| */ | |
| private void cacheNullResults(List<Object> missedIds, Map<Object, Object> resultMap, Class<?> modelClass) { | |
| for (Object missedId : missedIds) { | |
| if (!resultMap.containsKey(missedId)) { | |
| cacheNullValue(getRedisKey(modelClass, missedId)); | |
| } | |
| } | |
| } | |
| /** | |
| * 缓存空值 | |
| */ | |
| private void cacheNullValue(String redisKey) { | |
| long expireTime = getFloatingExpireTime(NULL_CACHE_EXPIRE_TIME); | |
| redisTemplate.opsForValue() | |
| .set(redisKey, NULL_VALUE_PLACEHOLDER, expireTime, TimeUnit.SECONDS); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cached null value for key: {} with expire time: {}s", redisKey, expireTime); | |
| } | |
| } | |
| /** | |
| * 缓存查询结果 | |
| */ | |
| private void cacheQueryResult(String redisKey, Object result) { | |
| long expireTime = getFloatingExpireTime(DEFAULT_CACHE_EXPIRE_TIME); | |
| redisTemplate.opsForValue() | |
| .set(redisKey, result, expireTime, TimeUnit.SECONDS); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cached result for key: {} with expire time: {}s", redisKey, expireTime); | |
| } | |
| } | |
| private String camelToUnderscore(String camelCase) { | |
| return CAMEL_TO_UNDERSCORE_PATTERN.matcher(camelCase) | |
| .replaceAll("$1_$2") | |
| .toLowerCase(); | |
| } | |
| /** | |
| * 检查指定名称的实体参数 | |
| */ | |
| private Object checkEntityParam(Class<?> modelClass, Map<String, Object> paramMap, String paramName) { | |
| if (paramMap.containsKey(paramName)) { | |
| Object entity = paramMap.get(paramName); | |
| if (modelClass.isInstance(entity)) { | |
| Object idValue = getIdValueFromEntity(modelClass, entity); | |
| if (idValue != null) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Extracted ID value from '{}' parameter: {}", paramName, idValue); | |
| } | |
| return idValue; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从Mapper类名中提取实体名称 | |
| */ | |
| private String extractEntityName(String mapperClassName) { | |
| if (mapperClassName.endsWith("Mapper")) { | |
| // 去掉"Mapper"后缀 | |
| return mapperClassName.substring(mapperClassName.lastIndexOf('.') + 1, mapperClassName.length() - 6); | |
| } else { | |
| // 如果不是以Mapper结尾,取最后一个点后的部分 | |
| return mapperClassName.substring(mapperClassName.lastIndexOf('.') + 1); | |
| } | |
| } | |
| /** | |
| * 从直接的主键字段中提取ID值 | |
| */ | |
| private Object extractIdFromDirectFields(Class<?> modelClass, Map<String, Object> paramMap) { | |
| Field idField = getIdFieldCached(modelClass); | |
| if (idField == null) { | |
| return null; | |
| } | |
| String idFieldName = idField.getName(); | |
| if (paramMap.containsKey(idFieldName)) { | |
| return paramMap.get(idFieldName); | |
| } | |
| // 也尝试检查下划线形式的字段名 | |
| String underscoreFieldName = camelToUnderscore(idFieldName); | |
| if (paramMap.containsKey(underscoreFieldName)) { | |
| return paramMap.get(underscoreFieldName); | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从实体对象参数中提取ID值 | |
| */ | |
| private Object extractIdFromEntityParams(Class<?> modelClass, Map<String, Object> paramMap) { | |
| // 检查"et"参数(entity的缩写) | |
| Object idFromEt = checkEntityParam(modelClass, paramMap, "et"); | |
| if (idFromEt != null) { | |
| return idFromEt; | |
| } | |
| // 检查"param1"参数(MyBatis的默认参数名) | |
| Object idFromParam1 = checkEntityParam(modelClass, paramMap, "param1"); | |
| if (idFromParam1 != null) { | |
| return idFromParam1; | |
| } | |
| // 遍历所有参数,查找实体对象类型的参数 | |
| for (Map.Entry<String, Object> entry : paramMap.entrySet()) { | |
| Object value = entry.getValue(); | |
| if (modelClass.isInstance(value)) { | |
| Object idValue = getIdValueFromEntity(modelClass, value); | |
| if (idValue != null) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Extracted ID value from parameter '{}': {}", entry.getKey(), idValue); | |
| } | |
| return idValue; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从Map参数中提取主键ID值 | |
| * <pre> | |
| * 支持的Map参数键: | |
| * - "ew": LambdaQueryWrapper等查询条件包装器 | |
| * - "param2": LambdaUpdateWrapper等更新条件包装器 | |
| * - "et": 实体对象(entity的缩写) | |
| * - "param1": MyBatis的默认第一个参数 | |
| * - 直接的主键字段名 | |
| * </pre> | |
| */ | |
| private Object extractIdFromMapParameter(Class<?> modelClass, Map<String, Object> paramMap) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Parameter is Map with keys: {}", paramMap.keySet()); | |
| } | |
| // 尝试从Wrapper中提取ID | |
| Object idFromWrapper = extractIdFromWrapperParams(modelClass, paramMap); | |
| if (idFromWrapper != null) { | |
| return idFromWrapper; | |
| } | |
| // 尝试从实体对象参数中提取ID | |
| Object idFromEntity = extractIdFromEntityParams(modelClass, paramMap); | |
| if (idFromEntity != null) { | |
| return idFromEntity; | |
| } | |
| // 尝试从直接的主键字段中提取ID | |
| return extractIdFromDirectFields(modelClass, paramMap); | |
| } | |
| /** | |
| * 从MyBatis-Plus的AbstractWrapper中提取主键ID值 | |
| * <p> | |
| * 支持简单的主键等值查询缓存 | |
| * | |
| * @param wrapper MyBatis-Plus查询条件包装器 | |
| * @param modelClass 实体类Class对象 | |
| * @return 提取到的主键值,如果不是简单查询则返回null | |
| */ | |
| private Object extractIdFromWrapper(AbstractWrapper<?, ?, ?> wrapper, Class<?> modelClass) { | |
| try { | |
| String sqlSegment = wrapper.getSqlSegment(); | |
| if (sqlSegment == null || sqlSegment.trim() | |
| .isEmpty()) { | |
| return null; | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Extracting ID from wrapper, SQL segment: {}", sqlSegment); | |
| } | |
| // 验证是否为简单的主键查询 | |
| if (!isSimpleIdWrapperQuery(sqlSegment, modelClass)) { | |
| return null; | |
| } | |
| // 从Wrapper参数中提取ID值 | |
| return extractIdValueFromParams(wrapper, sqlSegment); | |
| } catch (Exception e) { | |
| log.warn("Failed to extract ID from wrapper: {}", e.getMessage(), e); | |
| return null; | |
| } | |
| } | |
| /** | |
| * 从Wrapper参数中提取ID值 | |
| */ | |
| private Object extractIdFromWrapperParams(Class<?> modelClass, Map<String, Object> paramMap) { | |
| // 检查ew参数(MyBatis-Plus的Wrapper参数名) | |
| if (paramMap.containsKey("ew")) { | |
| Object wrapper = paramMap.get("ew"); | |
| if (wrapper instanceof AbstractWrapper) { | |
| Object idValue = extractIdFromWrapper((AbstractWrapper<?, ?, ?>) wrapper, modelClass); | |
| if (idValue != null) { | |
| return idValue; | |
| } | |
| } | |
| } | |
| // 检查param2参数(MyBatis-Plus的LambdaUpdateWrapper等操作) | |
| if (paramMap.containsKey("param2")) { | |
| Object wrapper = paramMap.get("param2"); | |
| if (wrapper instanceof AbstractWrapper) { | |
| Object idValue = extractIdFromWrapper((AbstractWrapper<?, ?, ?>) wrapper, modelClass); | |
| if (idValue != null) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Extracted ID value from 'param2' wrapper: {}", idValue); | |
| } | |
| return idValue; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从Wrapper参数中提取ID值 | |
| * | |
| * @param wrapper MyBatis-Plus查询条件包装器 | |
| * @param sqlSegment 原始SQL片段 | |
| * @return 提取到的ID值 | |
| */ | |
| private Object extractIdValueFromParams(AbstractWrapper<?, ?, ?> wrapper, String sqlSegment) { | |
| Map<String, Object> paramNameValuePairs = wrapper.getParamNameValuePairs(); | |
| if (paramNameValuePairs == null || paramNameValuePairs.isEmpty()) { | |
| return null; | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Wrapper paramNameValuePairs: {}", paramNameValuePairs); | |
| } | |
| // 从SQL片段中解析参数名 | |
| if (sqlSegment.contains("#{")) { | |
| String paramName = extractParamNameFromSql(sqlSegment); | |
| if (paramName != null) { | |
| Object idValue = paramNameValuePairs.get(paramName); | |
| if (idValue != null) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Found ID value from wrapper parameter '{}': {}", paramName, idValue); | |
| } | |
| return idValue; | |
| } | |
| } | |
| } | |
| // 兜底策略:如果只有一个参数,直接返回(适用于简单查询) | |
| if (paramNameValuePairs.size() == 1) { | |
| Object idValue = paramNameValuePairs.values() | |
| .iterator() | |
| .next(); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Found simple ID query for cache, ID value: {}", idValue); | |
| } | |
| return idValue; | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从Mapper类名中提取包路径前缀 | |
| */ | |
| private String extractPackagePrefix(String mapperClassName) { | |
| String packagePrefix = mapperClassName.substring(0, mapperClassName.lastIndexOf('.')); | |
| // 如果包名以mapper或dao结尾,去掉这一层 | |
| if (packagePrefix.endsWith(".mapper") || packagePrefix.endsWith(".dao")) { | |
| packagePrefix = packagePrefix.substring(0, packagePrefix.lastIndexOf('.')); | |
| } | |
| return packagePrefix; | |
| } | |
| /** | |
| * 从SQL片段中提取参数名 例如从 "id = #{ew.paramNameValuePairs.MPGENVAL2}" 中提取 "MPGENVAL2" | |
| * | |
| * @param sqlSegment SQL片段 | |
| * @return 参数名 | |
| */ | |
| private String extractParamNameFromSql(String sqlSegment) { | |
| int startIndex = sqlSegment.indexOf("#{"); | |
| int endIndex = sqlSegment.indexOf("}", startIndex); | |
| if (startIndex != -1 && endIndex != -1) { | |
| String paramPath = sqlSegment.substring(startIndex + 2, endIndex); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Found parameter path: {}", paramPath); | |
| } | |
| // 获取最后一个点号后面的部分作为参数名 | |
| int lastDotIndex = paramPath.lastIndexOf('.'); | |
| if (lastDotIndex != -1) { | |
| return paramPath.substring(lastDotIndex + 1); | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * 查找有效的实体类 | |
| */ | |
| private Class<?> findValidEntityClass(String[] possiblePaths, String mapperClassName) { | |
| for (String path : possiblePaths) { | |
| try { | |
| Class<?> entityClass = Class.forName(path); | |
| // 验证是否有@TableId注解的字段 | |
| if (hasTableIdField(entityClass)) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Found entity class from naming convention: {}", entityClass.getName()); | |
| } | |
| return entityClass; | |
| } | |
| } catch (ClassNotFoundException e) { | |
| // 继续尝试下一个路径 | |
| } | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Could not find entity class for mapper: {}", mapperClassName); | |
| } | |
| return null; | |
| } | |
| /** | |
| * 获取批量缓存结果 | |
| * | |
| * @param ids 批量ID集合 | |
| * @param modelClass 实体类Class对象 | |
| * @return 批量缓存结果,包含缓存命中和未命中的ID | |
| */ | |
| private BatchCacheResult getBatchCacheResults(Collection<?> ids, Class<?> modelClass) { | |
| // 构建所有可能的Redis keys | |
| List<String> redisKeys = new ArrayList<>(); | |
| Map<String, Object> keyToIdMap = new HashMap<>(); | |
| for (Object id : ids) { | |
| if (id != null) { | |
| String redisKey = getRedisKey(modelClass, id); | |
| redisKeys.add(redisKey); | |
| keyToIdMap.put(redisKey, id); | |
| } | |
| } | |
| if (redisKeys.isEmpty()) { | |
| return new BatchCacheResult(new ArrayList<>(), new ArrayList<>()); | |
| } | |
| // 使用Redis Cluster兼容的批量获取缓存 | |
| List<Object> cachedResults = multiGetClusterCompatible(redisKeys); | |
| return processBatchCacheResults(redisKeys, cachedResults, keyToIdMap); | |
| } | |
| /** | |
| * 从缓存获取结果 | |
| */ | |
| private Object getCachedResult(String redisKey) { | |
| Object cached = redisTemplate.opsForValue() | |
| .get(redisKey); | |
| if (cached != null) { | |
| if (NULL_VALUE_PLACEHOLDER.equals(cached)) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cache hit for null value, key: {}", redisKey); | |
| } | |
| // 对于NULL_VALUE,返回空集合让MyBatis正确处理 | |
| return new ArrayList<>(); | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cache hit for key: {}", redisKey); | |
| } | |
| return cached; | |
| } | |
| return null; | |
| } | |
| /** | |
| * 获取实体类字段名对应的数据库列名 优先使用@TableId注解的value值,否则转换为下划线命名 | |
| * | |
| * @param field 实体类字段 | |
| * @return 数据库列名 | |
| */ | |
| private String getColumnName(Field field) { | |
| TableId tableId = field.getAnnotation(TableId.class); | |
| if (tableId != null && !tableId.value() | |
| .isEmpty()) { | |
| return tableId.value(); | |
| } | |
| // 默认使用字段名的下划线形式 | |
| return camelToUnderscore(field.getName()); | |
| } | |
| /** | |
| * 通过泛型参数获取实体类 支持 BaseMapper<Entity>, MPJBaseMapper<Entity> 等 | |
| * | |
| * @param mapperClass Mapper类 | |
| * @return 实体类,未找到则返回null | |
| */ | |
| private Class<?> getEntityClassFromGeneric(Class<?> mapperClass) { | |
| try { | |
| Type[] genericInterfaces = mapperClass.getGenericInterfaces(); | |
| for (Type genericInterface : genericInterfaces) { | |
| if (genericInterface instanceof ParameterizedType parameterizedType) { | |
| Type rawType = parameterizedType.getRawType(); | |
| // 检查是否是MyBatis-Plus的Mapper接口 | |
| if (rawType instanceof Class<?> rawClass) { | |
| String rawClassName = rawClass.getName(); | |
| // 支持常见的Mapper父接口 | |
| if (rawClassName.contains("BaseMapper") || rawClassName.contains("MPJBaseMapper") || rawClassName.endsWith("Mapper")) { | |
| Type[] typeArguments = parameterizedType.getActualTypeArguments(); | |
| if (typeArguments.length > 0 && typeArguments[0] instanceof Class<?> entityClass) { | |
| // 验证是否有@TableId注解的字段 | |
| if (hasTableIdField(entityClass)) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Found entity class from generic: {}", entityClass.getName()); | |
| } | |
| return entityClass; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } catch (Exception e) { | |
| log.warn("Failed to get entity class from generic: {}", e.getMessage(), e); | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从Mapper类推断对应的实体类 优先通过泛型参数获取,如果失败则尝试命名规范推断 使用缓存机制避免重复的类加载和反射操作 | |
| * | |
| * @param mapperClass Mapper类 | |
| * @return 推断的实体类,未找到则返回null | |
| */ | |
| private Class<?> getEntityClassFromMapper(Class<?> mapperClass) { | |
| // 优先尝试从泛型参数获取实体类 | |
| Class<?> entityClass = getEntityClassFromGeneric(mapperClass); | |
| if (entityClass != null) { | |
| return entityClass; | |
| } | |
| // 如果泛型方式失败,使用命名规范推断 | |
| return getEntityClassFromNaming(mapperClass); | |
| } | |
| /** | |
| * 通过命名规范推断实体类(备用方案) | |
| * <p> | |
| * 性能优化:使用缓存避免重复的类加载和反射操作 | |
| * | |
| * @param mapperClass Mapper类 | |
| * @return 推断的实体类,未找到则返回null | |
| */ | |
| private Class<?> getEntityClassFromNaming(Class<?> mapperClass) { | |
| String mapperClassName = mapperClass.getName(); | |
| return ENTITY_CLASS_CACHE.computeIfAbsent(mapperClassName, key -> tryFindEntityClass(mapperClassName)); | |
| } | |
| /** | |
| * 生成浮动过期时间,防止缓存雪崩 在默认过期时间基础上添加随机浮动时间(±10%) | |
| * | |
| * @param baseExpireTime 基础过期时间(秒) | |
| * @return 浮动后的过期时间(秒) | |
| */ | |
| private long getFloatingExpireTime(long baseExpireTime) { | |
| // 计算浮动范围(±10%) | |
| long floatRange = baseExpireTime / 10; | |
| // 生成-floatRange到+floatRange之间的随机数 | |
| long randomFloat = RANDOM.nextLong(2 * floatRange + 1) - floatRange; | |
| return baseExpireTime + randomFloat; | |
| } | |
| /** | |
| * 获取实体类中标注了@TableId注解的字段 | |
| * | |
| * @param modelClass 实体类Class对象 | |
| * @return 主键字段,未找到则返回null | |
| */ | |
| private Field getIdField(Class<?> modelClass) { | |
| for (Field field : modelClass.getDeclaredFields()) { | |
| if (field.isAnnotationPresent(TableId.class)) { | |
| return field; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * 性能优化:缓存方式获取实体类的主键字段 避免重复反射操作 | |
| * | |
| * @param modelClass 实体类Class对象 | |
| * @return 标注了@TableId的字段,未找到则返回null | |
| */ | |
| private Field getIdFieldCached(Class<?> modelClass) { | |
| return ID_FIELD_CACHE.computeIfAbsent(modelClass, this::getIdField); | |
| } | |
| /** | |
| * 从各种类型的参数中提取主键ID值 | |
| * <pre> | |
| * 这是缓存系统的核心方法,支持以下参数类型: | |
| * 1. 直接的主键值(String、Integer、Long) | |
| * 2. Map参数(包含MyBatis-Plus的各种Wrapper和实体对象) | |
| * 3. 实体对象(通过反射获取@TableId字段值) | |
| * </pre> | |
| * | |
| * @param modelClass 实体类Class对象 | |
| * @param parameter MyBatis传入的参数对象 | |
| * @return 提取到的主键值,无法提取则返回null | |
| */ | |
| private Object getIdValue(Class<?> modelClass, Object parameter) { | |
| if (parameter == null) { | |
| return null; | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Getting ID value for model class: {}, parameter type: {}", modelClass.getName(), parameter.getClass() | |
| .getName()); | |
| } | |
| // 处理基本类型主键值(String、Integer、Long) | |
| if (isPrimitiveType(parameter)) { | |
| return parameter; | |
| } | |
| // 处理Map参数 | |
| if (parameter instanceof Map) { | |
| @SuppressWarnings("unchecked") | |
| Map<String, Object> paramMap = (Map<String, Object>) parameter; | |
| return extractIdFromMapParameter(modelClass, paramMap); | |
| } | |
| // 处理实体对象 | |
| if (modelClass.isInstance(parameter)) { | |
| return getIdValueFromEntity(modelClass, parameter); | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Parameter is not an instance of entity class: {}, parameter type: {}", modelClass.getName(), parameter.getClass() | |
| .getName()); | |
| } | |
| return null; | |
| } | |
| /** | |
| * 从实体对象中获取ID值 性能优化:使用缓存的字段访问器避免重复反射操作 | |
| * | |
| * @param modelClass 实体类Class对象 | |
| * @param entity 实体对象实例 | |
| * @return 主键值,获取失败则返回null | |
| */ | |
| private Object getIdValueFromEntity(Class<?> modelClass, Object entity) { | |
| if (!modelClass.isInstance(entity)) { | |
| return null; | |
| } | |
| Field idField = getIdFieldCached(modelClass); | |
| if (idField == null) { | |
| return null; | |
| } | |
| try { | |
| // 使用缓存的字段访问器提升性能 | |
| Function<Object, Object> accessor = FIELD_ACCESSOR_CACHE.computeIfAbsent(idField, field -> { | |
| field.setAccessible(true); | |
| return obj -> { | |
| try { | |
| return field.get(obj); | |
| } catch (IllegalAccessException e) { | |
| log.warn("Failed to get field value: {}", e.getMessage(), e); | |
| return null; | |
| } | |
| }; | |
| }); | |
| return accessor.apply(entity); | |
| } catch (Exception e) { | |
| log.warn("Failed to get ID from entity: {}", e.getMessage(), e); | |
| return null; | |
| } | |
| } | |
| /** | |
| * 逐个获取Redis键值 | |
| */ | |
| private List<Object> getIndividualKeys(List<String> redisKeys) { | |
| List<Object> results = new ArrayList<>(redisKeys.size()); | |
| for (String key : redisKeys) { | |
| try { | |
| Object value = redisTemplate.opsForValue() | |
| .get(key); | |
| results.add(value); | |
| } catch (Exception ex) { | |
| log.warn("Failed to get value for key: {}, error: {}", key, ex.getMessage(), ex); | |
| results.add(null); | |
| } | |
| } | |
| return results; | |
| } | |
| /** | |
| * 从MappedStatement推断对应的实体类 通过解析SQL ID获取Mapper类,然后推断实体类 | |
| * | |
| * @param ms MyBatis的MappedStatement对象 | |
| * @return 推断的实体类,失败则返回null | |
| */ | |
| private Class<?> getModelClass(MappedStatement ms) { | |
| try { | |
| String id = ms.getId(); // 类名+方法名 | |
| String mapperClassName = id.substring(0, id.lastIndexOf('.')); | |
| Class<?> mapperClass = Class.forName(mapperClassName); | |
| // 从Mapper类推断实体类 | |
| return getEntityClassFromMapper(mapperClass); | |
| } catch (Exception e) { | |
| log.warn("Failed to get model class from MappedStatement: {}", e.getMessage(), e); | |
| return null; | |
| } | |
| } | |
| /** | |
| * 生成Redis缓存键 格式:{包名.类名}:主键值 | |
| * <pre> | |
| * 使用Hash Tag确保相同实体类的所有key在Redis Cluster中分配到同一槽位 | |
| * 同时避免不同包下同名类的key冲突问题 | |
| * </pre> | |
| * | |
| * @param clazz 实体类Class对象 | |
| * @param idValue 主键值 | |
| * @return Redis缓存键 | |
| */ | |
| private String getRedisKey(Class<?> clazz, Object idValue) { | |
| String fullClassName = ShortClassNameUtil.simplifyClassName(clazz.getName(), null); | |
| // 使用Hash Tag语法 {fullClassName} 确保相同实体类的key都分配到同一个槽位 | |
| // 同时避免不同包下同名类的key冲突(如 com.au92.user.User 和 com.au92.admin.User) | |
| return "{" + fullClassName + "}:" + idValue; | |
| } | |
| /** | |
| * 处理批量查询,支持Redis mget优化 | |
| * <p> | |
| * 通过批量获取缓存减少Redis网络交互次数,提升性能 | |
| * | |
| * @param invocation MyBatis方法调用对象 | |
| * @param parameter 查询参数 | |
| * @param modelClass 实体类Class对象 | |
| * @return 查询结果列表 | |
| * @throws Throwable 执行过程中的异常 | |
| */ | |
| private Object handleBatchQuery(Invocation invocation, Object parameter, Class<?> modelClass) throws Throwable { | |
| BatchQueryContext context = prepareBatchQueryContext(parameter); | |
| if (context == null) { | |
| return invocation.proceed(); | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Handling batch query for {} IDs", context.ids.size()); | |
| } | |
| // 构建Redis keys并批量获取缓存 | |
| BatchCacheResult cacheResult = getBatchCacheResults(context.ids, modelClass); | |
| // 处理缓存未命中的ID,查询数据库 | |
| Object dbResults = handleCacheMissedIds(invocation, parameter, cacheResult.missedIds, modelClass); | |
| // 合并缓存和数据库结果 | |
| return mergeBatchResults(cacheResult.cachedResults, dbResults, cacheResult.missedIds, modelClass); | |
| } | |
| /** | |
| * 处理缓存未命中的ID,查询数据库 | |
| * | |
| * @param invocation MyBatis方法调用对象 | |
| * @param originalParameter 原始查询参数 | |
| * @param missedIds 未命中的ID列表 | |
| * @param modelClass 实体类Class对象 | |
| * @return 查询结果列表 | |
| * @throws Throwable 执行过程中的异常 | |
| */ | |
| private Object handleCacheMissedIds(Invocation invocation, Object originalParameter, List<Object> missedIds, Class<?> modelClass) throws Throwable { | |
| if (missedIds.isEmpty()) { | |
| return new ArrayList<>(); | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cache miss for {} IDs, querying database", missedIds.size()); | |
| } | |
| // 修改参数只查询未命中的ID | |
| @SuppressWarnings("unchecked") | |
| Map<String, Object> paramMap = (Map<String, Object>) originalParameter; | |
| Map<String, Object> newParamMap = new HashMap<>(paramMap); | |
| newParamMap.put("coll", missedIds); | |
| // 创建新的调用参数 | |
| Object[] newArgs = invocation.getArgs() | |
| .clone(); | |
| newArgs[1] = newParamMap; | |
| // 执行数据库查询 | |
| return new Invocation(invocation.getTarget(), invocation.getMethod(), newArgs).proceed(); | |
| } | |
| /** | |
| * 处理单条记录查询 | |
| * <p> | |
| * 先尝试从Redis缓存获取,缓存未命中则查询数据库并缓存结果 | |
| * | |
| * @param invocation MyBatis方法调用对象 | |
| * @param ms MappedStatement对象 | |
| * @param parameter 查询参数 | |
| * @return 查询结果 | |
| * @throws Throwable 执行过程中的异常 | |
| */ | |
| private Object handleQuery(Invocation invocation, MappedStatement ms, Object parameter) throws Throwable { | |
| Class<?> modelClass = getModelClass(ms); | |
| if (modelClass == null) { | |
| return invocation.proceed(); | |
| } | |
| // 检查是否是批量查询 | |
| if (isBatchQuery(ms, parameter)) { | |
| return handleBatchQuery(invocation, parameter, modelClass); | |
| } | |
| return handleSingleQuery(invocation, modelClass, parameter); | |
| } | |
| /** | |
| * 处理单条记录查询 | |
| */ | |
| private Object handleSingleQuery(Invocation invocation, Class<?> modelClass, Object parameter) throws Throwable { | |
| Object idValue = getIdValue(modelClass, parameter); | |
| if (idValue == null) { | |
| return invocation.proceed(); | |
| } | |
| String redisKey = getRedisKey(modelClass, idValue); | |
| // 尝试从缓存获取 | |
| Object cached = getCachedResult(redisKey); | |
| if (cached != null) { | |
| if (cached instanceof List) { | |
| return cached; | |
| } | |
| return List.of(cached); | |
| } | |
| // 缓存未命中,查询数据库并缓存结果 | |
| return queryDatabaseAndCache(invocation, redisKey); | |
| } | |
| /** | |
| * 处理更新操作(新增、修改、删除) 采用双删策略:操作前删除缓存 -> 执行数据库操作 -> 操作后再删除缓存 确保数据一致性,避免缓存和数据库数据不一致的问题 | |
| * | |
| * @param invocation MyBatis方法调用对象 | |
| * @param ms MappedStatement对象 | |
| * @param parameter 操作参数 | |
| * @return 操作结果 | |
| * @throws Throwable 执行过程中的异常 | |
| */ | |
| private Object handleUpdate(Invocation invocation, MappedStatement ms, Object parameter) throws Throwable { | |
| Class<?> modelClass = getModelClass(ms); | |
| if (modelClass == null) { | |
| return invocation.proceed(); | |
| } | |
| String redisKey = ""; | |
| Object idValue = getIdValue(modelClass, parameter); | |
| if (idValue != null) { | |
| redisKey = getRedisKey(modelClass, idValue); | |
| redisTemplate.delete(redisKey); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Deleted cache for key: {}", redisKey); | |
| } | |
| } else { | |
| log.warn("NOT Deleted cache for parameter: {}", JsonUtils.toJSONString(parameter)); | |
| } | |
| Object result = invocation.proceed(); | |
| if (StringUtils.isNotBlank(redisKey)) { | |
| // double delete cache | |
| redisTemplate.delete(redisKey); | |
| } | |
| return result; | |
| } | |
| /** | |
| * 检查类是否有@TableId注解的字段 用于验证类是否为有效的MyBatis-Plus实体类 | |
| * | |
| * @param clazz 要检查的类 | |
| * @return true表示有@TableId字段,false表示没有 | |
| */ | |
| private boolean hasTableIdField(Class<?> clazz) { | |
| for (Field field : clazz.getDeclaredFields()) { | |
| if (field.isAnnotationPresent(TableId.class)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * 检查是否是批量查询(如selectByIds) 通过方法名和参数类型判断是否为批量查询 | |
| * | |
| * @param ms MappedStatement对象 | |
| * @param parameter 查询参数 | |
| * @return true表示是批量查询,false表示不是 | |
| */ | |
| private boolean isBatchQuery(MappedStatement ms, Object parameter) { | |
| String sqlId = ms.getId(); | |
| // 检查方法名是否包含批量查询的关键词 | |
| if (sqlId.contains("selectByIds") || sqlId.contains("listByIds") || sqlId.contains("selectBatchIds") || sqlId.contains("selectByBatchIds")) { | |
| return true; | |
| } | |
| // 检查参数是否是Collection类型 | |
| if (parameter instanceof Map) { | |
| @SuppressWarnings("unchecked") | |
| Map<String, Object> paramMap = (Map<String, Object>) parameter; | |
| // MyBatis-Plus批量查询通常包含coll参数 | |
| return paramMap.containsKey("coll") && paramMap.get("coll") instanceof java.util.Collection; | |
| } | |
| return false; | |
| } | |
| /** | |
| * 检查是否为复杂查询(包含AND、OR的多条件查询) | |
| */ | |
| private boolean isComplexQuery(String normalizedSql) { | |
| return normalizedSql.contains(" and ") || normalizedSql.contains(" or "); | |
| } | |
| /** | |
| * 判断查询结果是否为空 | |
| */ | |
| private boolean isEmptyResult(Object result) { | |
| return result == null || (result instanceof Collection && ((Collection<?>) result).isEmpty()); | |
| } | |
| /** | |
| * 判断参数是否为基本类型的主键值 | |
| */ | |
| private boolean isPrimitiveType(Object parameter) { | |
| return parameter instanceof String || parameter instanceof Integer || parameter instanceof Long; | |
| } | |
| /** | |
| * 判断是否为简单的主键等值查询 | |
| * | |
| * @param normalizedSql 规范化的SQL片段 | |
| * @param idColumnLower 小写的主键列名 | |
| * @return true表示是简单主键查询 | |
| */ | |
| private boolean isSimpleIdQuery(String normalizedSql, String idColumnLower) { | |
| // 标准格式: id = ? | |
| if (normalizedSql.equals(idColumnLower + " = ?")) { | |
| return true; | |
| } | |
| // MyBatis-Plus格式: id = #{ew.paramNameValuePairs.MPGENVAL1} | |
| if (normalizedSql.startsWith(idColumnLower + " = #{") && normalizedSql.endsWith("}")) { | |
| String paramPattern = idColumnLower + " = #{"; | |
| return normalizedSql.equals(paramPattern + normalizedSql.substring(paramPattern.length())); | |
| } | |
| return false; | |
| } | |
| /** | |
| * 验证是否为简单的主键查询 | |
| * | |
| * @param sqlSegment SQL片段,通常是Wrapper的getSqlSegment()返回值 | |
| * @param modelClass 实体类Class对象 | |
| * @return 如果是简单的主键等值查询则返回true,否则返回false | |
| */ | |
| private boolean isSimpleIdWrapperQuery(String sqlSegment, Class<?> modelClass) { | |
| // 获取ID字段信息 | |
| Field idField = getIdFieldCached(modelClass); | |
| if (idField == null) { | |
| return false; | |
| } | |
| String idColumnName = getColumnName(idField); | |
| if (log.isDebugEnabled()) { | |
| log.debug("ID column name: {}", idColumnName); | |
| } | |
| // 规范化SQL片段 | |
| String normalizedSql = normalizeSqlSegment(sqlSegment); | |
| String idColumnLower = idColumnName.toLowerCase(); | |
| // 检查是否为复杂查询 | |
| if (isComplexQuery(normalizedSql)) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Skipping cache for complex query with multiple conditions: {}", normalizedSql); | |
| } | |
| return false; | |
| } | |
| // 判断是否为简单的主键等值查询 | |
| boolean isSimpleIdEqualsQuery = isSimpleIdQuery(normalizedSql, idColumnLower); | |
| if (!isSimpleIdEqualsQuery) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("Skipping cache for non-simple ID query: {}", normalizedSql); | |
| } | |
| } | |
| return isSimpleIdEqualsQuery; | |
| } | |
| /** | |
| * 合并批量查询的缓存和数据库结果 | |
| * | |
| * @param cachedResults 批量缓存命中结果列表 | |
| * @param dbResults 数据库查询结果 | |
| * @param missedIds 批量缓存未命中的ID列表 | |
| * @param modelClass 实体类Class对象 | |
| * @return 合并缓存和数据库查询的结果后的结果列表 | |
| */ | |
| private Object mergeBatchResults(List<Object> cachedResults, Object dbResults, List<Object> missedIds, Class<?> modelClass) { | |
| List<Object> finalResults = new ArrayList<>(cachedResults); | |
| Map<Object, Object> resultMap = new HashMap<>(); | |
| // 将缓存结果放入resultMap | |
| for (Object cached : cachedResults) { | |
| if (cached != null) { | |
| Object id = getIdValueFromEntity(modelClass, cached); | |
| if (id != null) { | |
| resultMap.put(id, cached); | |
| } | |
| } | |
| } | |
| if (dbResults instanceof List<?> dbResultList) { | |
| // 缓存数据库查询结果 | |
| cacheDatabaseResults(dbResultList, modelClass, finalResults, resultMap); | |
| // 对于数据库中也没有的ID,缓存NULL_VALUE | |
| cacheNullResults(missedIds, resultMap, modelClass); | |
| } | |
| if (log.isDebugEnabled()) { | |
| log.debug("Batch query completed: {} total results, {} from cache, {} from database", finalResults.size(), finalResults.size() - missedIds.size(), missedIds.size()); | |
| } | |
| return finalResults; | |
| } | |
| /** | |
| * Redis Cluster兼容的批量获取缓存方法 | |
| * <p> | |
| * 当Redis配置为Cluster模式时,multiGet要求所有key在同一槽位,否则会报错 | |
| * <p> | |
| * 通过Hash Tag语法确保同一实体类的key在同一槽位,使批量操作正常工作 | |
| * | |
| * @param redisKeys 要获取的Redis key列表 | |
| * @return 对应的值列表,顺序与key列表一致,未找到的值为null | |
| */ | |
| private List<Object> multiGetClusterCompatible(List<String> redisKeys) { | |
| if (redisKeys.isEmpty()) { | |
| return new ArrayList<>(); | |
| } | |
| try { | |
| // 尝试使用标准的multiGet,如果所有key都在同一槽位则正常执行 | |
| return redisTemplate.opsForValue() | |
| .multiGet(redisKeys); | |
| } catch (Exception e) { | |
| // 如果multiGet失败(通常是因为key分布在不同槽位),则回退到逐个获取 | |
| log.warn("multiGet failed, keys:[{}] falling back to individual gets: {}", redisKeys, e.getMessage(), e); | |
| return getIndividualKeys(redisKeys); | |
| } | |
| } | |
| /** | |
| * 规范化SQL片段用于匹配判断 | |
| * | |
| * @param sqlSegment 原始SQL片段 | |
| * @return 规范化后的SQL片段 | |
| */ | |
| private String normalizeSqlSegment(String sqlSegment) { | |
| String normalizedSql = sqlSegment.toLowerCase() | |
| .trim(); | |
| // 移除外层括号 | |
| if (normalizedSql.startsWith("(") && normalizedSql.endsWith(")")) { | |
| normalizedSql = normalizedSql.substring(1, normalizedSql.length() - 1) | |
| .trim(); | |
| } | |
| return normalizedSql; | |
| } | |
| /** | |
| * 准备批量查询上下文 | |
| */ | |
| private BatchQueryContext prepareBatchQueryContext(Object parameter) { | |
| if (!(parameter instanceof Map)) { | |
| return null; | |
| } | |
| @SuppressWarnings("unchecked") | |
| Map<String, Object> paramMap = (Map<String, Object>) parameter; | |
| Object coll = paramMap.get("coll"); | |
| if (!(coll instanceof Collection<?> ids) || ids.isEmpty()) { | |
| return null; | |
| } | |
| return new BatchQueryContext(ids); | |
| } | |
| /** | |
| * 处理批量缓存结果 | |
| * | |
| * @param redisKeys Redis缓存键列表 | |
| * @param cachedResults 批量获取的缓存结果列表 | |
| * @param keyToIdMap 键到ID的映射 | |
| * @return 批量缓存结果对象,包含命中结果和未命中的ID列表 | |
| */ | |
| private BatchCacheResult processBatchCacheResults(List<String> redisKeys, List<Object> cachedResults, Map<String, Object> keyToIdMap) { | |
| List<Object> finalResults = new ArrayList<>(); | |
| List<Object> missedIds = new ArrayList<>(); | |
| for (int i = 0; i < redisKeys.size(); i++) { | |
| String redisKey = redisKeys.get(i); | |
| Object cached = cachedResults.get(i); | |
| Object id = keyToIdMap.get(redisKey); | |
| if (cached != null) { | |
| if (!NULL_VALUE_PLACEHOLDER.equals(cached)) { | |
| finalResults.add(cached); | |
| if (log.isDebugEnabled()) { | |
| log.debug("Cache hit for batch query, key: {}", redisKey); | |
| } | |
| } | |
| // NULL_VALUE表示数据库中确实没有这条记录,不需要再查询 | |
| } else { | |
| // 缓存未命中,需要查询数据库 | |
| missedIds.add(id); | |
| } | |
| } | |
| return new BatchCacheResult(finalResults, missedIds); | |
| } | |
| /** | |
| * 查询数据库并缓存结果 | |
| */ | |
| private Object queryDatabaseAndCache(Invocation invocation, String redisKey) throws Throwable { | |
| // 缓存未命中,继续执行数据库查询 | |
| Object result = invocation.proceed(); | |
| if (isEmptyResult(result)) { | |
| // 缓存空值,避免频繁查询数据库 | |
| cacheNullValue(redisKey); | |
| return result; | |
| } | |
| // 缓存查询结果 | |
| cacheQueryResult(redisKey, result instanceof List ? ((List<?>) result).getFirst() : result); | |
| return result; | |
| } | |
| /** | |
| * 尝试根据命名规范查找实体类 | |
| */ | |
| private Class<?> tryFindEntityClass(String mapperClassName) { | |
| String entityName = extractEntityName(mapperClassName); | |
| String packagePrefix = extractPackagePrefix(mapperClassName); | |
| String[] possiblePaths = buildPossibleEntityPaths(packagePrefix, entityName); | |
| return findValidEntityClass(possiblePaths, mapperClassName); | |
| } | |
| /** | |
| * 批量查询上下文 | |
| * | |
| * @param ids 批量查询的ID集合 | |
| */ | |
| private record BatchQueryContext(Collection<?> ids) { | |
| } | |
| /** | |
| * 批量缓存结果 | |
| * | |
| * @param cachedResults 批量缓存命中结果列表 | |
| * @param missedIds 批量缓存未命中的ID列表 | |
| */ | |
| private record BatchCacheResult(List<Object> cachedResults, List<Object> missedIds) { | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.au92.common.util.exception; | |
| import lombok.experimental.UtilityClass; | |
| import org.apache.commons.lang3.StringUtils; | |
| /** | |
| * 更短的类名 | |
| * | |
| * @author p_x_c | |
| */ | |
| @UtilityClass | |
| public class ShortClassNameUtil { | |
| // 白名单,不需要简化的类名 | |
| public static final String PACKAGE_NAME = "com.au92"; | |
| /** | |
| * 简化类名 | |
| * | |
| * @param fullClassName 完整类名 | |
| * @return 简化后的类名 | |
| */ | |
| public String simplifyClassName(String fullClassName) { | |
| return simplifyClassName(fullClassName, PACKAGE_NAME); | |
| } | |
| /** | |
| * 简化类名 | |
| * | |
| * @param fullClassName 完整类名 | |
| * @param allowPackageName 允许的包名,如果类名包含该包名,则不进行简化 | |
| * @return 简化后的类名 | |
| */ | |
| public String simplifyClassName(String fullClassName, String allowPackageName) { | |
| if (StringUtils.isNotBlank(allowPackageName) && fullClassName.contains(allowPackageName)) { | |
| return fullClassName; | |
| } | |
| String[] parts = fullClassName.split("\\."); | |
| StringBuilder simplifiedName = new StringBuilder(); | |
| for (int i = 0; i < parts.length - 1; i++) { | |
| simplifiedName.append(parts[i].charAt(0)) | |
| .append('.'); | |
| } | |
| simplifiedName.append(parts[parts.length - 1]); | |
| return simplifiedName.toString(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment