Как мне регистрировать неперехваченные исключения в моем веб-сервисе 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) предоставляет ContainerResponseFilter (и ContainerResponseFilter в 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