我正在编写一个使用Jackson进行序列化的Spring Boot应用程序(RESTful webservice)。我有以下数据模型,这些数据模型将在服务及其 HTTP 客户端之间来回发送(因此这些模型将在 JSON 之间序列化/反序列化):
public abstract class BaseEntity {
@JsonIgnore
private Long id;
private UUID refId;
// Getters, setters, ctors, etc.
}
public abstract class BaseLookup extends BaseEntity {
private String name;
private String label;
private String description;
// Getters, setters, ctors, etc.
}
public class State extends BaseLookup {
private String abbrev; // "VT", "FL", etc.
// Getters, setters, ctors, etc.
}
public class Contact extends BaseEntity {
private String givenName;
private String surname;
private State state;
// Getters, setters, ctors, etc.
}
public class Account extends BaseEntity {
private Contact contact;
private String code;
// lots of other fields that will be generated server-side
// Getters, setters, ctors, etc.
}
因此,将有一些端点用于 CRUDdingAccounts
,其他端点用于 CRUDdingContacts
,等等。例如,AccountController
将公开 CRUDdingAccount
实例的端点:
@RestController
@RequestMapping(value = "/accounts")
public class AccountController {
@RequestMapping(method = RequestMethod.POST)
public void createAccount(@RequestBody Account account) {
// Do stuff and persist the account to the DB
}
}
我想简化HTTP客户端必须制作的JSON,以便创建新的Account
,Contact
等实例。同时,这些数据模型上有一些字段,我不想向客户端公开。类似于BaseEntity#id
的东西(这是数据库中实体的PK)。或者例如,在State
的情况下,我只是希望客户端了解(并使用)abbrev
字段等。我不希望他们看到其他BaseLookup
领域,甚至不知道他们。
因此,我的最终目标是允许客户端发布以下 JSON,并让自定义 Jackson 反序列化程序将该 JSON 转换为Account
实例:
{
"contact" : {
"givenName" : "Him",
"surname" : "Himself",
"state" : "NY"
},
"code" : "12345"
}
所以你看,就像我上面说的,这个JSON完成了几件事:
- 客户端在 POST 创建新实例时不提供
BaseEntity#id
或BaseEntity#refId
- 对于
contact.state
字段,这是一个具有多个字段(id
、refId
、name
、label
、description
、abbrev
)的BaseLookup
,用户只需要提供abbrev
字段,反序列化程序应该找出客户端引用的State
Account
类实际上还有许多其他在服务器端推断/生成的字段;客户端不需要知道它们就可以创建一个Account
实例。- 上面的JSON是如果我们使用Jackson的默认行为序列化
Account
将得到的简化形式;这是为了使客户端的事情更容易,在服务器端甚至更安全(不公开PK等)。
这里需要注意的重要一点是,发送到此控制器的contact
字段的 JSON 与将 POST 到ContactController
以创建新的Contact
实例的 JSON 相同。
问题是这样的:
public class AccountDeserializer extends StdDeserializer<Account> {
public AccountDeserializer() {
this(null);
}
public AccountDeserializer(Class<Account> accClazz) {
super(accClazz);
}
@Override
public Account deserialize(JsonParser jsonParser, DeserializationContext dCtx)
throws IOException, JsonProcessingException {
JsonNode jsonNode = jsonParser.codec.readTree(jsonParser)
Contact contact = ??? // TODO: How to invoke ContactDeserializer here?
String accountCode = node.get("code").asText();
// Generate lots of other Account field values here...
Account account = new Account(contact, accountCode, /* other fields here */);
return account;
}
}
由于我还将有一个ContactController
(用于 CRUDdingContact
实例,而不考虑关联的Account
),并且因为我有类似的愿望从客户端隐藏Contact
字段以及简化进入此ContactController#createContact
端点的JSON,因此除了此AccountDeserializer
之外,我还需要一个ContactDeserializer
......
public class ContactDeserializer extends StdDeserializer<Contact> {
// ...etc.
}
此ContactDeserializer
将负责将 JSON 转换为Contact
实例。但是由于Account
实例也包含Contact
实例,并且由于外部"帐户 JSON"中的"联系人 JSON"将与客户端发送到任何"联系终端节点"的任何 JSON 相同,因此我想以某种方式从AccountDeserializer
内部调用ContactDeserializer
。
这样,当ContactController
收到"联系 JSON"以创建新的Contact
实例时,ContactDeserializer
将参与完成工作。而且,如果AccountController
收到"帐户 JSON"以创建新的Account
实例,则AccountDeserializer
将参与完成该工作......它还使用该ContactDeserialzer
来处理帐户 JSON 的内部contact
字段的反序列化。
这能做到吗?!一个杰克逊反序列化器可以在其中重用其他反序列化器吗?如果是这样,如何?如果没有,那么这里的解决方案是什么?!
您可以通过调用ObjectCodec
的treeToValue
方法来调用ContactDeserializer
。如果您已在ObjectMapper
上注册了ContactDeserializer
,杰克逊将自动为您领取。
public class AccountDeserializer extends JsonDeserializer<Account> {
@Override
public Account deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
JsonNode node = p.readValueAsTree();
JsonNode contactNode = node.get("contact");
Contact contact = null;
if (contactNode != null) {
contact = p.getCodec().treeToValue(contactNode, Contact.class);
}
return new Account(contact, /* account properties */);
}
}
编辑
如果要将反序列化程序添加到由 Spring Boot 创建的现有映射器中,可以在其中一个配置类中自动连接它并根据需要进行自定义。
@Configuration
public class ObjectMapperConfiguration {
@Autowired
public void configureObjectMapper(ObjectMapper mapper) {
SimpleModule module = new SimpleModule()
.addDeserializer(Account.class, new AccountDeserializer())
.addDeserializer(Contact.class, new ContactDeserializer());
mapper.registerModule(module);
}
}