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