web-dev-qa-db-ja.com

RESTful JAX-RS Webサービスでキャッチされなかった例外をログに記録するにはどうすればよいですか?

JerseyとJacksonを使用して、Glassfish 3.1.2で実行されているRESTful Webサービスがあります。

@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ステータスに満足しています。

これらの予期しない例外のロギングを追加したいと思いますが、検索にもかかわらず、私shouldがどのようにこれを行っているのかわかりません。

実りのない試み

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

ただし、実際のプロジェクトはこの例ほど単純ではないので、このルートに行くのは嫌です。また、応答を手動で作成する必要はなく、この同じパターンを何度も実装する必要があります。

私は何をすべきか?

キャッチされていない例外のロギングを追加するより良い方法はありますか?これを実装する「正しい」方法はありますか?

39
Ashley Ross

キャッチされていないJAX-RS例外のロギングを実装するより良い方法がないため、catch-all ExceptionMapperのように使用しますは、この機能を追加する最もクリーンでシンプルな方法のようです。

これが私の実装です。

@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(およびJAX-RS 2.0)は、 ContainerResponseFilter (および JAX-RS 2.0のContainerResponseFilter )を提供します。

Jerseyバージョン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;
    }

}

フィルターはJerseyに登録する必要があります。 web.xmlでは、次のパラメーターをJerseyサーブレットに設定する必要があります。

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

さらに、エンティティはバッファリングする必要があります。それはさまざまな方法で行うことができます:サーブレットレベルのバッファリングの使用(Ashley Rossが https://stackoverflow.com/a/17129256/356408 を指摘したように)または ContainerRequestFilter を使用します。

12
Jonas

アプローチ#1は、1つの問題を除いて完璧です:WebApplicationExceptionをキャッチすることになります。 WebApplicationExceptionをデフォルトのロジック(たとえば、NotFoundException)を呼び出すか、特定のエラー条件に合わせてリソースが作成した特定のResponseを実行できるため、妨げられずに通過させることが重要です。

幸いなことに、Jerseyを使用している場合は、修正されたアプローチ#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...
    }
}
9
stevevls

ContainerResponseFilterが完全に変更されたため、受け入れられた答えはJersey 2では機能しません(またはコンパイルされません)。

私が見つけた最良の答えは、 Jerseyの@Adrianの答えだと思います...すべての例外を記録する方法ですが、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
successhawk

それらはおそらくすでにログに記録されており、適切なロガーを見つけて有効にするために必要なものはすべて揃っています。たとえば、Spring Boot + Jerseyの下では、application.propertiesに行を追加するだけです。

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

2
iirekm