Spring Boot+Infinispan嵌入式-当要缓存的对象被修改时,如何防止ClassCastException



我有一个带有Spring Boot 2.5.5和嵌入式Infinispan 12.1.7的web应用程序。

我有一个带有端点的控制器,通过ID:获取Person对象

@RestController
public class PersonController {
private final PersonService service;
public PersonController(PersonService service) {
this.service = service;
}
@GetMapping("/person/{id}")
public ResponseEntity<Person> getPerson(@PathVariable("id") String id) {
Person person = this.service.getPerson(id);
return ResponseEntity.ok(person);
}
}

以下是在getPerson方法上使用@Cacheable注释的PersonService实现:

public interface PersonService {
Person getPerson(String id);
}
@Service
public class PersonServiceImpl implements PersonService {
private static final Logger LOG = LoggerFactory.getLogger(PersonServiceImpl.class);
@Override
@Cacheable("person")
public Person getPerson(String id) {
LOG.info("Get Person by ID {}", id);
Person person = new Person();
person.setId(id);
person.setFirstName("John");
person.setLastName("Doe");
person.setAge(35);
person.setGender(Gender.MALE);
person.setExtra("extra value");
return person;
}
}

这是Person类:

public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String firstName;
private String lastName;
private Integer age;
private Gender gender;
private String extra;
/* Getters / Setters */
...
}

我将infinispan配置为使用基于文件系统的缓存存储:

<?xml version="1.0" encoding="UTF-8"?>
<infinispan xmlns="urn:infinispan:config:12.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:12.1 https://infinispan.org/schemas/infinispan-config-12.1.xsd">
<cache-container default-cache="default">
<serialization marshaller="org.infinispan.commons.marshall.JavaSerializationMarshaller">
<allow-list>
<regex>com.example.*</regex>
</allow-list>
</serialization>
<local-cache-configuration name="mirrorFile">
<persistence passivation="false">
<file-store path="${infinispan.disk.store.dir}"
shared="false"
preload="false"
purge="false"
segmented="false">
</file-store>
</persistence>
</local-cache-configuration>
<local-cache name="person" statistics="true" configuration="mirrorFile">
<memory max-count="500"/>
<expiration lifespan="86400000"/>
</local-cache>
</cache-container>
</infinispan>

我请求终结点获取id为"1"的人员:http://localhost:8090/assets-第一次调用webapp/person/1
PersonService.getPerson(String),并缓存结果
我再次请求端点获取id为"1"的人员,并在缓存中检索结果。

我通过使用getter/setter删除extra字段来更新Person对象,并添加一个extra2字段:

public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String firstName;
private String lastName;
private Integer age;
private Gender gender;
private String extra2;
...
public String getExtra2() {
return extra2;
}
public void setExtra2(String extra2) {
this.extra2 = extra2;
}
}

我再次请求端点获取id为"1"的人,但抛出了ClassCastException

java.lang.ClassCastException: com.example.controller.Person cannot be cast to com.example.controller.Person] with root cause java.lang.ClassCastException: com.example.controller.Person cannot be cast to com.example.controller.Person
at com.example.controller.PersonServiceImpl$$EnhancerBySpringCGLIB$$ec42b86.getPerson(<generated>) ~[classes/:?]
at com.example.controller.PersonController.getPerson(PersonController.java:19) ~[classes/:?]

我通过删除extra2字段并添加extra字段来回滚对Person对象的修改
我再次请求端点获取id为"1"的人,但总是抛出ClassCastException

infinispan使用的编组器是JavaSerializationMarshaller。

我想,如果类已经重新编译,那么java序列化不允许取消对缓存数据的访问。

但我想知道如何避免这种情况,尤其是能够管理类的更新(添加/删除字段),而在访问缓存数据时不会出现异常。

有人有解决方案吗?

我最终创建了自己的Marshaller,它在JSON中序列化/反序列化,灵感来自以下类:GenericJackson2JsonRedisSerializer.java

public class JsonMarshaller extends AbstractMarshaller {
private static final byte[] EMPTY_ARRAY = new byte[0];
private final ObjectMapper objectMapper;
public JsonMarshaller() {
this.objectMapper = objectMapper();
}
private ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// Serialize/Deserialize objects from any fields or creators (constructors and (static) factory methods). Ignore getters/setters.
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.ANY);
objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
// Register support of other new Java 8 datatypes outside of date/time: most notably Optional, OptionalLong, OptionalDouble
objectMapper.registerModule(new Jdk8Module());
// Register support for Java 8 date/time types (specified in JSR-310 specification)
objectMapper.registerModule(new JavaTimeModule());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
objectMapper.registerModule(new SimpleModule("NullValue Module").addSerializer(new NullValueSerializer(null)));
objectMapper.registerModule(
new SimpleModule("SimpleKey Module")
.addSerializer(new SimpleKeySerializer())
.addDeserializer(SimpleKey.class, new SimpleKeyDeserializer(objectMapper))
);
return objectMapper;
}
@Override
protected ByteBuffer objectToBuffer(Object o, int estimatedSize) throws IOException, InterruptedException {
return ByteBufferImpl.create(objectToBytes(o));
}
private byte[] objectToBytes(Object o) throws JsonProcessingException {
if (o == null) {
return EMPTY_ARRAY;
}
return objectMapper.writeValueAsBytes(o);
}
@Override
public Object objectFromByteBuffer(byte[] buf, int offset, int length) throws IOException, ClassNotFoundException {
if (isEmpty(buf)) {
return null;
}
return objectMapper.readValue(buf, Object.class);
}
@Override
public boolean isMarshallable(Object o) throws Exception {
return true;
}
@Override
public MediaType mediaType() {
return MediaType.APPLICATION_JSON;
}
private static boolean isEmpty(byte[] data) {
return (data == null || data.length == 0);
}
/**
* {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of {@link NullValue}.
*/
private static class NullValueSerializer extends StdSerializer<NullValue> {
private static final long serialVersionUID = 1999052150548658808L;
private final String classIdentifier;
/**
* @param classIdentifier can be {@literal null} and will be defaulted to {@code @class}.
*/
NullValueSerializer(String classIdentifier) {
super(NullValue.class);
this.classIdentifier = StringUtils.isNotBlank(classIdentifier) ? classIdentifier : "@class";
}
@Override
public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeStringField(classIdentifier, NullValue.class.getName());
jgen.writeEndObject();
}
}
}

