jersery集成jackson实现restful api,由于jdk版本不一致导致的坑

问题背景

项目中使用jersey+jackson实现 restful api,返回信息格式为json,开发测试环境都是OK的,然鹅在线上当访问时一直报500,错误信息如下:

com.sun.jersey.spi.container.ContainerResponse write
: The registered message body writers compatible with the MIME media type are:
application/json ->
  com.sun.jersey.json.impl.provider.entity.JSONJAXBElementProvider$App
  com.sun.jersey.json.impl.provider.entity.JSONArrayProvider$App
  com.sun.jersey.json.impl.provider.entity.JSONObjectProvider$App
  com.sun.jersey.json.impl.provider.entity.JSONRootElementProvider$App
  com.sun.jersey.json.impl.provider.entity.JSONListElementProvider$App
  com.sun.jersey.core.impl.provider.entity.FormProvider
  com.sun.jersey.core.impl.provider.entity.StringProvider
  com.sun.jersey.core.impl.provider.entity.ByteArrayProvider
  com.sun.jersey.core.impl.provider.entity.FileProvider
  com.sun.jersey.core.impl.provider.entity.InputStreamProvider
  com.sun.jersey.core.impl.provider.entity.DataSourceProvider
  com.sun.jersey.core.impl.provider.entity.XMLJAXBElementProvider$General
  com.sun.jersey.core.impl.provider.entity.ReaderProvider
  com.sun.jersey.core.impl.provider.entity.DocumentProvider
  com.sun.jersey.core.impl.provider.entity.StreamingOutputProvider
  com.sun.jersey.core.impl.provider.entity.SourceProvider$SourceWriter
  com.sun.jersey.json.impl.provider.entity.JSONJAXBElementProvider$General
  com.sun.jersey.json.impl.provider.entity.JSONArrayProvider$General
  com.sun.jersey.json.impl.provider.entity.JSONObjectProvider$General
  com.sun.jersey.json.impl.provider.entity.JSONWithPaddingProvider
  com.sun.jersey.server.impl.template.ViewableMessageBodyWriter
  com.sun.jersey.core.impl.provider.entity.XMLRootElementProvider$General
  com.sun.jersey.core.impl.provider.entity.XMLListElementProvider$General
  com.sun.jersey.json.impl.provider.entity.JSONRootElementProvider$General
  com.sun.jersey.json.impl.provider.entity.JSONListElementProvider$General
  
Mapped exception to response: 500 (Internal Server Error)
javax.ws.rs.WebApplicationException: com.sun.jersey.api.MessageException: A message body writer for Java class com.account.manage.common.AccountInfoRes
ponse, and Java type class com.account.manage.common.AccountInfoResponse, and MIME media type application/json was not found
        at com.sun.jersey.spi.container.ContainerResponse.write(ContainerResponse.java:285)
        at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1479)
        at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1391)
        at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1381)
        at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:416)
        at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:538)
        at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:716)
        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 org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)

相关依赖:

jersery 依赖
 <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-server</artifactId>
      <version>1.17</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey.contribs</groupId>
      <artifactId>jersey-apache-client</artifactId>
      <version>1.17</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey.contribs</groupId>
      <artifactId>jersey-multipart</artifactId>
      <version>1.17</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-servlet</artifactId>
      <version>1.17</version>
    </dependency>
jackson依赖
  <dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-json-provider</artifactId>
    <version>2.7.4</version>
    <exclusions>
      <exclusion>
        <artifactId>jackson-databind</artifactId>
        <groupId>com.fasterxml.jackson.core</groupId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-base</artifactId>
    <version>2.7.4</version>
 </dependency>

问题分析

开发测试与线上环境唯一不同的就是jdk版本,线上是jdk1.6,本地及测试时jdk7及以上,猜测是不是由于版本不一致导致的问题,有了猜测,还需要进一步的去验证猜测是不是正确:

  • 在开发DEA中切切换项目的jdk的版本,当设置为1.6进行测试时,果然不出所料报了和线上一样的错误,这就进一步验证了确实是因为版本不一致导致的,但我们不能止步于此,需要更深入的了解到,到底是在哪里出错的,这个只能读源码了;
  • 找到报错的位置,很明显通过一层层调用,最后是在调用com.sun.jersey.spi.container.ContainerResponse.write(ContainerResponse.java:285)这个方法时里面报错,打开源代码很快能够定位好发生异常的代码处
 final MessageBodyWriter p = getMessageBodyWorkers().getMessageBodyWriter(
                entity.getClass(), entityType,
                annotations, contentType);
        if (p == null) {
            String message = "A message body writer for Java class " + entity.getClass().getName() +
                    ", and Java type " + entityType +
                    ", and MIME media type " + contentType + " was not found";
            LOGGER.severe(message);
            Map<MediaType, List<MessageBodyWriter>> m = getMessageBodyWorkers().
                    getWriters(contentType);
            LOGGER.severe("The registered message body writers compatible with the MIME media type are:\n" +
                    getMessageBodyWorkers().writersToString(m));

            if (request.getMethod().equals("HEAD")) {
                isCommitted = true;
                responseWriter.writeStatusAndHeaders(-1, this);
                responseWriter.finish();
                return;
            } else {
                throw new WebApplicationException(new MessageException(message), 500);
            }
        }
  • 很显然是在获取MessageBodyWriter时获取为null,进入_getMessageBodyWriter方法,通过debug,能够知道该方法是通过mediaType获取对应的MessageBodyWriter的provider,在此处获取不到,那么MessageBodyWriter是什么呢,通过看源码它是一个接口,jersey所有的写response的操作都直接或间接的实现它,与之相对应的是MessageBodyReader接口;
  • 那MessageBodyWriter和MessageBodyReader是什么时候加载的呢,对于这种通过spring实现的web项目,当时是在应用启动的时候加载与初始化了应用所需的所有资源,启动debug,在jerseyserver启动实现类WebApplicationImpl的初始化方法_initiate()
        // Initiate context resolvers
        crf.init(providerServices, injectableFactory);

        // Initiate the exception mappers
        exceptionFactory.init(providerServices);

        // Initiate message body readers/writers
        bodyFactory.init();

        // Initiate string readers
        stringReaderFactory.init(providerServices);

        // Inject on all components
        Errors.setReportMissingDependentFieldOrMethod(true);
        cpFactory.injectOnAllComponents();
        cpFactory.injectOnProviderInstances(resourceConfig.getProviderSingletons());

