问题背景
项目中使用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错误,错误信息跟之前一样。