Spring Boot + Infinispan embedded - 当要缓存的对象被修改时,如何防止ClassCastException?
Spring Boot + Infinispan embedded - how to prevent ClassCastException when the object to be cached has been modified?
我有一个带有 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;
}
}
这是人物 class:
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 如果 class 已经被重新编译,序列化不允许取消缓存数据。
但我想知道如何避免这种情况,尤其是在访问缓存数据时能够管理 class(adding/removing 字段)的更新而不会出现异常。
有人有解决办法吗?
最好的选择是将缓存编码更改为 application/x-protostream
并序列化您的对象 using the ProtoStream library.
<local-cache-configuration name="mirrorFile">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</local-cache>
Infinispan 缓存默认在内存中保存实际的 Java 对象,而不对其进行序列化。配置的编组器仅用于将条目写入磁盘。
当您修改 class 时,Spring 可能会在新的 class 加载程序中创建一个同名的新 class。但是缓存中的对象仍然使用旧 classloader 中的 class,因此它们与新 class.
不兼容
配置 application/x-java-object
以外的编码媒体类型会告诉 Infinispan 序列化内存中的对象。
您还可以将缓存编码更改为 application/x-java-serialized-object
,以便您的对象使用 JavaSerializationMarshaller
存储在内存中,它已经用于在磁盘上存储对象。但是使用 Java 序列化来维护与旧版本的兼容性需要做很多工作,并且需要提前计划:您需要一个 serialVersionUUID
字段,可能是一个版本字段,以及一个 readExternal()
实现可以读取旧格式。使用 ProtoStream,因为它基于 Protobuf 模式,您可以轻松添加新的(可选)字段并忽略不再使用的字段,只要您不更改或重复使用字段编号。
我终于在 JSON 中创建了自己的编组器 serialize/deserialize,受到以下 class 的启发: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 对象的Serializer/Deserializer:
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>
我有一个带有 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;
}
}
这是人物 class:
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 如果 class 已经被重新编译,序列化不允许取消缓存数据。
但我想知道如何避免这种情况,尤其是在访问缓存数据时能够管理 class(adding/removing 字段)的更新而不会出现异常。
有人有解决办法吗?
最好的选择是将缓存编码更改为 application/x-protostream
并序列化您的对象 using the ProtoStream library.
<local-cache-configuration name="mirrorFile">
<encoding>
<key media-type="application/x-protostream"/>
<value media-type="application/x-protostream"/>
</encoding>
</local-cache>
Infinispan 缓存默认在内存中保存实际的 Java 对象,而不对其进行序列化。配置的编组器仅用于将条目写入磁盘。
当您修改 class 时,Spring 可能会在新的 class 加载程序中创建一个同名的新 class。但是缓存中的对象仍然使用旧 classloader 中的 class,因此它们与新 class.
不兼容配置 application/x-java-object
以外的编码媒体类型会告诉 Infinispan 序列化内存中的对象。
您还可以将缓存编码更改为 application/x-java-serialized-object
,以便您的对象使用 JavaSerializationMarshaller
存储在内存中,它已经用于在磁盘上存储对象。但是使用 Java 序列化来维护与旧版本的兼容性需要做很多工作,并且需要提前计划:您需要一个 serialVersionUUID
字段,可能是一个版本字段,以及一个 readExternal()
实现可以读取旧格式。使用 ProtoStream,因为它基于 Protobuf 模式,您可以轻松添加新的(可选)字段并忽略不再使用的字段,只要您不更改或重复使用字段编号。
我终于在 JSON 中创建了自己的编组器 serialize/deserialize,受到以下 class 的启发: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 对象的Serializer/Deserializer:
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>