我应该如何在我的RESTful JAX-RS Web服务中logging未捕获的exception?

我有一个使用Jersey和Jackson在Glassfish 3.1.2下运行的RESTful Web服务:

@Stateless @LocalBean @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Path("users") public class UserRestService { private static final Logger log = ...; @GET @Path("{userId:[0-9]+}") public User getUser(@PathParam("userId") Long userId) { User user; user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId); return user; } } 

对于预期的exception,我抛出相应的WebApplicationException ,并且如果发生意外的exception,则返回HTTP 500状态。

我现在想为这些意想不到的exception添加日志logging,但是尽pipesearch,但无法find我应该如何去做这件事。

无果而终的尝试

我已经尝试过使用Thread.UncaughtExceptionHandler并且可以确认它应用在方法体内部,但是它的uncaughtException方法从来没有被调用,因为别的东西在它们到达我的处理程序之前处理了未捕获的exception。

其他想法:#1

我见过一些人使用的另一个选项是一个ExceptionMapper ,它捕获所有exception,然后过滤掉WebApplicationException:

 @Provider public class ExampleExceptionMapper implements ExceptionMapper<Throwable> { private static final Logger log = ...; public Response toResponse(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException)t).getResponse(); } else { log.error("Uncaught exception thrown by REST service", t); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) // Add an entity, etc. .build(); } } } 

虽然这种方法可能有效,但我觉得像滥用ExceptionMapper应该被用来做什么,也就是说,将某些例外映射到特定的响应。

其他想法:#2

大多数示例JAX-RS代码直接返回Response对象。 遵循这种方法,我可以将我的代码更改为如下所示:

 public Response getUser(@PathParam("userId") Long userId) { try { User user; user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId); return Response.ok().entity(user).build(); } catch (Throwable t) { return processException(t); } } private Response processException(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException)t).getResponse(); } else { log.error("Uncaught exception thrown by REST service", t); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) // Add an entity, etc. .build(); } } 

然而,我不敢走这条路,因为我的实际项目并不像这个例子那么简单,我不得不一次又一次地实现这个模式,更不用说手动build立响应了。

我该怎么办?

有没有更好的方法为未捕获的exception添加日志logging? 有没有一个“正确”的方式来执行这个?

对于没有更好的方法来实现对未捕获的JAX-RSexception的日志logging,像在其他想法中一样使用全能的ExceptionMapper :#1似乎是添加此function的最简单,最简单的方法。

