web-dev-qa-db-fra.com

Comment dois-je enregistrer les exceptions non capturées dans mon service Web RESTful JAX-RS?

J'ai un service Web RESTful fonctionnant sous Glassfish 3.1.2 en utilisant Jersey et 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;
    }
}

Pour les exceptions attendues, je jette le WebApplicationException approprié, et je suis satisfait du statut HTTP 500 qui est renvoyé si une exception inattendue se produit.

Je voudrais maintenant ajouter la journalisation de ces exceptions inattendues, mais malgré la recherche, je ne peux pas savoir comment je dois procéder.

Tentative infructueuse

J'ai essayé d'utiliser un Thread.UncaughtExceptionHandler et peut confirmer qu'elle est appliquée à l'intérieur du corps de la méthode, mais sa méthode uncaughtException n'est jamais appelée, car quelque chose d'autre gère les exceptions non capturées avant qu'elles n'atteignent mon gestionnaire.

Autres idées: # 1

Une autre option que j'ai vu certaines personnes utiliser est un ExceptionMapper , qui intercepte toutes les exceptions puis filtre les 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();
        }
    }
}

Bien que cette approche puisse fonctionner, cela me semble être une mauvaise utilisation de ce que les ExceptionMappers sont censés être utilisés, c'est-à-dire mapper certaines exceptions à certaines réponses.

Autres idées: # 2

La plupart des exemples de code JAX-RS renvoient directement l'objet Response . En suivant cette approche, je pourrais changer mon code en quelque chose comme:

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();
    }
}

Cependant, je me méfie de suivre cette voie, car mon projet réel n'est pas aussi simple que cet exemple, et je devrais implémenter ce même modèle encore et encore, sans parler de la nécessité de créer manuellement les réponses.

Que devrais-je faire?

Existe-t-il de meilleures méthodes pour ajouter la journalisation des exceptions non interceptées? Existe-t-il une "bonne" façon de mettre cela en œuvre?

39
Ashley Ross

Faute d'une meilleure façon d'implémenter la journalisation pour les exceptions JAX-RS non capturées, en utilisant un fourre-tout ExceptionMapper comme dans Autres idées: # 1 semble être le moyen le plus propre et le plus simple d'ajouter cette fonctionnalité.

Voici ma mise en œuvre:

@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();
    }
}
26
Ashley Ross

Jersey (et JAX-RS 2.0) fournit ContainerResponseFilter (et ContainerResponseFilter dans JAX-RS 2. ).

L'utilisation du filtre de réponse Jersey version 1.x ressemblerait à

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;
    }

}

Le filtre doit être enregistré sur Jersey. Dans web.xml, le paramètre suivant doit être défini sur Servlet Jersey:

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

De plus, l'entité doit être mise en mémoire tampon. Cela peut être fait de différentes manières: en utilisant la mise en mémoire tampon au niveau du servlet (comme l'a souligné Ashley Ross https://stackoverflow.com/a/17129256/356408 ) ou en utilisant ContainerRequestFilter .

12
Jonas

L'approche n ° 1 est parfaite, sauf pour un problème: vous finissez par attraper WebApplicationException. Il est important de laisser le WebApplicationException passer sans entrave car il invoquera la logique par défaut (par exemple NotFoundException) ou il peut transporter un Response spécifique que la ressource a conçu pour une erreur particulière état.

Heureusement, si vous utilisez Jersey, vous pouvez utiliser une approche n ° 1 modifiée et implémenter ExtendedExceptionMapper . Il s'étend du standard ExceptionMapper pour ajouter la possibilité d'ignorer conditionnellement certains types d'exceptions. Vous pouvez ainsi filtrer WebApplicationException comme ceci:

@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...
    }
}
9
stevevls

La réponse acceptée ne fonctionne pas (ni même compile) dans Jersey 2 car ContainerResponseFilter a été totalement modifié.

Je pense que la meilleure réponse que j'ai trouvée est la réponse de @ Adrian dans Jersey ... comment enregistrer toutes les exceptions, mais toujours invoquer ExceptionMappers où il a utilisé un RequestEventListener et axé sur le RequestEvent.Type.ON_EXCEPTION.

Cependant, j'ai fourni une autre alternative ci-dessous qui est une rotation de la réponse @stevevls ici.

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
successhawk

Ils sont probablement déjà enregistrés, tout ce dont vous avez besoin pour trouver et activer un enregistreur approprié. Par exemple, sous Spring Boot + Jersey, il vous suffit d'ajouter une ligne à application.properties:

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

2
iirekm