【问题标题】:Possible to do custom method processing with ResteasyClient (Proxy Framework)?可以使用 ResteasyClient(代理框架)进行自定义方法处理吗?
【发布时间】:2023-04-08 10:47:02
【问题描述】:

是否可以使用ResteasyClient (Proxy Framework) 注册 DynamicFeature,类似于在服务器端可以完成的操作?

类似这样的:

final ResteasyClient client = new ResteasyClientBuilder().build();
client.register(new MyDynamicFeature());

MyDynamicFeature 实现 DynamicFeature 的地方

我试图弄清楚如何让 ClientResponseFilter 根据资源方法上存在的注释检查 http 返回状态,而 DynamicFeature 似乎是访问 ResourceInfo 的最有希望的线索。

所以本质上,我想做这样的事情:

@POST
@Path("some/path/user")
@ExpectedHttpStatus(201) // <- this would have to be passed on somehow as expectedStatus
User createUser(User request);

然后在 ClientResponseFilter(或任何其他解决方案)中像这样:

@Override
public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
    if (responseContext.getStatus() != expectedStatus) {
        // explode
    }
}

因为在 ClientResponseFilter 中,我看不到任何方法可以知道定义过滤器当前正在分析的 REST 调用的资源方法是什么。

问题是框架现在只检查响应状态是否成功,它不检查它是 200 还是 201,我们想改进它。

这里有一些文章似乎解释了一些非常相似的东西,但这似乎不适用于 ClientResponseFilter / ResteasyClient:

