我应该如何在RESTful JAX-RS Web服务中记录未捕获的异常?

Ash*_*oss 39 java rest jersey jackson glassfish-3

我使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

对于预期的异常,我抛出适当的WebApplicationException,我对发生意外异常时返回的HTTP 500状态感到满意.

我现在想为这些意外的异常添加日志记录,但是尽管搜索,但是无法找到我应该如何处理这个问题.

没有结果的尝试

我已经尝试使用a Thread.UncaughtExceptionHandler并且可以确认它是在方法体内应用的,但是它的uncaughtException方法永远不会被调用,因为其他东西在它们到达我的处理程序之前处理未捕获的异常.

其他想法:#1

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

@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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然这种方法可行,但我觉得滥用ExceptionMappers应该用于什么,即将某些异常映射到某些响应.

其他想法:#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();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,我对走这条路线很谨慎,因为我的实际项目并不像这个例子那么简单,我必须一遍又一遍地实现这个相同的模式,更不用说必须手动建立响应.

我该怎么办?

是否有更好的方法为未捕获的异常添加日志记录?有没有"正确"的方式来实现这个?

Ash*_*oss 26

由于缺乏更好的方法来实现未捕获的JAX-RS异常的日志记录,使用ExceptionMapper其他思想中的catch-all :#1似乎是添加此功能的最简洁,最简单的方法.

这是我的实现:

@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();
    }
}
Run Code Online (Sandbox Code Playgroud)


Jon*_*nas 12

Jersey(和JAX-RS 2.0)提供ContainerResponseFilter(以及JAX-RS 2.0中的ContainerResponseFilter).

使用Jersey版本1.x响应过滤器看起来像

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;
    }

}
Run Code Online (Sandbox Code Playgroud)

过滤器应该注册到泽西岛.在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>
Run Code Online (Sandbox Code Playgroud)

此外,实体应该被缓冲.它可以通过各种方式完成:使用servlet级缓冲(如Ashley Ross指出/sf/answers/1199047951/)或使用ContainerRequestFilter.


ste*_*vls 9

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

幸运的是,如果您使用的是Jersey,则可以使用修改后的方法#1并实现ExtendedExceptionMapper.它从标准扩展ExceptionMapper到添加有条件地忽略某些类型的异常的能力.你可以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...
    }
}
Run Code Online (Sandbox Code Playgroud)


suc*_*awk 5

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

我认为我找到的最佳答案是@Adrian在泽西岛的答案 ......如何记录所有异常,但仍调用ExceptionMappers,他使用RequestEventListener并专注于RequestEvent.Type.ON_EXCEPTION.

但是,我在下面提供了另一个替代方案,这是对@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 (i.e. 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");
    }

}
Run Code Online (Sandbox Code Playgroud)