这是我的实现:

 @Provider public class ThrowableExceptionMapper implements ExceptionMapper<Throwable> { private static final Logger log = Logger.getLogger(ThrowableExceptionMapper.class); @Context HttpServletRequest request; @Override public Response toResponse(Throwable t) { if (t instanceof WebApplicationException) { return ((WebApplicationException) t).getResponse(); } else { String errorMessage = buildErrorMessage(request); log.error(errorMessage, t); return Response.serverError().entity("").build(); } } private String buildErrorMessage(HttpServletRequest req) { StringBuilder message = new StringBuilder(); String entity = "(empty)"; try { // How to cache getInputStream: http://stackoverflow.com/a/17129256/356408 InputStream is = req.getInputStream(); // Read an InputStream elegantly: http://stackoverflow.com/a/5445161/356408 Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); entity = s.hasNext() ? s.next() : entity; } catch (Exception ex) { // Ignore exceptions around getting the entity } message.append("Uncaught REST API exception:\n"); message.append("URL: ").append(getOriginalURL(req)).append("\n"); message.append("Method: ").append(req.getMethod()).append("\n"); message.append("Entity: ").append(entity).append("\n"); return message.toString(); } private String getOriginalURL(HttpServletRequest req) { // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 String scheme = req.getScheme(); // http String serverName = req.getServerName(); // hostname.com int serverPort = req.getServerPort(); // 80 String contextPath = req.getContextPath(); // /mywebapp String servletPath = req.getServletPath(); // /servlet/MyServlet String pathInfo = req.getPathInfo(); // /a/b;c=123 String queryString = req.getQueryString(); // d=789 // Reconstruct original requesting URL StringBuilder url = new StringBuilder(); url.append(scheme).append("://").append(serverName); if (serverPort != 80 && serverPort != 443) { url.append(":").append(serverPort); } url.append(contextPath).append(servletPath); if (pathInfo != null) { url.append(pathInfo); } if (queryString != null) { url.append("?").append(queryString); } return url.toString(); } } 

Jersey(和JAX-RS 2.0) 在JAX-RS 2.0中提供了ContainerResponseFilter (和ContainerResponseFilter )。

使用Jersey版本1.x响应filter将如下所示

 public class ExceptionsLoggingContainerResponseFilter implements ContainerResponseFilter { private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionsLoggingContainerResponseFilter.class); @Override public ContainerResponse filter(ContainerRequest request, ContainerResponse response) { Throwable throwable = response.getMappedThrowable(); if (throwable != null) { LOGGER.info(buildErrorMessage(request), throwable); } return response; } private String buildErrorMessage(ContainerRequest request) { StringBuilder message = new StringBuilder(); message.append("Uncaught REST API exception:\n"); message.append("URL: ").append(request.getRequestUri()).append("\n"); message.append("Method: ").append(request.getMethod()).append("\n"); message.append("Entity: ").append(extractDisplayableEntity(request)).append("\n"); return message.toString(); } private String extractDisplayableEntity(ContainerRequest request) { String entity = request.getEntity(String.class); return entity.equals("") ? "(blank)" : entity; } } 

filter应该注册到泽西岛。 在web.xml中,应将以下参数设置为Jersey servlet:

 <init-param> <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name> <param-value>my.package.ExceptionsLoggingContainerResponseFilter</param-value> </init-param> 

Furhtermore,实体应该被缓冲。 它可以通过多种方式完成:使用servlet级缓冲(如Ashley Ross指出https://stackoverflow.com/a/17129256/356408 )或使用ContainerRequestFilter 。

方法#1是完美的,除了一个问题:最终捕获WebApplicationException 。 让WebApplicationException不受阻碍地通过是非常重要的,因为它将调用默认的逻辑(例如NotFoundException ),或者它可以携带特定的Response ,以便为特定的错误条件制定资源。

幸运的是,如果您使用的是Jersey,则可以使用修改后的方法#1并实现ExtendedExceptionMapper 。 它从标准的ExceptionMapper扩展到添加有条件地忽略某些types的exception的能力。 因此可以像这样过滤掉WebApplicationException

 @Provider public class UncaughtThrowableExceptionMapper implements ExtendedExceptionMapper<Throwable> { @Override public boolean isMappable(Throwable throwable) { // ignore these guys and let jersey handle them return !(throwable instanceof WebApplicationException); } @Override public Response toResponse(Throwable throwable) { // your uncaught exception handling logic here... } } 

接受的答案在泽西岛2中不起作用(甚至编译),因为ContainerResponseFilter完全改变了。

我认为我find的最佳答案是@Adrian在Jersey中的回答…如何logging所有exception,但是仍然在其中使用RequestEventListener的情况下调用ExceptionMappers ,并着重于RequestEvent.Type.ON_EXCEPTION。

不过,我在下面提供了另一个替代scheme,这里是@stevevls的答案。

 import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status.Family; import javax.ws.rs.ext.Provider; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.glassfish.jersey.spi.ExtendedExceptionMapper; /** * The purpose of this exception mapper is to log any exception that occurs. * Contrary to the purpose of the interface it implements, it does not change or determine * the response that is returned to the client. * It does this by logging all exceptions passed to the isMappable and then always returning false. * */ @Provider public class LogAllExceptions implements ExtendedExceptionMapper<Throwable> { private static final Logger logger = Logger.getLogger(LogAllExceptions.class); @Override public boolean isMappable(Throwable thro) { /* Primarily, we don't want to log client errors (ie 400's) as an error. */ Level level = isServerError(thro) ? Level.ERROR : Level.INFO; /* TODO add information about the request (using @Context). */ logger.log(level, "ThrowableLogger_ExceptionMapper logging error.", thro); return false; } private boolean isServerError(Throwable thro) { /* Note: We consider anything that is not an instance of WebApplicationException a server error. */ return thro instanceof WebApplicationException && isServerError((WebApplicationException)thro); } private boolean isServerError(WebApplicationException exc) { return exc.getResponse().getStatusInfo().getFamily().equals(Family.SERVER_ERROR); } @Override public Response toResponse(Throwable throwable) { //assert false; logger.fatal("ThrowableLogger_ExceptionMapper.toResponse: This should not have been called."); throw new RuntimeException("This should not have been called"); } } 

他们可能已经被logging了,你只需要find并启用适当的logging器即可。 例如在Spring Boot + Jersey下,所有你需要的就是添加一行到application.properties

logging.level.org.glassfish.jersey.server.ServerRuntime$Responder=TRACE