Jersey,Jackson和JAX-RS POST多种JSON格式 [英] Jersey, Jackson and JAX-RS POST multiple JSON formats
问题描述
我正在尝试定义以下代码:
I am trying to be able to define the following code:
public class MyObject {
private String name;
... // Other attributes
}
@Path(...)
@Stateless
public class MyRestResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(List<MyObject> myObjects) {
// Do some stuff there
}
}
我知道我需要使用:
DeserializationConfig.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true
正确设置对象映射器能够在我的其余资源上接受单个值作为数组。我成功设置了那个部分。
to setup correctly my object mapper to be able to accept single value as array on my rest resources. I succeed to setup that part.
这种方法的问题在于以下内容不可区分:
My problem with this approach is that the following content is not differentiable:
{
"name": "a name",
... // other attributes
}
和
[{
"name": "a name",
... // other attributes
}]
将导致大小为1的列表(List)。然后,在方法create(List myObjects)中,我将无法区分List和发送到Rest资源的单个对象。
will result into a list (List) of size one. Then, in the method create(List myObjects), I will not be able to do the difference between the List and the Single Object sent to the Rest Resource.
然后,我的问题是如何做这样的事情。想法是只有一个接受Arrays和Single值的@POST?
Then, my question is how to do something like that. The idea is to have only one @POST that accepts both Arrays and Single values?
理想情况下,我将摆脱ObjectMapper的配置以避免让将Single Object设置为JSON文档的另一个级别。例如,我不想允许:
Ideally, I will get rid of the configuration of the ObjectMapper to avoid letting the possibility to set Single Object into the other level of the JSON document. For example, I do not want to allow that:
{
...
"attributes": {
...
}
}
通常情况下这种格式应该是强制性的:
where normally this format should be mandatory:
{
...
"attributes": [{
...
}]
}
基于此,我试图设置我的List的对象包装器来设置我是否能够区分列表和对象。有类似的东西:
Based on that, I tried to put in place an object wrapper of my List to set if I am able to the difference between the list and the object. With something like that:
public class ObjectWrapper<T> {
private List<T> list;
private T object;
public boolean isObject() {
return list == null;
}
}
资源变为:
@Path(...)
@Stateless
public class MyRestResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(ObjectWrapper myObjects) {
// Do some stuff there
}
}
并尝试通过JAX-RS / Jersey / Jackson机制对我的内容进行反序列化。如果我现在让解决方案,反序列化失败,因为预期的JSON格式如下:
and trying to put in place the deserialization of my content through the JAX-RS/Jersey/Jackson mechanisms. If I let the solution as it is now, the deserialization fails due to the fact that the JSON format expected is the following:
{
"list": [{
"name": "a name",
... // other attributes
}]
}
然后我尝试编写自定义反序列化器,但我在此任务中有点迷失。我有类似的东西:
Then I tried to write a custom deserializer but I am a bit lost in this task. I have something like that:
public class ObjectWrapperDeserializer<T> extends JsonDeserializer<T> {
@Override
public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
... // What to put there to deserialize Array or Object
}
}
我只想反序列化根级别,将反序列化的内容设置为对象包装器。当完成不同@Provider的配置时,我还希望在使用@ApplicationPath注释的类中配置该功能。
I just want to deserialize the root level to set the content deserialized into the object wrapper. I also want to keep the feature configured in a class annotated with @ApplicationPath when the configuraiton of the different @Provider are done.
我希望所有信息都能给出一个充分了解我想要做什么以及我已经测试过的内容。
I hope that all the info will give a sufficient picture of what I want to do and what I already tested.
等待关于如何在同一路径上接受数组或对象的资源的建议。
Waiting for suggestion on how to do a resource that accept Arrays or Objects on the same path.
提前多多谢谢。
推荐答案
好的,最后我成功了建立一个完全符合我要求的机制。但是,我不确定是否会产生诸如性能或此类事情的负面后果。
Ok, finally I succeed to put in place a mechanism that do exactly what I am looking for. But, I am not sure if there are negative consequences such the performance or such things.
首先,我定义了一个可以接受List或Single Object的类:
First, I defined a class that can accept both List or Single Object:
public class RootWrapper<T> {
private List<T> list;
private T object;
}
然后,我需要一个能够知道哪种T的自定义反序列化器用于反序列化和处理集合或单个对象的类型。
Then, I need a custom deserializer that is able to know which kind of T type to deserialize and to handle the collection or the single object.
public class RootWrapperDeserializer extends JsonDeserializer<CollectionWrapper<?>> {
private Class contentType;
public RootWrapperDeserializer(Class contentType) {
this.contentType = contentType;
}
@Override
public RootWrapper deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// Retrieve the object mapper and read the tree.
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode root = mapper.readTree(jp);
RootWrapper wrapper = new RootWrapper();
// Check if the root received is an array.
if (root.isArray()) {
List list = new LinkedList();
// Deserialize each node of the array using the type expected.
Iterator<JsonNode> rootIterator = root.getElements();
while (rootIterator.hasNext()) {
list.add(mapper.readValue(rootIterator.next(), contentType));
}
wrapper.setList(list);
}
// Deserialize the single object.
else {
wrapper.setObject(mapper.readValue(root, contentType));
}
return wrapper;
}
}
据我所知,我尝试只反序列化手动完成根级别然后让Jackson接下来的操作。我只需要知道我期望在Wrapper中出现哪种真实类型。
As far as I know, I try to only deserialize the root level manually and then let Jackson take the next operations in charge. I only have to know which real type I expect to be present in the Wrapper.
在这个阶段,我需要一种方法告诉Jersey / Jackson使用哪个解串器。我找到的一种方法是创建一种反序列化器注册表,其中存储了使用正确的反序列化器反序列化的类型。我扩展了Deserializers.Base类。
At this stage, I need a way to tell Jersey/Jackson which deserializer to use. One way I found for that is to create a sort of deserializer registry where are stored the type to deserialize with the right deserializer. I extended the Deserializers.Base class for that.
public class CustomDeserializers extends Deserializers.Base {
// Deserializers caching
private Map<Class, RootWrapperDeserializer> deserializers = new HashMap<>();
@Override
public JsonDeserializer<?> findBeanDeserializer(JavaType type,
DeserializationConfig config, DeserializerProvider provider,
BeanDescription beanDesc, BeanProperty property) throws JsonMappingException {
// Check if we have to provide a deserializer
if (type.getRawClass() == RootWrapper.class) {
// Check the deserializer cache
if (deserializers.containsKey(type.getRawClass())) {
return deserializers.get(type.getRawClass());
}
else {
// Create the new deserializer and cache it.
RootWrapperDeserializer deserializer =
new RootWrapperDeserializer(type.containedType(0).getRawClass());
deserializers.put(type.getRawClass(), deserializer);
return deserializer;
}
}
return null;
}
}
好的,然后我有我的反序列化器注册表创建新的反序列化仅在需要时保留并在创建后保留它们。我不确定这种方法是否存在任何并发问题。我知道Jackson做了很多缓存,并且一旦在特定的反序列化上下文中第一次调用它,就不会每次调用findBeanDeserializer。
Ok, then I have my deserializers registry that create new deserializer only on demand and keep them once created. What I am not sure about that approach is if there is any concurrency issue. I know that Jackson do a lot of caching and do not call every time the findBeanDeserializer once it was called a first time on a specific deserialization context.
现在我创建了我的不同的类,我需要做一些管道将所有东西组合在一起。在我创建ObjectMapper的提供程序中,我可以将deserializers注册表设置为创建的对象映射器,如下所示:
Now I have created my different classes, I need to do some plumbing to combine everything together. In a provider where I create the ObjectMapper, I can setup the deserializers registry to the created object mapper like below:
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class JsonObjectMapper implements ContextResolver<ObjectMapper> {
private ObjectMapper jacksonObjectMapper;
public JsonObjectMapper() {
jacksonObjectMapper = new ObjectMapper();
// Do some custom configuration...
// Configure a new deserializer registry
jacksonObjectMapper.setDeserializerProvider(
jacksonObjectMapper.getDeserializerProvider().withAdditionalDeserializers(
new RootArrayObjectDeserializers()
)
);
}
@Override
public ObjectMapper getContext(Class<?> arg0) {
return jacksonObjectMapper;
}
}
然后,我还可以定义我的@ApplicationPath我的REST应用程序如下:
Then, I can also define my @ApplicationPath that is my REST application like following:
public abstract class AbstractRestApplication extends Application {
private Set<Class<?>> classes = new HashSet<>();
public AbstractRestApplication() {
classes.add(JacksonFeature.class);
classes.add(JsonObjectMapper.class);
addResources(classes);
}
@Override
public Set<Class<?>> getClasses() {
return classes;
}
@Override
public Set<Object> getSingletons() {
final Set<Object> singletons = new HashSet<>(1);
singletons.add(new JacksonJsonProvider());
return singletons;
}
private void addResources(Set<Class<?>> classes) {
classes.add(SomeRestResource.class);
// ...
}
}
现在,一切都到位,我可以写一个这样的REST资源方法:
Now, everything is in place and I can write a REST resource method like that:
@POST
@Path("somePath")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response create(RootWrapper<SpecificClass> wrapper) {
if (wrapper.isObject()) {
// Do something for one single object
SpecificClass sc = wrapper.getObject();
// ...
return Response.ok(resultSingleObject).build();
}
else {
// Do something for list of objects
for (SpecificClass sc = wrapper.getList()) {
// ...
}
return Response.ok(resultList).build();
}
}
这就是全部。不要犹豫,评论解决方案。反馈非常受欢迎,特别是在反序列化过程中,我真的不确定它对性能和并发性是否安全。
That's all. Do not hesitate to comment the solution. Feedbacks are really welcome especially around the way of deserialization process where I am really not sure that it is safe for performance and concurrency.
这篇关于Jersey,Jackson和JAX-RS POST多种JSON格式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!