Как я должен регистрировать исключение исключений в моем веб-сервисе 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();
    }
}

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

Что мне делать?

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

Ответ 1

Из-за отсутствия лучшего способа реализовать ведение журнала для исключенных исключений 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();
    }
}

Ответ 2

Джерси (и 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>

Furhtermore, сущность должна быть буферизована. Это можно сделать разными способами: используя буферизацию уровня сервлета (как Эшли Росс указал fooobar.com/questions/94259/...) или используя ContainerRequestFilter.

Ответ 3

Подход №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...
    }
}

Ответ 4

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

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

}

Ответ 5

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

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