spring 无法从Redis缓存获取集合

mm9b1k5b  于 2023-09-29  发布在  Spring
关注(0)|答案(2)|浏览(131)

我们使用Redis缓存在应用程序的缓存中存储数据。我们直接使用@Cacheable来允许缓存,并使用Redis来缓存。下面是配置
Redis配置-

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig implements CachingConfigurer {

@Value("${spring.cache.redis.time-to-live}")
Long redisTTL;

@Bean
public RedisCacheConfiguration cacheConfiguration(ObjectMapper objectMapper) {
    objectMapper = objectMapper.copy();
    objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module())
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(redisTTL))
            .disableCachingNullValues()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}

@Bean
public RedissonClient reddison(@Value("${spring.redis.host}") final String redisHost,
                               @Value("${spring.redis.port}") final int redisPort,
                               @Value("${spring.redis.cluster.nodes}") final String clusterAddress,
                               @Value("${spring.redis.use-cluster}") final boolean useCluster,
                               @Value("${spring.redis.timeout}") final int timeout) {
    Config config = new Config();
    if (useCluster) {
        config.useClusterServers().addNodeAddress(clusterAddress).setTimeout(timeout);
    } else {
        config.useSingleServer().setAddress(String.format("redis://%s:%d", redisHost, redisPort)).setTimeout(timeout);
    }
    return Redisson.create(config);
}

@Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redissonClient) {
    return new RedissonConnectionFactory(redissonClient);
}


@Bean
public RedisCacheManager cacheManager(RedissonClient redissonClient, ObjectMapper objectMapper) {
    this.redissonConnectionFactory(redissonClient).getConnection().flushDb();
    RedisCacheManager redisCacheManager= RedisCacheManager.builder(this.redissonConnectionFactory(redissonClient))
            .cacheDefaults(this.cacheConfiguration(objectMapper))
            .build();
    redisCacheManager.setTransactionAware(true);
    return redisCacheManager;
}

@Override
public CacheErrorHandler errorHandler() {
    return new RedisCacheErrorHandler();
}

@Slf4j
public static class RedisCacheErrorHandler implements CacheErrorHandler {

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
        log.info("Unable to get from cache " + cache.getName() + " : " + exception.getMessage());
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
        log.info("Unable to put into cache " + cache.getName() + " : " + exception.getMessage());
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
        log.info("Unable to evict from cache " + cache.getName() + " : " + exception.getMessage());
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
        log.info("Unable to clean cache " + cache.getName() + " : " + exception.getMessage());
    }
}
}

服务等级-

@Service
@AllArgsConstructor
@Transactional
public class CompanyServiceImpl implements CompanyService {

private final CompanyRepository companyRepository;

@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
public Optional<CompanyEntity> findByName(String companyName) {
    return companyRepository.findByName(companyName);
}

}

公司级-

@Entity    
@Jacksonized
@AllArgsConstructor
@NoArgsConstructor
public class CompanyEntity  {

@Id
private Long id;

@ToString.Exclude
@OneToMany(mappedBy = "comapnyENtity", cascade = CascadeType.ALL,fetch = FetchType.EAGER)
private List<EmployeeEntity> employeeEntities;

}

一旦我们运行了服务,缓存也会正确完成。一旦我们启动查询,我们将在缓存中获得以下记录-

> get Company::ABC

“{"@class”:”com。abc。实体。CompanyEntity”,“createdTs”:1693922698604,“id”:100000000002,“名称”:“ABC”,“description”:“ABC操作”,“活动”:true,“EmployeeEntities”:[“org. hibernate 收藏。内部的PersistentBag”,[{"@class”:”com。abc。实体。EmployeeEntity”,“createdTs”:1693922698604,“ID”:10000000002,“删除实体”:{"@class”:”com。abc。EmployeeLevel”,“levelId”:100000000000,“名称”:“H1”,“active”:真}}]]}”
但是,当我们尝试第二次执行查询时,它仍然进入该高速缓存方法,并具有以下日志-

Unable to get from cache Company : Could not read JSON: failed to lazily initialize a 
    collection, could not initialize proxy - no Session (through reference chain: 
    com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"]); nested exception 
    is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a c 
    collection, could not initialize proxy - no Session (through reference chain: 
    com.abc.entity.CompanyEntity$CompanyEntityBuilder["employeeEntities"])

我从各种SO回答中了解到,这是由于代理子对象的会话不可用。但是我们使用EAGER模式进行缓存,整个集合也存在于缓存中。但它仍然进入缓存方法并从db获取值。我们如何防止它并直接从缓存中使用它。

UPDATE如果我们使用LAZY加载,集合对象不会被缓存,并且为null。但是我们需要缓存的集合,因为方法不会按顺序调用,缓存的方法稍后将返回null。

isr3a4wc

isr3a4wc1#

找到所需的答案here。我的缓存集合引用未正确反序列化。在应用所需的更改之后,我能够成功地从Redis缓存中反序列化缓存的集合对象。
现有Redis配置中的更改-

@Bean
public RedisCacheConfiguration cacheConfiguration(ObjectMapper objectMapper) {
    objectMapper = objectMapper.copy();
    objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    objectMapper.registerModules(new JavaTimeModule(), new Hibernate5Module(), new Jdk8Module())
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY).addMixIn(Collection.class, HibernateCollectionMixIn.class);
    return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofDays(redisTTL))
            .disableCachingNullValues()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}

作为修复的一部分,添加了两个新类-

class HibernateCollectionIdResolver extends TypeIdResolverBase {

public HibernateCollectionIdResolver() {
}

@Override
public String idFromValue(Object value) {
    //translate from HibernanteCollection class to JDK collection class
    if (value instanceof PersistentArrayHolder) {
        return Array.class.getName();
    } else if (value instanceof PersistentBag || value instanceof PersistentIdentifierBag || value instanceof PersistentList) {
        return List.class.getName();
    } else if (value instanceof PersistentSortedMap) {
        return TreeMap.class.getName();
    } else if (value instanceof PersistentSortedSet) {
        return TreeSet.class.getName();
    } else if (value instanceof PersistentMap) {
        return HashMap.class.getName();
    } else if (value instanceof PersistentSet) {
        return HashSet.class.getName();
    } else {
        //default is JDK collection
        return value.getClass().getName();
    }
}

@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
    return idFromValue(value);
}

//deserialize the json annotated JDK collection class name to JavaType
@Override
public JavaType typeFromId(DatabindContext ctx, String id) throws IOException {
    try {
        return ctx.getConfig().constructType(Class.forName(id));
    } catch (ClassNotFoundException e) {
        throw new UnsupportedOperationException(e);
    }
}

@Override
public JsonTypeInfo.Id getMechanism() {
    return JsonTypeInfo.Id.CLASS;
}

}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.CLASS
)
@JsonTypeIdResolver(value = HibernateCollectionIdResolver.class)
public class HibernateCollectionMixIn {
}
xpcnnkqh

