41

我有一个使用 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;
    }
}

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

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

徒劳的尝试

我尝试使用 aThread.UncaughtExceptionHandler并且可以确认它已应用在方法主体内,但它的uncaughtException方法从未被调用,因为其他东西正在处理未捕获的异常,然后它们到达我的处理程序。

其他想法:#1

我见过一些人使用的另一个选项是 an 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();
        }
    }
}

虽然这种方法可能有效,但在我看来,这就像滥用 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();
    }
}

但是,我对走这条路持怀疑态度,因为我的实际项目不像这个例子那么简单,我必须一遍又一遍地实现相同的模式,更不用说必须手动构建响应。

我应该怎么办?

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

4

5 回答 5

26

由于缺乏更好的方法来实现未捕获的 JAX-RS 异常的日志记录,因此使用其他想法ExceptionMapper中的全部内容:#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();
    }
}
于 2013-10-30T10:46:20.013 回答
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;
    }

}

过滤器应注册到泽西岛。在 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>

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

于 2013-10-30T11:23:16.830 回答
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...
    }
}
于 2014-07-30T00:28:49.890 回答
6

接受的答案在 Jersey 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");
    }

}
于 2015-11-05T04:54:15.013 回答
2

它们可能已经被记录了,您只需找到并启用正确的记录器。例如在 Spring Boot + Jersey 下,您只需添加一行application.properties

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

于 2017-01-05T12:00:32.360 回答