java - servicios - ¿Cómo debo registrar excepciones no detectadas en mi servicio web RESTful JAX-RS?
servicios web rest (5)
Tengo un servicio web RESTful que funciona bajo Glassfish 3.1.2 usando Jersey y Jackson:
@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;
}
}
Para las excepciones esperadas, lanzo la WebApplicationException
apropiada, y estoy satisfecho con el estado de HTTP 500 que se devuelve si se produce una excepción inesperada.
Ahora me gustaría agregar el registro para estas excepciones inesperadas, pero a pesar de la búsqueda, no puedo averiguar cómo debería hacerlo .
Intento infructuoso
He intentado usar Thread.UncaughtExceptionHandler
y puedo confirmar que se aplica dentro del cuerpo del método, pero nunca se llama a su método uncaughtException
, ya que otra cosa maneja las excepciones no detectadas antes de que lleguen a mi controlador.
Otras ideas: # 1
Otra opción que he visto que algunas personas usan es un ExceptionMapper
, que capta todas las excepciones y luego filtra 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();
}
}
}
Si bien este enfoque puede funcionar, me parece un mal uso de lo que se supone que se debe usar ExceptionMappers, es decir, mapear ciertas excepciones a ciertas respuestas.
Otras ideas: # 2
La mayoría de la muestra del código JAX-RS devuelve el objeto Response
directamente. Siguiendo este enfoque, podría cambiar mi código a algo como:
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();
}
}
Sin embargo, desconfío de seguir esta ruta, ya que mi proyecto real no es tan simple como este ejemplo, y tendría que implementar este mismo patrón una y otra vez, sin mencionar tener que compilar manualmente las respuestas.
¿Que debería hacer?
¿Hay mejores métodos para agregar el registro de excepciones no detectadas? ¿Hay una forma "correcta" de implementar esto?
A falta de una mejor forma de implementar el registro para excepciones JAX-RS no detectadas, usando un ExceptionMapper
catch-all como en Otras Ideas: # 1 parece ser la forma más limpia y simple de agregar esta funcionalidad.
Aquí está mi implementación:
@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://.com/a/17129256/356408
InputStream is = req.getInputStream();
// Read an InputStream elegantly: http://.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://.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();
}
}
El enfoque n. ° 1 es perfecto, excepto por un problema: termina capturando WebApplicationException
. Es importante permitir que la WebApplicationException
pase sin obstáculos porque invocará la lógica predeterminada (por ejemplo, NotFoundException
) o puede llevar una Response
específica que el recurso diseñó para una condición de error en particular.
Afortunadamente, si está utilizando Jersey, puede utilizar un Enfoque modificado n. ° 1 e implementar ExtendedExceptionMapper . Se extiende desde el ExceptionMapper
estándar para agregar la capacidad de ignorar de manera condicional ciertos tipos de excepciones. Por lo tanto, puede filtrar WebApplicationException
manera:
@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...
}
}
Jersey (y JAX-RS 2.0) proporciona ContainerResponseFilter (y ContainerResponseFilter en JAX-RS 2.0 ).
Usar el filtro de respuesta de la versión 1.x de Jersey se vería como
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;
}
}
El filtro debe estar registrado en Jersey. En web.xml, el siguiente parámetro debe establecerse en Jersey servlet:
<init-param>
<param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
<param-value>my.package.ExceptionsLoggingContainerResponseFilter</param-value>
</init-param>
Furhtermore, la entidad debe ser amortiguada. Se puede hacer de varias maneras: usando el buffer de nivel de servlet (como Ashley Ross señaló https://.com/a/17129256/356408 ) o utilizando ContainerRequestFilter .
La respuesta aceptada no funciona (o incluso compila) en Jersey 2 porque ContainerResponseFilter se modificó por completo.
Creo que la mejor respuesta que he encontrado es la respuesta de @ Adrian en Jersey ... cómo registrar todas las excepciones, pero todavía invoco a ExceptionMappers donde utilizó un RequestEventListener y se centró en RequestEvent.Type.ON_EXCEPTION.
Sin embargo, he proporcionado otra alternativa a continuación que es un giro en @stevevls respuesta aquí.
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");
}
}
Probablemente ya estén registrados, todo lo que necesita para encontrar y habilitar el registrador adecuado. Por ejemplo, en Spring Boot + Jersey, todo lo que necesita es agregar una línea a application.properties
:
logging.level.org.glassfish.jersey.server.ServerRuntime$Responder=TRACE