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
メソッドは呼び出されません。
一部の人々が使用しているのを見た別のオプションは 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の使用目的の誤用、つまり特定の例外を特定の応答にマッピングするように感じます。
ほとんどのサンプル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();
}
}
ただし、実際のプロジェクトはこの例ほど単純ではないので、このルートに行くのは嫌です。また、応答を手動で作成する必要はなく、この同じパターンを何度も実装する必要があります。
キャッチされていない例外のロギングを追加するより良い方法はありますか?これを実装する「正しい」方法はありますか?
キャッチされていない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();
}
}
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 を使用します。
アプローチ#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...
}
}
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");
}
}
それらはおそらくすでにログに記録されており、適切なロガーを見つけて有効にするために必要なものはすべて揃っています。たとえば、Spring Boot + Jerseyの下では、application.properties
に行を追加するだけです。
logging.level.org.glassfish.jersey.server.ServerRuntime$Responder=TRACE