前后代码省略,显然bodyFactory.init() 也就是MessageBodyFactory.init()方法初始化MessageBodyReader和MessageBodyWriter,

 public void init() {
        initReaders();
        initWriters();
    }

此处我们只看initWriters()方法,initReaders也是同样的逻辑,MessageBodyFactory的属性
Map<MediaType, List> writerProviders,通过map保存MediaType与之对应的MessageBodyWriter

  • 一步步跟踪源代码,最终进入ProviderServicel.getServiceClasses(Class<?> service, Set sp)方法中,service类型就是MessageBodyWriter,获取所有实现MessageBodyWriter的Class:
 Class<?>[] pca = ServiceFinder.find(service, true).toClassArray();

最关键的是ServiceFinder.toClassArray()方法,

  public Class<T>[] toClassArray() throws ServiceConfigurationError {
        List<Class<T>> result = new ArrayList<Class<T>>();

        Iterator<Class<T>> i = classIterator();
        while (i.hasNext())
            result.add(i.next());
        return result.toArray((Class<T>[])Array.newInstance(Class.class,result.size()));
    }

会创建一个Class的Iterator,通过迭代将能够通过反射创建实例的Class放置在result中,i.next()方法其实调用的是ServiceFinder.LazyClassIterator.next()方法,代码如下:

		@SuppressWarnings("unchecked")
        public Class<T> next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            String cn = nextName;
            nextName = null;
            try {
                return (Class<T>)ReflectionHelper.classForNameWithException(cn, loader);
            } catch (ClassNotFoundException ex) {
                fail(serviceName,
                        SpiMessages.PROVIDER_NOT_FOUND(cn, service));
            } catch (NoClassDefFoundError ex) {
                fail(serviceName,
                        SpiMessages.DEPENDENT_CLASS_OF_PROVIDER_NOT_FOUND(
                                ex.getLocalizedMessage(), cn, service));
            } catch (ClassFormatError ex) {
                fail(serviceName,
                        SpiMessages.DEPENDENT_CLASS_OF_PROVIDER_FORMAT_ERROR(
                                ex.getLocalizedMessage(), cn, service));
            } catch (Exception x) {
                fail(serviceName,
                        SpiMessages.PROVIDER_CLASS_COULD_NOT_BE_LOADED(cn, service, x.getLocalizedMessage()),
                        x);
            }

            return null;    /* This cannot happen */
        }

在这里当nextName为Jackson的处理类JacksonJsonProvider时,即 return (Class)ReflectionHelper.classForNameWithException(cn, loader) 在这一步不会返回JacksonJsonProvider的Class,导致在下一步没法初始化实例

 	for (ProviderClass pc : getServiceClasses(provider)) {
            Object o = getComponent(pc);
            if (o != null) {
                ps.add(provider.cast(o));
            }
       }

最终导致在处理业务请求后,jersey通过jackson写响应操作时,获取不到对应的Writer而失败; 但是在jdk1.7版本及以上正常的,由于最终调用的jdk的native方法就再没有深入研究为啥会出现
这个情况,总之这是jdk本身的一个坑。

public static Class classForNameWithException(String name, ClassLoader cl)
            throws ClassNotFoundException {
        if (cl != null) {
            try {
                return Class.forName(name, false, cl);
            } catch (ClassNotFoundException ex) {
            }
        }
        return Class.forName(name);
    }


  private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

解决方案

为避免jdk版本不同而踩坑,通过采用另一种解决方案

替换jackson依赖
<dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-json</artifactId>
      <version>1.17</version>
</dependency>
web.xml配置
<servlet>
    <servlet-name>jersey-serlvet</servlet-name>
    <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
    <init-param>
      <param-name>jersey.config.server.provider.packages</param-name>
      <param-value>******</param-value>
    </init-param>
   <init-param>
      <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
      <param-value>true</param-value>
    </init-param>

若采用jersey-json,则必须配置com.sun.jersey.api.json.POJOMappingFeature,因为处理jackson的provider为JacksonProviderProxy其中会通过该属性判断json是否与entity映射

 public void setFeaturesAndProperties(FeaturesAndProperties fp) {
        this.jacksonEntityProviderFeatureSet = fp.getFeature("com.sun.jersey.api.json.POJOMappingFeature");
    }

public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
        return this.jacksonEntityProviderFeatureSet && (this.jaxbProvider.isWriteable(type, genericType, annotations, mediaType) || this.pojoProvider.isWriteable(type, genericType, annotations, mediaType));
    }

在判断是否可以进行写操作时jacksonEntityProviderFeatureSet必须为ture,否则还是会报500错误,错误信息跟之前一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值