【问题讨论】:

    标签: jax-rs resteasy


    【解决方案1】:

    首先,我不能把这个解决方案归功于我,但我要把答案贴在这里。

    另外,您可能会问我们为什么要这样做?因为我们需要/想要测试服务返回正确的 http 状态,但不幸的是,我们正在测试的服务并不总是为相同的 http 方法返回相同的 http 状态。

    例如在下面的示例中,post 返回 HttpStatus.OK,同一服务的另一个 post 方法可以返回 HttpStatus.CREATED。

    这是我们最终得到的解决方案,ClientResponseFilter 的组合:

    import java.io.IOException;
    import java.util.UUID;
    
    import javax.ws.rs.client.ClientRequestContext;
    import javax.ws.rs.client.ClientResponseContext;
    import javax.ws.rs.client.ClientResponseFilter;
    
    /**
     * {@link ClientResponseFilter} which will handle setting the HTTP StatusCode property for use with
     * {@link HttpStatusResponseInterceptor}
     */
    public class HttpStatusResponseFilter implements ClientResponseFilter {
    
        public static final String STATUS_CODE = "StatusCode-" + UUID.randomUUID();
    
        @Override
        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
            requestContext.setProperty(STATUS_CODE, responseContext.getStatusInfo());
        }
    }
    

    和 ReaderInterceptor:

    import java.io.IOException;
    import java.lang.annotation.Annotation;
    
    import javax.ws.rs.ServerErrorException;
    import javax.ws.rs.core.Response.Status;
    import javax.ws.rs.ext.ReaderInterceptor;
    import javax.ws.rs.ext.ReaderInterceptorContext;
    
    /**
     * {@link ReaderInterceptor} which will verify the success HTTP status code returned from the server against the
     * expected successful HTTP status code {@link SuccessStatus}
     *
     * @see HttpStatusResponseFilter
     */
    public class HttpStatusResponseInterceptor implements ReaderInterceptor {
    
        @Override
        public Object aroundReadFrom(ReaderInterceptorContext interceptorContext) throws ServerErrorException, IOException {
            Status actualStatus = (Status) interceptorContext.getProperty(HttpStatusResponseFilter.STATUS_CODE);
            if (actualStatus == null) {
                throw new IllegalStateException("Property " + HttpStatusResponseFilter.STATUS_CODE + " does not exist!");
            }
    
            Status expectedStatus = null;
            for (Annotation annotation : interceptorContext.getAnnotations()) {
                if (annotation.annotationType() == SuccessStatus.class) {
                    expectedStatus = ((SuccessStatus) annotation).value();
                    break;
                }
            }
    
            if (expectedStatus != null && expectedStatus != actualStatus) {
                throw new ServerErrorException(String.format("Invalid status code returned. Expected %d, but got %d.",
                        expectedStatus.getStatusCode(), actualStatus.getStatusCode()), actualStatus);
            }
    
            return interceptorContext.proceed();
        }
    }
    

    我们在创建客户端时注册这两个:

        final ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
        client.register(new HttpStatusResponseFilter());
        client.register(new HttpStatusResponseInterceptor());
    

    SuccessStatus 是一个注解,我们使用它来注解我们想要专门检查的方法,例如像这样:

    @POST
    @Path("some/foobar")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @SuccessStatus(Status.OK)
    Foobar createFoobar(Foobar foobar);
    

    【讨论】:

      【解决方案2】:

      无法在您的客户端中注册DynamicFeature

      DynamicFeature documentation

      用于动态注册后匹配的 JAX-RS 元提供程序 部署时 JAX-RS 应用程序设置期间的提供程序。 JAX-RS 运行时使用动态特性来注册提供者 应应用于特定的资源类和方法,并且 覆盖任何基于注释的绑定定义 注册资源过滤器或拦截器实例。

      实现这个接口的提供者可以用@Provider注解 注释以便在扫描时被 JAX-RS 运行时发现 对于资源和提供者。 仅支持此提供程序类型 服务器 API 的一部分

      JAX-RS Client API 可用于使用在 HTTP 协议之上公开的任何 Web 服务,并且不限于使用 JAX-RS 实现的服务。

      请注意 JAX-RS 客户端 API不直接调用资源类。相反,它会向服务器生成 HTTP 请求。因此,您将无法从资源类中读取注释。


      更新 1

      我不确定这是否对您有用,但由于您想从您的客户端访问服务器资源类,因此值得一提的是 Jersey 提供了一个 proxy-based client APIorg.glassfish.jersey.client.proxy 包) .

      基本思想是您可以附加standard JAX-RS annotations to an interface,然后在服务器端通过资源类实现该接口,同时通过使用java.lang.reflect.Proxy调用正确的低级客户端 API 方法。

      这个例子摘自Jersey documentation:

      考虑一个在http://localhost:8080 公开资源的服务器。资源可以通过如下接口来描述:

      @Path("myresource")
      public interface MyResourceIfc {
      
          @GET
          @Produces("text/plain")
          String get();
      
          @POST
          @Consumes("application/xml")
          @Produces("application/xml")
          MyBean postEcho(MyBean bean);
      
          @GET
          @Path("{id}")
          @Produces("text/plain")
          String getById(@PathParam("id") String id);
      }
      

      您可以使用this package 中定义的WebResourceFactory 类通过此接口访问服务器端资源。这是一个例子:

      Client client = ClientBuilder.newClient();
      WebTarget target = client.target("http://localhost:8080/");
      MyResourceIfc resource = WebResourceFactory.newResource(MyResourceIfc.class, target);
      
      String responseFromGet = resource.get();
      MyBean responseFromPost = resource.postEcho(myBeanInstance);
      String responseFromGetById = resource.getById("abc");
      

      我不确定 RESTEasy 是否提供了类似的东西。


      更新 2

      RESTEasy 还提供了proxy framework。见documentation

      RESTEasy 有一个客户端代理框架,允许您使用 JAX-RS 注释来调用远程 HTTP 资源。它的工作方式是编写一个 Java 接口并在方法和接口上使用 JAX-RS 注释。例如:

      public interface SimpleClient {
      
          @GET
          @Path("basic")
          @Produces("text/plain")
          String getBasic();
      
          @PUT
          @Path("basic")
          @Consumes("text/plain")
          void putBasic(String body);
      
          @GET
          @Path("queryParam")
          @Produces("text/plain")
          String getQueryParam(@QueryParam("param") String param);
      
          @GET
          @Path("matrixParam")
          @Produces("text/plain")
          String getMatrixParam(@MatrixParam("param") String param);
      
          @GET
          @Path("uriParam/{param}")
          @Produces("text/plain")
          int getUriParam(@PathParam("param") int param);
      }
      

      RESTEasy 有一个基于 Apache HttpClient 的简单 API。您生成一个代理,然后您可以调用代理上的方法。调用的方法会根据您如何注释方法并发布到服务器而转换为 HTTP 请求。以下是您的设置方法:

      Client client = ClientFactory.newClient();
      WebTarget target = client.target("http://example.com/base/uri");
      ResteasyWebTarget rtarget = (ResteasyWebTarget) target;
      
      SimpleClient simple = rtarget.proxy(SimpleClient.class);
      simple.putBasic("hello world");
      

      您也可以直接使用 RESTEasy 客户端扩展接口:

      ResteasyClient client = new ResteasyClientBuilder().build();
      ResteasyWebTarget target = client.target("http://example.com/base/uri");
      
      SimpleClient simple = target.proxy(SimpleClient.class);
      simple.putBasic("hello world");
      

      [...]

      该框架还支持 JAX-RS 定位器模式,但在客户端。因此,如果您有一个仅使用 @Path 注释的方法,则该代理方法将返回该方法返回的接口的新代理。

      [...]

      通常可以在客户端和服务器之间共享一个接口。在这种情况下,您只需让您的 JAX-RS 服务实现一个带注释的接口,然后重用相同的接口来创建客户端代理以在客户端调用。


      更新 3

      由于您已经在使用RESTEasy Proxy Framework 并且假设您的服务器资源实现了您用来创建客户端代理的相同接口,以下解决方案应该可以工作。

      来自 Spring AOP 的 ProxyFactory 已经包含了 RESTEasy 客户端。该解决方案基本上创建了一个 proxy of the proxy 来拦截正在调用的方法。

      以下类存储Method 实例:

      public class MethodWrapper {
      
          private Method method;
      
          public Method getMethod() {
              return method;
          }
      
          public void setMethod(Method method) {
              this.method = method;
          }
      }
      

      下面的代码很神奇:

      ResteasyClient client = new ResteasyClientBuilder().build();
      ResteasyWebTarget target = client.target("http://example.com/api");
      ExampleResource resource = target.proxy(ExampleResource.class);
      
      MethodWrapper wrapper = new MethodWrapper();
      
      ProxyFactory proxyFactory = new ProxyFactory(resource);
      proxyFactory.addAdvice(new MethodInterceptor() {
      
          @Override
          public Object invoke(MethodInvocation invocation) throws Throwable {
              wrapper.setMethod(invocation.getMethod());
              return invocation.proceed();
          }
      });
      
      ExampleResource resourceProxy = (ExampleResource) proxyFactory.getProxy();
      Response response = resourceProxy.doSomething("Hello World!");
      
      Method method = wrapper.getMethod();
      ExpectedHttpStatus expectedHttpStatus = method.getAnnotation(ExpectedHttpStatus.class);
      
      int status = response.getStatus();
      int expectedStatus = annotation.status();
      

      有关更多信息,请查看文档:

      【讨论】:

      • 是的,所以我们使用的是 RESTeasy 代理框架。所以现在的问题是,我如何挤入一个知道当前正在处理哪个资源的响应过滤器,以便我可以向接口中定义的方法添加自定义注释,并通过其值将信息传递给响应过滤器。我将在上面添加说明。
      • 对不起......现在是疯狂的时期。我们实际上走了另一条路。一旦我有更多的喘息空间,我会发布一些东西。
      • 我现在已经发布了我们的解决方案。关于您的更新#3:不知道这对于不同的方法会如何工作。我们不会为我们执行的每个方法创建客户端。所以不确定我们如何在我们的场景中使用您的解决方案访问这些信息。
      猜你喜欢
      • 2011-10-27
      • 2022-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-19
      • 1970-01-01
      • 2016-04-25
      相关资源
      最近更新 更多