Горячее ли получить тело в виде String из весеннего реактивного ClientRequest? - PullRequest
0 голосов
/ 11 апреля 2020

В тестовом методе получен экземпляр 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 я обнаружил, как тестируются BodyInserters . Более или менее понятно, как создать BodyInserter.Context (второй параметр), но я не могу понять, как создать первый параметр, поэтому тело запроса может быть извлечено через него.

Пожалуйста, покажите обычный (или хотя бы пригодный для использования) способ получить тело запроса из экземпляра ClientRequest.

1 Ответ

0 голосов
/ 18 апреля 2020

Итак, немного сложнее для такого простого случая, но мне потребовалось реализовать 5 классов, чтобы извлечь тело из ClientRequest. Кажется, это слишком много для такой простой проблемы.

Мне все еще любопытно, может ли быть короткое решение для этого, поэтому вы можете предложить другой ответ на этот вопрос, чтобы я мог принять его .

К сожалению, нужно сказать, что дизайн ClientRequest, BodyInserters и большинства других вещей из 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:


1.

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

2.

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 outputMessage = MinimalHttpOutputMessage.INSTANCE;

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

3.

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

4.

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
}

5.

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
  }
}

6.

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;
  }
}
...