无法使用 Jersey 客户端解组 JSON 对象数组

Posted

技术标签:

【中文标题】无法使用 Jersey 客户端解组 JSON 对象数组【英文标题】:Cannot unmarshal a JSON array of objects using Jersey Client 【发布时间】:2012-03-26 11:56:15 【问题描述】:

我正在尝试解组的单元素 JSON 数组:

[
   
      "id":"42",
      "status":"Active",
      "name":"purple monkey dishwasher"
   
]

对应的 Java 类(为简洁起见省略了 getter 和 setter):

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Badge

    @XmlElement(name="id")
    private String id;

    @XmlElement(name="status")
    private Status status;

    @XmlElement(name="name")
    private String name;

    public static enum Status
    
        Active,
        NotActive
    

发出 HTTP 请求的 Jersey 客户端代码应该将上述 JSON 解组为单元素 List<Foo>

Client client = Client.create();
WebResource apiRoot = client.resource("http://localhost:9000/api");
List<Badge> badges = apiRoot.path("/badges").get(new GenericType<List<Badge>>());

最后一行,特别是WebResource#get() 调用,抛出以下异常:

javax.xml.bind.UnmarshalException: unexpected element (uri:"", local:"status"). Expected elements are <badge>
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.handleEvent(UnmarshallingContext.java:662)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportError(Loader.java:258)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportError(Loader.java:253)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportUnexpectedChildElement(Loader.java:120)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext$DefaultRootLoader.childElement(UnmarshallingContext.java:1063)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext._startElement(UnmarshallingContext.java:498)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.startElement(UnmarshallingContext.java:480)
    at com.sun.xml.bind.v2.runtime.unmarshaller.InterningXmlVisitor.startElement(InterningXmlVisitor.java:75)
    at com.sun.xml.bind.v2.runtime.unmarshaller.StAXStreamConnector.handleStartElement(StAXStreamConnector.java:247)
    at com.sun.xml.bind.v2.runtime.unmarshaller.StAXStreamConnector.bridge(StAXStreamConnector.java:181)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:369)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal(UnmarshallerImpl.java:341)
    at com.sun.jersey.core.provider.jaxb.AbstractListElementProvider.readFrom(AbstractListElementProvider.java:232)
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:552)
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:522)
    at com.sun.jersey.api.client.WebResource.handle(WebResource.java:617)
    at com.sun.jersey.api.client.WebResource.get(WebResource.java:191)
    at com.redacted.badge.client.BadgerImpl.findAllBadges(BadgerImpl.java:105)
    at com.redacted.webapp.admin.BadgeAction.unspecified(BadgeAction.java:40)
    at org.apache.struts.actions.DispatchAction.dispatchMethod(DispatchAction.java:245)
    at org.apache.struts.actions.DispatchAction.execute(DispatchAction.java:170)
    at org.apache.struts.chain.commands.servlet.ExecuteAction.execute(ExecuteAction.java:58)
    at org.apache.struts.chain.commands.AbstractExecuteAction.execute(AbstractExecuteAction.java:67)
    at org.apache.struts.chain.commands.ActionCommandBase.execute(ActionCommandBase.java:51)
    at org.apache.commons.chain.impl.ChainBase.execute(ChainBase.java:190)
    at org.apache.commons.chain.generic.LookupCommand.execute(LookupCommand.java:304)
    at org.apache.commons.chain.impl.ChainBase.execute(ChainBase.java:190)
    at org.apache.struts.chain.ComposableRequestProcessor.process(ComposableRequestProcessor.java:283)
    at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1913)
    at org.apache.struts.action.ActionServlet.doGet(ActionServlet.java:449)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:119)
    at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:55)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.MemberFilter.doFilter(MemberFilter.java:83)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.AuthFilter.doFilter(AuthFilter.java:113)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.displaytag.filter.ResponseOverrideFilter.doFilter(ResponseOverrideFilter.java:125)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.LanguageHandlingFilter.doFilter(LanguageHandlingFilter.java:151)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.SetCharacterEncodingFilter.doFilter(SetCharacterEncodingFilter.java:146)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.PartnerFilter.doFilter(PartnerFilter.java:59)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.SessionStatusFilter.doFilter(SessionStatusFilter.java:113)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:470)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
    at com.googlecode.psiprobe.Tomcat60AgentValve.invoke(Tomcat60AgentValve.java:30)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
    at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:859)
    at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:588)
    at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
    at java.lang.Thread.run(Thread.java:680)

