【问题标题】:Hot to get body as String from spring reactive ClientRequest?从spring响应ClientRequest获取正文作为字符串很热?
【发布时间】:2020-07-24 04:17:06
【问题描述】:

在测试方法中接收到org.springframework.web.reactive.function.client.ClientRequest 的实例。

我想验证它的 HttpMethod、URI 和 body。

除了身体之外,如何得到所有东西是很明显的。

ClientRequest request = makeInstance(...);

assertEquals(HttpMethod.POST, request.method());
assertEquals("somewhere/else", request.url().toString());

// ? unclear how to extract body using the BodyInserter

BodyInserter<?, ? super ClientHttpRequest> inserter = request.body();

inserter.insert(%outputMessage%, %context%);

我在 Spring how BodyInserters are tested 的来源中找到了。如何创建BodyInserter.Context(第二个参数)或多或少清楚,但我不明白如何构造第一个参数,因此可以通过它提取请求体。

请展示一种从ClientRequest 实例获取请求正文的传统(或至少可用)方式。

【问题讨论】:

    标签: java spring reactive-programming spring-webflux


    【解决方案1】:

    对于这样一个简单的案例来说有点复杂,但我需要实现 5 个类,从 ClientRequest 中提取一个主体。

    似乎太多了,我仍然很好奇这个问题是否有一个简短的解决方案。欢迎您对这个问题提出另一个答案,所以我可以接受。

    不幸的是,必须说ClientRequestBodyInserters 以及org.springframework.web.reactive.*** 的大部分其他东西的设计都有很大的改进空间。目前它只是一堆接口,每个接口都有大量方法,根据这些类,测试代码通常需要付出很多努力。

    实现此方法的主要目标:

    static <T> T extractBody(ClientRequest request, Class<T> clazz) {
    
      InsertionReceiver<T> receiver = InsertionReceiver.forClass(clazz);
      return receiver.receiveValue(request.body());
    }
    

    这里是InsertionReceiver的实现:


    import org.springframework.http.ReactiveHttpOutputMessage;
    import org.springframework.web.reactive.function.BodyInserter;
    
    public interface InsertionReceiver<T> {
    
      T receiveValue(BodyInserter<?, ? extends ReactiveHttpOutputMessage> bodyInserter);
    
      static <T> InsertionReceiver<T> forClass(Class<T> clazz) {
        return new SimpleValueReceiver<>(clazz);
      }
    }
    

    import java.util.concurrent.atomic.AtomicReference;
    import org.springframework.http.ReactiveHttpOutputMessage;
    import org.springframework.web.reactive.function.BodyInserter;
        
    class SimpleValueReceiver<T> implements InsertionReceiver<T> {
    
      private static final Object DUMMY = new Object();
    
      private final Class<T> clazz;
      private final AtomicReference<Object> reference;
    
      SimpleValueReceiver(Class<T> clazz) {
        this.clazz = clazz;
        this.reference = new AtomicReference<>(DUMMY);
      }
    
      @Override
      public T receiveValue(BodyInserter<?, ? extends ReactiveHttpOutputMessage> bodyInserter) {
        demandValueFrom(bodyInserter);
    
        return receivedValue();
      }
    
      private void demandValueFrom(BodyInserter<?, ? extends ReactiveHttpOutputMessage> bodyInserter) {    
        var inserter = (BodyInserter<?, ReactiveHttpOutputMessage>) bodyInserter;
    
        inserter.insert(
            MinimalHttpOutputMessage.INSTANCE,
            new SingleWriterContext(new WriteToConsumer<>(reference::set))
        );
      }
    
      private T receivedValue() {
        Object value = reference.get();
        reference.set(DUMMY);
    
        T validatedValue;
    
        if (value == DUMMY) {
          throw new RuntimeException("Value was not received, Check your inserter worked properly");
        } else if (!clazz.isAssignableFrom(value.getClass())) {
          throw new RuntimeException(
              "Value has unexpected type ("
                  + value.getClass().getTypeName()
                  + ") instead of (" + clazz.getTypeName() + ")");
        } else {
          validatedValue = clazz.cast(value);
        }
    
        return validatedValue;
      }
    }
    

    class WriteToConsumer<T> implements HttpMessageWriter<T> {
    
      private final Consumer<T> consumer;
      private final List<MediaType> mediaTypes;
    
      WriteToConsumer(Consumer<T> consumer) {
        this.consumer = consumer;
        this.mediaTypes = Collections.singletonList(MediaType.ALL);
      }
    
      @Override
      public List<MediaType> getWritableMediaTypes() {
        return mediaTypes;
      }
    
      @Override
      public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
        return true;
      }
    
      @Override
      public Mono<Void> write(
          Publisher<? extends T> inputStream,
          ResolvableType elementType,
          MediaType mediaType,
          ReactiveHttpOutputMessage message,
          Map<String, Object> hints
      ) {
        inputStream.subscribe(new OneValueConsumption<>(consumer));
        return Mono.empty();
      }
    }
    

    class MinimalHttpOutputMessage implements ReactiveHttpOutputMessage {
    
      public static MinimalHttpOutputMessage INSTANCE = new MinimalHttpOutputMessage();
    
      private MinimalHttpOutputMessage() {
      }
    
      @Override
      public HttpHeaders getHeaders() {
        return HttpHeaders.EMPTY;
      }
    
      // other overridden methods are omitted as they do nothing,
      // i.e. return null, false, or have empty bodies
    }
    

    class OneValueConsumption<T> implements Subscriber<T> {
    
      private final Consumer<T> consumer;
      private int remainedAccepts;
    
      public OneValueConsumption(Consumer<T> consumer) {
        this.consumer = Objects.requireNonNull(consumer);
        this.remainedAccepts = 1;
      }
    
      @Override
      public void onSubscribe(Subscription s) {
        s.request(1);
      }
    
      @Override
      public void onNext(T o) {
        if (remainedAccepts > 0) {
          consumer.accept(o);
          remainedAccepts -= 1;
        } else {
          throw new RuntimeException("No more values can be consumed");
        }
      }
    
      @Override
      public void onError(Throwable t) {
        throw new RuntimeException("Single value was not consumed", t);
      }
    
      @Override
      public void onComplete() {
        // nothing
      }
    }
    

    class SingleWriterContext implements BodyInserter.Context {
    
      private final List<HttpMessageWriter<?>> singleWriterList;
    
      SingleWriterContext(HttpMessageWriter<?> writer) {
        this.singleWriterList = List.of(writer);
      }
    
      @Override
      public List<HttpMessageWriter<?>> messageWriters() {
        return singleWriterList;
      }
    
      @Override
      public Optional<ServerHttpRequest> serverRequest() {
        return Optional.empty();
      }
    
      @Override
      public Map<String, Object> hints() {
        return null;
      }
    }
    

    【讨论】:

    • 这可以很好地获取请求正文,能否请您也添加响应正文的代码?我尝试这样做,但响应 Body 需要 BodyExtractor。
    • @ArunkumarArjunan 如果您的意思是 ClientResponse,那么提取它的主体是一个不同(而且更简单)的问题。在测试代​​码中它可能是response.body(BodyExtractors.toMono(String.class)).block()。但我建议查看 WebTestClient,它可以帮助更自然地测试响应。
    • extractBody(ClientRequest request, Class clazz) -> 这里的 clazz 是你的模型吗?传递一个String.Class会抛出异常,我们还可以传递什么其他类型给方法?
    【解决方案2】:

    我找到了一个比较简单的方法,就是模仿BodyInserters.fromValue()实现自己的BodyInserter

    public static class CustomerInserter<T> implements BodyInserter<T, ReactiveHttpOutputMessage> {
    
        private T body;
    
        private CustomerInserter(T body) {
            this.body = body;
        }
    
        public static <T> CustomerInserter<T> fromValue(T body) {
            return new CustomerInserter<T>(body);
        }
    
        public T getBody() {
            return this.body;
        }
    
        @Override
        public Mono<Void> insert(ReactiveHttpOutputMessage outputMessage, Context context) {
            Mono<T> publisher = Mono.just(this.body);
            MediaType mediaType = outputMessage.getHeaders().getContentType();
            ResolvableType bodyType = ResolvableType.forInstance(this.body);
            return context.messageWriters().stream()
                    .filter(messageWriter -> messageWriter.canWrite(bodyType, mediaType))
                    .findFirst()
                    .map(item -> (HttpMessageWriter<T>) item)
                    .map(writer -> this.write(publisher, bodyType, mediaType, outputMessage, context, writer))
                    .orElseGet(() -> Mono.error(unsupportedError(bodyType, context, mediaType)));
        }
    
        private Mono<Void> write(Publisher<? extends T> input, ResolvableType type,
                                            @Nullable MediaType mediaType, ReactiveHttpOutputMessage message,
                                            BodyInserter.Context context, HttpMessageWriter<T> writer) {
    
            return context.serverRequest()
                    .map(request -> {
                        ServerHttpResponse response = (ServerHttpResponse) message;
                        return writer.write(input, type, type, mediaType, request, response, context.hints());
                    })
                    .orElseGet(() -> writer.write(input, type, mediaType, message, context.hints()));
        }
    
        private UnsupportedMediaTypeException unsupportedError(ResolvableType bodyType,
                                                       BodyInserter.Context context, @Nullable MediaType mediaType) {
    
            List<MediaType> supportedMediaTypes = context.messageWriters().stream()
                    .flatMap(reader -> reader.getWritableMediaTypes(bodyType).stream())
                    .collect(Collectors.toList());
    
            return new UnsupportedMediaTypeException(mediaType, supportedMediaTypes, bodyType);
        }
    }
    

    简单的单元测试。

    Response response = webClient.post()
                .uri("/xxx")
                .body(CustomerInserter.fromValue(body)) //
                .retrieve()
                .bodyToMono(Response.class)
                .block();
    
    WebClient webClient = WebClient.builder()
                .baseUrl("http://127.0.0.1:8080")
                .filter((request, next) -> {
                    CustomerInserter<?> inserter = (CustomerInserter<?>) request.body();
                    // Some things can be done here
                    Object body = inserter.getBody();
                    return next.exchange(request);
                }).build();
    

    【讨论】:

      猜你喜欢
      • 2011-08-11
      • 1970-01-01
      • 2018-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-03
      • 1970-01-01
      • 2016-05-03
      相关资源
      最近更新 更多