私はJAX-RSで新しいクライアントAPIライブラリを使用し始めており、これまでのところとても気に入っています。しかし、理解できないことが1つ見つかりました。私が使用しているAPIには、たとえば次のようなカスタムエラーメッセージ形式があります。
{
"code": 400,
"message": "This is a message which describes why there was a code 400."
}
ステータスコードとして400を返しますが、何が間違っていたかを説明するエラーメッセージも含まれています。
ただし、JAX-RS 2.0クライアントは400ステータスを一般的なものに再マッピングしており、適切なエラーメッセージを失っています。それは正しくそれをBadRequestExceptionにマッピングしますが、一般的な「HTTP 400 Bad Request」メッセージを伴います。
javax.ws.rs.BadRequestException: HTTP 400 Bad Request
at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.Java:908)
at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.Java:770)
at org.glassfish.jersey.client.JerseyInvocation.access$500(JerseyInvocation.Java:90)
at org.glassfish.jersey.client.JerseyInvocation$2.call(JerseyInvocation.Java:671)
at org.glassfish.jersey.internal.Errors.process(Errors.Java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.Java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.Java:228)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.Java:424)
at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.Java:667)
at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.Java:396)
at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.Java:296)
実際のエラーメッセージにアクセスするために挿入できるインターセプターまたはカスタムエラーハンドラーのようなものはありますか?私はドキュメントを調べてきましたが、それを行う方法がわかりません。
私は今ジャージーを使用していますが、CXFを使用してこれを試したところ、同じ結果が得られました。コードは次のようになります。
Client client = ClientBuilder.newClient().register(JacksonFeature.class).register(GzipInterceptor.class);
WebTarget target = client.target("https://somesite.com").path("/api/test");
Invocation.Builder builder = target.request()
.header("some_header", value)
.accept(MediaType.APPLICATION_JSON_TYPE)
.acceptEncoding("gzip");
MyEntity entity = builder.get(MyEntity.class);
更新:
以下のコメントに記載されているソリューションを実装しました。クラスはJAX-RS 2.0クライアントAPIで少し変更されているため、少し異なります。デフォルトの動作が一般的なエラーメッセージを表示し、実際のエラーメッセージを破棄するのはまだ間違っていると私はまだ思います。エラーオブジェクトが解析されない理由を理解していますが、解析されていないバージョンが返されているはずです。結局、ライブラリが既に行っているレプリケート例外マッピングを作成することになります。
助けてくれてありがとう。
これが私のフィルタークラスです:
@Provider
public class ErrorResponseFilter implements ClientResponseFilter {
private static ObjectMapper _MAPPER = new ObjectMapper();
@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
// for non-200 response, deal with the custom error messages
if (responseContext.getStatus() != Response.Status.OK.getStatusCode()) {
if (responseContext.hasEntity()) {
// get the "real" error message
ErrorResponse error = _MAPPER.readValue(responseContext.getEntityStream(), ErrorResponse.class);
String message = error.getMessage();
Response.Status status = Response.Status.fromStatusCode(responseContext.getStatus());
WebApplicationException webAppException;
switch (status) {
case BAD_REQUEST:
webAppException = new BadRequestException(message);
break;
case UNAUTHORIZED:
webAppException = new NotAuthorizedException(message);
break;
case FORBIDDEN:
webAppException = new ForbiddenException(message);
break;
case NOT_FOUND:
webAppException = new NotFoundException(message);
break;
case METHOD_NOT_ALLOWED:
webAppException = new NotAllowedException(message);
break;
case NOT_ACCEPTABLE:
webAppException = new NotAcceptableException(message);
break;
case UNSUPPORTED_MEDIA_TYPE:
webAppException = new NotSupportedException(message);
break;
case INTERNAL_SERVER_ERROR:
webAppException = new InternalServerErrorException(message);
break;
case SERVICE_UNAVAILABLE:
webAppException = new ServiceUnavailableException(message);
break;
default:
webAppException = new WebApplicationException(message);
}
throw webAppException;
}
}
}
}
私はあなたがこのようなことをしたいと信じています:
Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
System.out.println( response.getStatusType() );
return null;
}
return response.readEntity( MyEntity.class );
あなたが試すことができる別のこと(私はこのAPIが何かを置く場所、すなわちヘッダーまたはエンティティまたは何にあるかわからないため)は次のとおりです:
Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) {
// if they put the custom error stuff in the entity
System.out.println( response.readEntity( String.class ) );
return null;
}
return response.readEntity( MyEntity.class );
一般にREST応答コードをJava例外にマッピングする場合は、クライアントフィルターを追加してそれを行うことができます。
class ClientResponseLoggingFilter implements ClientResponseFilter {
@Override
public void filter(final ClientRequestContext reqCtx,
final ClientResponseContext resCtx) throws IOException {
if ( resCtx.getStatus() == Response.Status.BAD_REQUEST.getStatusCode() ) {
throw new MyClientException( resCtx.getStatusInfo() );
}
...
上記のフィルターでは、コードごとに特定の例外を作成するか、応答コードとエンティティをラップする1つの一般的な例外タイプを作成できます。
カスタムフィルターを作成する以外に、ジャージークライアントにカスタムエラーメッセージを取得する方法は他にもあります。 (フィルターは優れたソリューションですが)
1)HTTPヘッダーフィールドにエラーメッセージを渡します。詳細なエラーメッセージは、JSON応答および「x-error-message」などの追加のヘッダーフィールドにあります。
Serverは、HTTPエラーヘッダーを追加します。
ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg)){
rb.header("x-error-message", errMsg);
}
return rb.build();
Clientは例外(私の場合はNotFoundException)をキャッチし、応答ヘッダーを読み取ります。
try {
Integer accountId = 2222;
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target("http://localhost:8080/rest-jersey/rest");
webTarget = webTarget.path("/accounts/"+ accountId);
Invocation.Builder ib = webTarget.request(MediaType.APPLICATION_JSON);
Account resp = ib.get(new GenericType<Account>() {
});
} catch (NotFoundException e) {
String errorMsg = e.getResponse().getHeaderString("x-error-message");
// do whatever ...
return;
}
2)別の解決策は、例外をキャッチして応答の内容を読み取ることです。
try {
// same as above ...
} catch (NotFoundException e) {
String respString = e.getResponse().readEntity(String.class);
// you can convert to JSON or search for error message in String ...
return;
}
クラス WebApplicationException はそのために設計されましたが、何らかの理由で、メッセージのパラメーターとして指定したものを無視して上書きします。
そのため、パラメーターを尊重する独自の拡張機能WebAppException
を作成しました。これは単一のクラスであり、応答フィルターやマッパーを必要としません。
処理中にどこからでもスローされる可能性があるため、Response
を作成するよりも例外を優先します。
簡単な使い方:
throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");
クラス:
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.Response.StatusType;
public class WebAppException extends WebApplicationException {
private static final long serialVersionUID = -9079411854450419091L;
public static class MyStatus implements StatusType {
final int statusCode;
final String reasonPhrase;
public MyStatus(int statusCode, String reasonPhrase) {
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
}
@Override
public int getStatusCode() {
return statusCode;
}
@Override
public Family getFamily() {
return Family.familyOf(statusCode);
}
@Override
public String getReasonPhrase() {
return reasonPhrase;
}
}
public WebAppException() {
}
public WebAppException(int status) {
super(status);
}
public WebAppException(Response response) {
super(response);
}
public WebAppException(Status status) {
super(status);
}
public WebAppException(String message, Response response) {
super(message, response);
}
public WebAppException(int status, String message) {
super(message, Response.status(new MyStatus(status, message)). build());
}
public WebAppException(Status status, String message) {
this(status.getStatusCode(), message);
}
public WebAppException(String message) {
this(500, message);
}
}
次の作品
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
これにつまずく人のためのはるかに簡潔な解決策:
.get(Class<T> responseType)
または結果タイプを引数として取るその他のメソッドの呼び出しInvocation.Builder
は、Response
ではなく、目的のタイプの値を返します。副作用として、これらのメソッドは受信したステータスコードが2xxの範囲にあるかどうかをチェックし、そうでない場合は適切なWebApplicationException
をスローします。
ドキュメント から:
スロー:サーバーから返された応答の応答ステータスコードが成功せず、指定された応答タイプがResponseでない場合は、WebApplicationException。
これにより、WebApplicationException
をキャッチし、実際のResponse
を取得し、含まれているエンティティを例外の詳細として処理し(ApiExceptionInfo
)、適切な例外をスローできます(ApiException
) 。
public <Result> Result get(String path, Class<Result> resultType) {
return perform("GET", path, null, resultType);
}
public <Result> Result post(String path, Object content, Class<Result> resultType) {
return perform("POST", path, content, resultType);
}
private <Result> Result perform(String method, String path, Object content, Class<Result> resultType) {
try {
Entity<Object> entity = null == content ? null : Entity.entity(content, MediaType.APPLICATION_JSON);
return client.target(uri).path(path).request(MediaType.APPLICATION_JSON).method(method, entity, resultType);
} catch (WebApplicationException webApplicationException) {
Response response = webApplicationException.getResponse();
if (response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
throw new ApiException(response.readEntity(ApiExceptionInfo.class), webApplicationException);
} else {
throw webApplicationException;
}
}
}
ApiExceptionInfo
は私のアプリケーションのカスタムデータタイプです。
import lombok.Data;
@Data
public class ApiExceptionInfo {
private int code;
private String message;
}
ApiException
は私のアプリケーションのカスタム例外タイプです:
import lombok.Getter;
public class ApiException extends RuntimeException {
@Getter
private final ApiExceptionInfo info;
public ApiException(ApiExceptionInfo info, Exception cause) {
super(info.toString(), cause);
this.info = info;
}
}
[少なくともResteasyでは] @Chuck Mによって提供され、ClientResponseFilter
に基づくソリューションには大きな欠点が1つあります。
ClientResponseFilterに基づいて使用すると、BadRequestException
、NotAuthorizedException
、...例外はjavax.ws.rs.ProcessingException
によってラップされます。
プロキシのクライアントがこのjavax.ws.rs.ResponseProcessingException
例外を強制的にキャッチされてはなりません。
フィルターがないと、元の残りの例外が発生します。デフォルトでキャッチして処理しても、あまり効果はありません。
catch (WebApplicationException e) {
//does not return response body:
e.toString();
// returns null:
e.getCause();
}
エラーから説明を抽出すると、問題は別のレベルで解決できます。WebApplicationException
例外、すべての親残りの例外。javax.ws.rs.core.Responseが含まれます。ヘルパーメソッドを記述するだけで、例外がWebApplicationException
タイプの場合は、応答本文もチェックされます。以下はScalaのコードですが、アイデアは明確でなければなりません。メソッドは、残りの例外の明確な説明を返します。
private def descriptiveWebException2String(t: WebApplicationException): String = {
if (t.getResponse.hasEntity)
s"${t.toString}. Response: ${t.getResponse.readEntity(classOf[String])}"
else t.toString
}
次に、クライアントに正確なエラーを表示する責任を移します。共有例外ハンドラを使用して、クライアントの労力を最小限に抑えます。