我在Badge 上尝试了各种注释组合,或者使用数组代替GenericType

List<Badge> badges = Arrays.asList(apiRoot.path("/badges").get(Badge[].class));

或使用中间ClientResponse

GenericType<List<Badge>> type = new GenericType<List<Badge>>();
ClientResponse clientResponse = apiRoot.path("/badges").get(ClientResponse.class);
List<Badge> badges = clientResponse.getEntity(type);

但目前还没有解决问题。

更令人困惑的是,我现有的设置没有问题解组其他结构内的 JSON 编码 Badges,如下所示:


   "userid":"123456789",
   "userbadges":[
      
         "badge":
              "id":"42",
              "status":"Active",
              "name":"purple monkey dishwasher"
         ,
         "earned":"2012-03-06 18:16:18.172"
      
   ]

我做错了什么?

【问题讨论】:

您是否尝试将状态枚举类型提取到它自己的类中并在该类中添加@XmlEnum 注释? 状态字段不是问题,不是远程的。 【参考方案1】:

通过使用 JacksonJsonProvider 作为 Jersey 客户端实例的 MessageBody(Reader|Writer) 提供程序,我能够以最小的努力解决这个问题:

ClientConfig cfg = new DefaultClientConfig();
cfg.getClasses().add(JacksonJsonProvider.class);
Client client = Client.create(cfg);

Jackson 的 MessageBodyReader 实现似乎比 Jersey JSON 实现的更好。

感谢How can I customize serialization of a list of JAXB objects to JSON? 为我指明杰克逊方向。

【讨论】:

这种方式没有帮助。但是new DefaultClientConfig(JacksonJaxbJsonProvider.class) 可以。【参考方案2】:

注意:我是EclipseLink JAXB (MOXy) 领导,也是JAXB (JSR-222) 专家组的成员。

您可以使用在 EclipseLink 2.4 中添加到 MOXy 组件的 JSON Binding 扩展来处理这个用例:

演示

Jersey 客户端 API 允许您在客户端的服务器端利用相同的 MessageBodyReader/MessageBodyWriter

package forum9627170;

import java.util.List;
import org.example.Customer;
import com.sun.jersey.api.client.*;
import com.sun.jersey.api.client.config.*;

public class Demo 

    public static void main(String[] args) 
        ClientConfig cc = new DefaultClientConfig();
        cc.getClasses().add(MOXyJSONProvider.class);
        Client client = Client.create(cc);
        WebResource apiRoot = client.resource("http://localhost:9000/api");
        List<Badge> badges = apiRoot.path("/badges").accept("application/json").get(new GenericType<List<Badge>>());

        for(Badge badge : badges) 
            System.out.println(badge.getId());
        
    


MOXyJSONProvider

下面是一个通用的MessageBodyReader/MessageBodyWriter,可以与任何服务器/客户端一起使用,以启用 MOXy 作为 JSON 绑定提供程序。

package forum9627170;

import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.xml.transform.stream.StreamSource;

import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.*;
import javax.xml.bind.*;