SimpleKey对象的序列化程序/反序列化程序:

public class SimpleKeySerializer extends StdSerializer<SimpleKey> {
private static final Logger LOG = LoggerFactory.getLogger(SimpleKeySerializer.class);
protected SimpleKeySerializer() {
super(SimpleKey.class);
}
@Override
public void serialize(SimpleKey simpleKey, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
serializeFields(simpleKey, gen, provider);
gen.writeEndObject();
}
@Override
public void serializeWithType(SimpleKey value, JsonGenerator gen, SerializerProvider provider, TypeSerializer typeSer) throws IOException {
WritableTypeId typeId = typeSer.typeId(value, JsonToken.START_OBJECT);
typeSer.writeTypePrefix(gen, typeId);
serializeFields(value, gen, provider);
typeSer.writeTypeSuffix(gen, typeId);
}
private void serializeFields(SimpleKey simpleKey, JsonGenerator gen, SerializerProvider provider) {
try {
Object[] params = (Object[]) FieldUtils.readField(simpleKey, "params", true);
gen.writeArrayFieldStart("params");
gen.writeObject(params);
gen.writeEndArray();
} catch (Exception e) {
LOG.warn("Could not read 'params' field from SimpleKey {}: {}", simpleKey, e.getMessage(), e);
}
}
}
public class SimpleKeyDeserializer extends StdDeserializer<SimpleKey> {
private final ObjectMapper objectMapper;
public SimpleKeyDeserializer(ObjectMapper objectMapper) {
super(SimpleKey.class);
this.objectMapper = objectMapper;
}
@Override
public SimpleKey deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Object> params = new ArrayList<>();
TreeNode treeNode = jp.getCodec().readTree(jp);
TreeNode paramsNode = treeNode.get("params");
if (paramsNode.isArray()) {
for (JsonNode paramNode : (ArrayNode) paramsNode) {
Object[] values = this.objectMapper.treeToValue(paramNode, Object[].class);
params.addAll(Arrays.asList(values));
}
}
return new SimpleKey(params.toArray());
}
}

我配置了如下的infinispan:

<?xml version="1.0" encoding="UTF-8"?>
<infinispan xmlns="urn:infinispan:config:12.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:12.1 https://infinispan.org/schemas/infinispan-config-12.1.xsd">
<cache-container default-cache="default">
<serialization marshaller="com.example.JsonMarshaller">
<allow-list>
<regex>com.example.*</regex>
</allow-list>
</serialization>
<local-cache-configuration name="mirrorFile">
<persistence passivation="false">
<file-store path="${infinispan.disk.store.dir}"
shared="false"
preload="false"
purge="false"
segmented="false">
</file-store>
</persistence>
</local-cache-configuration>
<local-cache name="person" statistics="true" configuration="mirrorFile">
<memory max-count="500"/>
<expiration lifespan="86400000"/>
</local-cache>
</cache-container>
</infinispan>

最好的选择是将缓存编码更改为application/x-protostream,并使用ProtoStream库序列化对象。

<local-cache-configuration name="mirrorFile">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</local-cache>

Infinispan缓存默认在内存中保存实际的Java对象,而不序列化它。配置的整理器仅用于将条目写入磁盘。

当您修改类时,Spring可能会在新的类加载器中创建一个同名的新类。但是缓存中的对象仍然使用旧类加载器中的类,因此它们与新类不兼容。

配置application/x-java-object以外的编码媒体类型会告诉Infinispan也要序列化驻留在内存中的对象。

您还可以将缓存编码更改为application/x-java-serialized-object,以便使用已用于在磁盘上存储对象的JavaSerializationMarshaller将对象存储在内存中。但是,使用Java序列化来保持与旧版本的兼容性是一项艰巨的工作,并且需要提前规划:您需要一个serialVersionUUID字段,可能是一个版本字段,以及一个可以读取旧格式的readExternal()实现。使用ProtoStream,因为它基于Protobuf模式,只要不更改或重用字段号,就可以轻松添加新的(可选)字段并忽略不再使用的字段。

相关内容

  • 没有找到相关文章

最新更新