假设有一个如下所示的类:
public class Employee {
private String name;
private boolean nameSpecified;
private LocalDateTime birthday;
private boolean birthdaySpecified;
private String jobTitle;
private boolean jobTitleSpecified;
public String getName() {
if (!nameSpecified) {
throw new IllegalStateException("The property `name` is not specified");
}
return name;
}
public void setName(String name) {
this.name = name;
this.nameSpecified = true;
}
public LocalDateTime getBirthday() {
if (! birthdaySpecified) {
throw new IllegalStateException("The property `birthday` is not specified");
}
return birthday;
}
public void setBirthday(LocalDateTime name) {
this.birthday = birthday;
this.birthdaySpecified = true;
}
public String getJobTitle() {
if (!jobTitleSpecified) {
throw new IllegalStateException("The property `jobTitle` is not specified");
}
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
this.jobTitleSpecified = true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Employee");
String separator = " {";
if (nameSpecified) {
builder.append(separator).append("name=").append(name);
separator = ", ";
}
if (birthdaySpecified) {
builder.append(separator).append("birthday=").append(birthday);
separator = ", ";
}
if (jobTitleSpecified) {
builder.append(separator).append("jobTitle=").append(jobTitle);
}
return builder.append("}").toString();
}
}
很明显,Employee对象不是POJO,一些属性没有指定或设置为null是完全不同的。
这是ORM中流行的设计
- 未指定:属性值未知
- Null:值属性已知,没有数据。
例如,从 hibernate3 开始,它被惰性 scalar 属性使用。
这实际上是一种使对象动态化,同时保留强类型语言优点的方法。
显然,默认的Jackson无法序列化/反序列化这个对象,并且Jackson的行为需要被覆盖,未指定的属性必须被跳过(就像
toString
的逻辑)。
未指定的属性需要被Jackson忽略,这与@JsonFilter的要求非常相似,唯一的区别是过滤规则不是固定的,而是每个对象都是独立的。
我尝试通过自定义 Jackson 模块注册自定义序列化器和反序列化器来解决此问题。然而,我的自定义读/写策略并不像标准 Jackson 行为那么全面,并且不可避免地破坏了一些高级 Jackson 功能,例如 @JsonAlias、@JsonView、继承和多态性。
因此,我需要实现动态对象过滤而不破坏任何高级 Jackson 功能,就好像每个对象都有自己的@JsonFilter。
有没有一种优雅的方式来实现我的要求?
其实上面提到的类是从我自己的框架中提取出来的逻辑等价物,非常简单,适合作为一个独立的问题来讨论。在我的框架中,为了处理此类动态对象,我自定义了 ImmutableSerializer 和 ImmutableDeserializer 来处理动态对象的 Jackson 序列化/反序列化。然而,我的方法无法适应所有高级 Jackson 功能,并且用户报告了过去两年与某些高级 Jackson 功能被破坏相关的错误,这一直困扰着我。
如果我答对了你的问题,你可以这样做:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.io.IOException;
public class SerializerTest {
public static void main(String[] args) throws Exception {
// Create an instance of MyEntity with name and age
MyEntity entity = new MyEntity("John Doe", 30);
// Create a customized ObjectMapper for serialization
ObjectMapper mapper = createObjectMapper();
// Serialize the entity to JSON and print the output
String json = mapper.writeValueAsString(entity);
System.out.println(json);
}
public static ObjectMapper createObjectMapper() {
// Instantiate a new ObjectMapper
ObjectMapper mapper = new ObjectMapper();
// Configure the mapper to ignore empty properties for serialization
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
// Register a module to customize serialization for Property class
mapper.registerModule(new SimpleModule() {
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
// Register a serializer modifier to handle instances of Property class
context.addBeanSerializerModifier(new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
// If the property being serialized is of type Property, use the custom PropertySerializer
if (Property.class.isAssignableFrom(desc.getBeanClass())) {
return new PropertySerializer((JsonSerializer<Object>) serializer);
}
// Otherwise, use the default serializer
return serializer;
}
});
}
});
return mapper;
}
}
// Represents an entity with name and age properties, using the Property wrapper
class MyEntity {
private final Property<String> name;
private final Property<Integer> age;
// Constructor initializing name and age with the Property wrapper
public MyEntity(String name, Integer age) {
this.name = new Property<>(name);
this.age = new Property<>(age);
}
// Getters for name and age properties
public Property<String> getName() {
return name;
}
public Property<Integer> getAge() {
return age;
}
}
// A generic wrapper class for entity properties, tracking if a value has been explicitly set
class Property<T> {
private T value; // The actual value of the property
private boolean isSet = false; // Flag to track if the value has been explicitly set
// Constructor that initializes the property value and marks it as set
public Property(T value) {
setValue(value); // Using setValue ensures isSet is correctly updated
}
// Getter for the property value
public T getValue() {
return value;
}
// Setter for the property value, also marks the property as set
public void setValue(T value) {
this.value = value;
this.isSet = true;
}
// Checks if the property has been explicitly set
public boolean isSet() {
return isSet;
}
}
// Custom JsonSerializer for the Property class to handle serialization based on the isSet flag
class PropertySerializer extends JsonSerializer<Property<?>> {
private final JsonSerializer<Object> defaultSerializer; // The default serializer for the actual value of the property
// Constructor receiving the default serializer to be used for the property's value
public PropertySerializer(JsonSerializer<Object> serializer) {
defaultSerializer = serializer;
}
// Custom serialization logic that only serializes the property if it has been explicitly set
@Override
public void serialize(Property<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if(!value.isSet()) {
return; // Skip serialization if the property has not been set
}
// Use the default serializer to serialize the value of the property
defaultSerializer.serialize(value.getValue(), gen, provider);
}
// Determines if a Property is considered empty (not set), used to decide if a property should be omitted
@Override
public boolean isEmpty(SerializerProvider provider, Property<?> value) {
return !value.isSet();
}
}