@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class MOXyJSONProvider implements 
    MessageBodyReader<Object>, MessageBodyWriter<Object>

    @Context
    protected Providers providers;

    public boolean isReadable(Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) 
        return true;
    

    public Object readFrom(Class<Object> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException, WebApplicationException 
            try 
                Class domainClass = getDomainClass(genericType);
                Unmarshaller u = getJAXBContext(domainClass, mediaType).createUnmarshaller();
                u.setProperty("eclipselink.media-type", mediaType.toString());
                u.setProperty("eclipselink.json.include-root", false);
                return u.unmarshal(new StreamSource(entityStream), domainClass).getValue();
             catch(JAXBException jaxbException) 
                throw new WebApplicationException(jaxbException);
            
    

    public boolean isWriteable(Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) 
        return true;
    

    public void writeTo(Object object, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders,
        OutputStream entityStream) throws IOException,
        WebApplicationException 
        try 
            Marshaller m = getJAXBContext(getDomainClass(genericType), mediaType).createMarshaller();
            m.setProperty("eclipselink.media-type", mediaType.toString());
            m.setProperty("eclipselink.json.include-root", false);
            m.marshal(object, entityStream);
         catch(JAXBException jaxbException) 
            throw new WebApplicationException(jaxbException);
        
    

    public long getSize(Object t, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) 
        return -1;
    

    private JAXBContext getJAXBContext(Class<?> type, MediaType mediaType) 
        throws JAXBException 
        ContextResolver<JAXBContext> resolver 
            = providers.getContextResolver(JAXBContext.class, mediaType);
        JAXBContext jaxbContext;
        if(null == resolver || null == (jaxbContext = resolver.getContext(type))) 
            return JAXBContext.newInstance(type);
         else 
            return jaxbContext;
        
    

    private Class<?> getDomainClass(Type genericType) 
        if(genericType instanceof Class) 
            return (Class) genericType;
         else if(genericType instanceof ParameterizedType) 
            return (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0];
         else 
            return null;
        
    



更多信息

MOXy as Your JAX-RS JSON Provider - Client Side MOXy as Your JAX-RS JSON Provider - Server Side Specifying EclipseLink MOXy as Your JAXB Provider

更新

在 GlassFish 4 中,EclipseLink JAXB (MOXy) 是 Jersey 使用的默认 JSON 绑定提供程序:

http://blog.bdoughan.com/2013/06/moxy-is-new-default-json-binding.html

【讨论】:

谢谢,我会试一试。我很想知道为什么这不能开箱即用——你知道吗?另外,我知道直到您发布答案:) 只是时间问题 @MДΓΓБДLL - 我已经更新了我的答案。我相信它不是开箱即用的,因为您收藏中的 then 项目没有 badge 作为根元素。 哦,有趣。再看看我可以解组的 JSON 和我不能解组的 JSON 之间的区别,我明白你的意思了。 这种方法不是标准的。要在 Jersey 中生成和使用标准 JSON,必须使用 Jackson!这很简单,快速,并且是由 Jersey 团队设计的。 @yvesamsellem - (-1?) 这种方法使用标准的“MessageBodyReader/Writer”接口与 Jersey 客户端 API 进行交互,并清楚地表明您不需要使用 Jackson 来生产和消费标准JSON 到泽西岛。早期版本的 MOXy 已在 GlassFish (blog.bdoughan.com/2012/02/glassfish-312-is-full-of-moxy.html) 中,包含 JSON 绑定的版本将包含在未来的 GlassFish 版本中。堆栈跟踪清楚地表明 JAXB 实现正在与 JSON 一起使用,并且 MOXy 支持比 Jackson 更多的 JAXB 注释。【参考方案3】:

默认情况下,Jersey 使用 JAXB 进行(非)编组过程,不幸的是,JAXB JSON 处理器不是标准的(忽略单元素数组,将空数组转换为单元素空数组。 ..)。

所以,你有两个选择:

    将 JAXB 配置为更标准(see here 了解更多); 使用 Jackson 而不是 JAXB — 我建议这样做。

使用 Jackson 客户端的方式如下:

ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
Client client = Client.create(clientConfig);
List<Badge> badges = client.resource("/badges").getEntity(new GenericType<List<Badge>>() );

【讨论】:

