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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19621653/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-12 18:52:39  来源:igfitidea点击:

How should I log uncaught exceptions in my RESTful JAX-RS web service?

javarestjerseyHymansonglassfish-3

提问by Ashley Ross

I have a RESTful web service running under Glassfish 3.1.2 using Jersey and Hymanson:

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

For expected exceptions, I throw the appropriate WebApplicationException, and I'm happy with the HTTP 500 status that is returned if an unexpected exception occurs.

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

I would now like to add logging for these unexpected exceptions, but despite searching, cannot find out how I shouldbe going about this.

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

Fruitless Attempt

徒劳的尝试

I have tried using a Thread.UncaughtExceptionHandlerand can confirm that it is applied inside the method body, but its uncaughtExceptionmethod is never called, as something else is handling the uncaught exceptions before they reach my handler.

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

Other Ideas: #1

其他想法:#1

Another option I've seen some people use is an ExceptionMapper, which catches all exceptions and then filters out WebApplicationExceptions:

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

While this approach may work, it feels to me like misuse of what ExceptionMappers are supposed to be used for, that is, mapping certain exceptions to certain responses.

虽然这种方法可能有效,但在我看来,这就像误用了 ExceptionMappers 的用途,即将某些异常映射到某些响应。

Other Ideas: #2

其他想法:#2

Most sample JAX-RS code returns the Responseobject directly. Following this approach, I could change my code to something like:

大多数示例 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();
    }
}

However, I'm leery of going this route, as my actual project is not as simple as this example, and I would have to implement this same pattern over and over again, not to mention having to manually build up the Responses.

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

What should I do?

我该怎么办?

Are there better methods for adding logging for uncaught exceptions? Is there a "right" way of implementing this?

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

回答by Ashley Ross

For lack of a better way to implement logging for uncaught JAX-RS exceptions, using a catch-all ExceptionMapperas in Other Ideas: #1seems to be the cleanest, simplest way to add this functionality.

由于缺乏一种更好的方法来实现未捕获的 JAX-RS 异常的日志记录,使用ExceptionMapperOther Ideas: #1中的包罗万象的方法似乎是添加此功能的最简洁、最简单的方法。

Here's my implementation:

这是我的实现:

@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();
    }
}

回答by Jonas

Jersey (and JAX-RS 2.0) provides ContainerResponseFilter(and ContainerResponseFilter in JAX-RS 2.0).

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

Using Jersey version 1.x response filter would look like

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

}

Filter should be registered to Jersey. In web.xml the following parameter should be set to Jersey servlet:

过滤器应注册到泽西岛。在 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, entity should be buffered. It can be done in various ways: using servlet level buffering (as Ashley Ross pointed out https://stackoverflow.com/a/17129256/356408) or using ContainerRequestFilter.

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

回答by stevevls

Approach #1 is perfect except for one problem: you end up catching WebApplicationException. It's important to let the WebApplicationExceptionpass through unhindered because it will either invoke default logic (e.g. NotFoundException) or it can carry a specific Responsethat the resource crafted for a particular error condition.

方法#1 是完美的,除了一个问题:你最终会捕获WebApplicationException. 让WebApplicationException传递不受阻碍很重要,因为它会调用默认逻辑(例如NotFoundException),或者它可以携带Response资源为特定错误条件制作的特定信息。

Luckily, if you're using Jersey, you can use a modified Approach #1 and implement the ExtendedExceptionMapper. It's extends from the standard ExceptionMapperto add the ability to conditionally ignore certain types of exceptions. You can thereby filter out WebApplicationExceptionlike so:

幸运的是,如果您使用 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...
    }
}

回答by successhawk

The accepted answer does not work (or even compile) in Jersey 2 because ContainerResponseFilter was totally changed.

接受的答案在 Jersey 2 中不起作用(甚至编译),因为 ContainerResponseFilter 已完全更改。

I think the best answer I've found is @Adrian's answer in Jersey... how to log all exceptions, but still invoke ExceptionMapperswhere he used a RequestEventListener and focused on the RequestEvent.Type.ON_EXCEPTION.

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

However, I have provided another alternative below that is a spin on @stevevls answer here.

但是,我在下面提供了另一种选择,即对 @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");
    }

}

回答by iirekm

They are probably already logged, all you need to find and enable proper logger. For example under Spring Boot + Jersey, all you need is to add a line to application.properties:

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

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

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