xpcnnkqh2#

JsonMappingException表示Jackson尝试反序列化Hibernate代理对象,但由于反序列化期间Hibernate会话不可用而无法执行此操作。
因此,您需要确保employeeEntities集合在序列化之前正确初始化为非代理状态,以便Jackson正确地从该高速缓存中反序列化CompanyEntity对象 * 而不需要 * Hibernate会话。
您可以通过调整服务方法来确保集合的正确初始化,以便在缓存CompanyEntity之前强制初始化employeeEntities集合!

@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
public Optional<CompanyEntity> findByName(String companyName) {
    Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName);
    companyEntityOpt.ifPresent(companyEntity -> {
        companyEntity.getEmployeeEntities().size();  // Force initialization of the collection
    });
    return companyEntityOpt;
}

这样,employeeEntities集合就从Hibernate代理转换为常规Java集合。这应该有助于避免在该高速缓存进行反序列化时遇到的JsonMappingException问题。
这假设您使用的是FetchType.EAGER,这意味着当您获取CompanyEntity时,employeeEntities集合将自动加载。
如果问题仍然存在,您可以检查分离实体是否有帮助:

@Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
public Optional<CompanyEntity> findByName(String companyName) {
    Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName);
    companyEntityOpt.ifPresent(companyEntity -> {
        companyEntity.getEmployeeEntities().size();  // Force initialization of the collection
        // Obtain entity manager and detach the entity
        EntityManager em = // get entity manager bean
        em.detach(companyEntity);
    });
    return companyEntityOpt;
}

从Hibernate会话中分离实体会将其转换为普通的POJO。
请注意,要获得EntityManager,您需要将其注入到服务类中,并且您应该确保在分离实体之前正确初始化稍后将访问的所有关系和属性。
另一种方法,避免直接缓存Hibernate托管实体或确保Hibernate代理不被序列化,是使用DTO(数据传输对象)将持久化模型与应用程序逻辑中使用的对象分离。

  • 创建一个与CompanyEntity类对应的DTO类。
  • 在缓存之前,将CompanyEntity示例Map到DTO示例。
  • 缓存DTO示例而不是实体示例。
  • 从该高速缓存阅读时,您将获得一个DTO示例,然后可以在必要时将其Map回实体示例。

在您的服务类中,它看起来像这样:

@Service
@AllArgsConstructor
@Transactional
public class CompanyServiceImpl implements CompanyService {

    private final CompanyRepository companyRepository;
    private final ModelMapper modelMapper; // Bean for mapping entity to DTO

    @Cacheable(key = "#companyName", value = COMPANY_CACHE_NAME, cacheManager = "cacheManager")
    public Optional<CompanyDTO> findByName(String companyName) {
        Optional<CompanyEntity> companyEntityOpt = companyRepository.findByName(companyName);
        return companyEntityOpt.map(companyEntity -> {
            companyEntity.getEmployeeEntities().size(); // Force initialization of the collection
            return modelMapper.map(companyEntity, CompanyDTO.class); // Map entity to DTO before caching
        });
    }
}

在此方法中,您将使用ModelMapper或其他Map框架将实体Map到DTO。该DTO将被缓存,避免了您遇到的Hibernate代理问题。
请记住为EmployeeEntity和作为对象图一部分的任何其他实体创建相应的DTO。
这种方法需要创建额外的类并修改服务逻辑,但它将在Hibernate实体和缓存内容之间创建一个清晰的分离,这有助于避免类似这样的问题。

相关问题