Как мне регистрировать неперехваченные исключения в моем веб-сервисе RESTful JAX-RS?

У меня есть веб-сервис RESTful, работающий под Glassfish 3.1.2 с использованием Джерси и Джексона:

@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, который возвращается в случае непредвиденного исключения.

Теперь я хотел бы добавить ведение журнала для этих неожиданных исключений, но, несмотря на поиск, не могу узнать, как мне поступить об этом.

Бесплодная попытка

Я пытался с помощью 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();
        }
    }
}

Хотя этот подход может работать, мне кажется, что он неправильно использует то, для чего предполагается использовать 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();
    }
}

Тем не менее, я опасаюсь идти по этому пути, так как мой настоящий проект не так прост, как этот пример, и мне пришлось бы снова и снова реализовывать один и тот же шаблон, не говоря уже о необходимости вручную создавать ответы.

Что я должен делать?

Существуют ли более эффективные методы добавления журналов для необработанных исключений? Есть ли "правильный" способ реализации этого?

5 ответов

Из-за отсутствия лучшего способа реализовать ведение журнала для неперехваченных исключений 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://stackru.com/a/17129256/356408
            InputStream is = req.getInputStream();
            // Read an InputStream elegantly: http://stackru.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://stackru.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();
    }
}

Джерси (и JAX-RS 2.0) предоставляет ContainerResponseFilterContainerResponseFilter в JAX-RS 2.0).

Использование фильтра ответа версии 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 следующий параметр должен быть установлен на сервлет Джерси:

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

Более того, сущность должна быть в буфере. Это можно сделать различными способами: используя буферизацию на уровне сервлета (как указала Эшли Росс /questions/3420413/http-servlet-zapros-poteryaet-parametryi-iz-tela-post-posle-prochteniya-ego-odin-raz/3420428#3420428) или используя ContainerRequestFilter.

Подход № 1 идеален, за исключением одной проблемы: в конечном итоге вы ловите WebApplicationException, Важно, чтобы WebApplicationException пройти через беспрепятственно, потому что он будет вызывать логику по умолчанию (например, NotFoundException) или он может нести определенный Response что ресурс создан для определенного состояния ошибки.

К счастью, если вы используете Джерси, вы можете использовать модифицированный подход № 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...
    }
}

Принятый ответ не работает (и даже не компилируется) в Джерси 2, потому что ContainerResponseFilter был полностью изменен.

Я думаю, что лучший ответ, который я нашел, - это ответ @ Адриана в Джерси... как регистрировать все исключения, но при этом вызывать 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");
    }

}

Они, вероятно, уже зарегистрированы, все, что вам нужно, чтобы найти и включить надлежащий регистратор. Например, под Spring Boot + Jersey все, что вам нужно, это добавить строку в application.properties:

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

Другие вопросы по тегам