是的,正如my self-answer 所说,我正在尝试杰克逊,这似乎不那么古怪了。 很高兴知道。仅当您想要自定义(取消)编组过程时,才需要使用自定义 JacksonProvider。如果你不这样做,使用 Jersey 提供的默认值就足够了(JSONConfiguration 特性)。 碰巧的是,我实际上使用的是自定义提供程序,这样我就可以将 JAXB 注释与 Jackson 注释一起使用:wiki.fasterxml.com/… 默认的 Jersey Jackson 提供程序配置为使用 JAXB 注释和 Jackson 注释 ;-) 唯一的问题是该提供程序的确切配置没有记录:-(【参考方案4】:

我遇到了类似的问题,通过以下方法解决了

    像这样制作一个 JAXB 上下文解析器

    import java.util.ArrayList;
    import java.util.List;
    
    import javax.ws.rs.ext.ContextResolver;
    import javax.ws.rs.ext.Provider;
    import javax.xml.bind.JAXBContext;
    
    import com.sun.jersey.api.json.JSONConfiguration;
    import com.sun.jersey.api.json.JSONJAXBContext;
    
    @Provider
    public class JAXBContextResolver implements ContextResolver<JAXBContext> 
    
        private JAXBContext       context;
    
        private Class<?>[]        types    =  Badge.class ;
    
        private List<Class<?>>    classes    = new ArrayList<Class<?>>();
    
        public JAXBContextResolver() throws Exception 
            this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), types);
    
            for (Class<?> clazz : types) 
                classes.add(clazz);
            
        
    
        public JAXBContext getContext(Class<?> objectType) 
            return classes.contains(objectType) ? context : null;
        
    
    
    

    将上下文解析器添加到您的客户端

    ClientConfig config = new DefaultClientConfig();
    config.getClasses().add(JAXBContextResolver.class);
    
    Client client = Client.create(config);
    

    现在你可以得到对象数组了

    WebResource apiRoot = client.resource("http://localhost:9000/api");
    Badge[] badges = apiRoot.path("/badges").get(Badge[].class);
    

如果你想要一个列表,只需使用

   Arrays.asList(badges)

【讨论】:

【参考方案5】:

导入这个

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.17</version>
    <scope>compile</scope>
</dependency>

这是解组的代码

import com.sun.jersey.api.json.JSONJAXBContext;
import com.sun.jersey.api.json.JSONUnmarshaller;
public static <T> T unmarshalJson(String jsonTxt, Class<T> clazz) throws JAXBException 
    JSONJAXBContext jctx = new JSONJAXBContext(clazz);
    JSONUnmarshaller unm = jctx.createJSONUnmarshaller();
    return (T)unm.unmarshalFromJSON(new StringReader(jsonTxt), clazz);

【讨论】:

【参考方案6】:

这可能与生产者未将单例列表正确编码为 JSON 的问题有关。请参阅this article 以获得更完整的解释和建议的解决方案。

根据文章描述的内容以及错误消息,我猜测正在生成以下内容:


   
      "id":"42",
      "status":"Active",
      "name":"purple monkey dishwasher"
   

根据文章,解决方案在于扩展和自定义提供程序,以纠正单例列表和空列表如何格式化为 JSON。

很遗憾,这篇文章是德文的,我必须自己翻译 - 如果它不能真正解决您的问题,请告诉我。如果确实如此,则归功于该文章的作者 Dirk Dittmar。

PS - 如果您像我一样使用 Chrome 翻译页面,请确保切换回原始代码以查看代码 sn-ps,因为它们的一部分被错误地“翻译”为空白。

【讨论】:

我(基本上)只使用JacksonJsonProvider,而不是通过繁琐的创建自己的提供程序。 @MДΓΓБДLL - 听起来不错!感谢您的有趣帖子。

以上是关于无法使用 Jersey 客户端解组 JSON 对象数组的主要内容,如果未能解决你的问题,请参考以下文章

Akka HTTP:如何将 Json 格式响应解组为域对象

json:无法将字符串解组为 MyMap.map 类型的 Go 值

Grails 无法将 JSON 中的日期/时间解组回 joda DateTime

无法将字符串解组为 int64 类型的 Go 值

MarshallingException:无法解组结果参考

无法模拟 Glassfish Jersey 客户端响应对象