简体   繁体   中英

set custom encoder/decoder or typeAdapter for WebClient using gson

I'm having difficulty finding anything relating to this particular scenario

I have spring boot configured, and in it i am using the reactive WebClient to consume a REST Api. I have configured this to use gson , however would like to know how to add my custom TypeAdapters for more complicated objects.

All i'm finding are references to WebClient.Builder.codecs() which seems to take only Jackson converters using ObjectMapper .

Is this not possible at all?

This seems like the approach that worked for me. It is mainly based off the Jackson code adapted to Gson. This is in no way optimised and probably has some corner cases that are missed, however it should handle basic json parsing

Helper Class:

class GsonEncoding {
    static final List<MimeType> mimeTypes = Stream.of(new MimeType("application", "json"),
                                                              new MimeType("application", "*+json"))
                                                          .collect(Collectors.toUnmodifiableList());

    static final byte[]                 NEWLINE_SEPARATOR = {'\n'};
    static final Map<MediaType, byte[]> STREAM_SEPARATORS;

    static {
        STREAM_SEPARATORS = new HashMap<>();
        STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
        STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
    }

    static void logValue(final Logger log, @Nullable Map<String, Object> hints, Object value) {
        if (!Hints.isLoggingSuppressed(hints)) {
            if (log.isLoggable(Level.FINE)) {
                boolean traceEnabled = log.isLoggable(Level.FINEST);
                String message = Hints.getLogPrefix(hints) + "Encoding [" + LogFormatUtils.formatValue(value, !traceEnabled) + "]";
                if (traceEnabled) {
                    log.log(Level.FINEST, message);
                } else {
                    log.log(Level.FINE, message);
                }
            }
        }
    }

    static boolean supportsMimeType(@Nullable MimeType mimeType) {
        return (mimeType == null || GsonEncoding.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
    }

    static boolean isTypeAdapterAvailable(Gson gson, Class<?> clazz) {
        try {
            gson.getAdapter(clazz);
            return true;
        } catch(final IllegalArgumentException e) {
            return false;
        }
    }

}

Encoder:

@Log
@RequiredArgsConstructor
@Component
public class GsonEncoder implements HttpMessageEncoder<Object> {

    private final Gson gson;

    @Override
    public List<MediaType> getStreamingMediaTypes() {
        return Collections.singletonList(MediaType.APPLICATION_STREAM_JSON);
    }

    @Override
    public boolean canEncode(final ResolvableType elementType, final MimeType mimeType) {
        Class<?> clazz = elementType.toClass();
        if (!GsonEncoding.supportsMimeType(mimeType)) {
            return false;
        }
        if (Object.class == clazz) {
            return true;
        }
        if (!String.class.isAssignableFrom(elementType.resolve(clazz))) {
           return GsonEncoding.isTypeAdapterAvailable(gson, clazz);
        }
        return false;
    }

    @Override
    public Flux<DataBuffer> encode(final Publisher<?> inputStream, final DataBufferFactory bufferFactory, final ResolvableType elementType, final MimeType mimeType, final Map<String, Object> hints) {
        Assert.notNull(inputStream, "'inputStream' must not be null");
        Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
        Assert.notNull(elementType, "'elementType' must not be null");

        if (inputStream instanceof Mono) {
            return Mono.from(inputStream)
                       .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints))
                       .flux();
        } else {
            byte[] separator = streamSeparator(mimeType);
            if (separator != null) { // streaming
                try {
                    return Flux.from(inputStream)
                               .map(value -> encodeStreamingValue(value, bufferFactory, hints, separator));
                } catch (Exception ex) {
                    return Flux.error(ex);
                }
            } else { // non-streaming
                ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
                return Flux.from(inputStream)
                           .collectList()
                           .map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints))
                           .flux();
            }

        }
    }

    @Nullable
    private byte[] streamSeparator(@Nullable MimeType mimeType) {
        for (MediaType streamingMediaType : this.getStreamingMediaTypes()) {
            if (streamingMediaType.isCompatibleWith(mimeType)) {
                return GsonEncoding.STREAM_SEPARATORS.getOrDefault(streamingMediaType, GsonEncoding.NEWLINE_SEPARATOR);
            }
        }
        return null;
    }


    @Override
    public List<MimeType> getEncodableMimeTypes() {
        return GsonEncoding.mimeTypes;
    }


    @Override
    public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
        GsonEncoding.logValue(log, hints, value);
        byte[] bytes = gson.toJson(value).getBytes();
        DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }


    private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints, byte[] separator) {
        GsonEncoding.logValue(log, hints, value);
        byte[] bytes = gson.toJson(value).getBytes();
        int offset;
        int length;
        offset = 0;
        length = bytes.length;
        DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length);
        buffer.write(bytes, offset, length);
        buffer.write(separator);
        return buffer;
    }

}

Decoder:

@Log
@RequiredArgsConstructor
@Component
public class GsonDecoder implements HttpMessageDecoder<Object> {

    private static final int MAX_IN_MEMORY_SIZE = 2000 * 1000000;

    private final Gson gson;

    @Override
    public Map<String, Object> getDecodeHints(final ResolvableType resolvableType, final ResolvableType elementType, final ServerHttpRequest request, final ServerHttpResponse response) {
        return Hints.none();
    }

    @Override
    public boolean canDecode(final ResolvableType elementType, final MimeType mimeType) {
        if (CharSequence.class.isAssignableFrom(elementType.toClass())) {
            return false;
        }
        if (!GsonEncoding.supportsMimeType(mimeType)) {
            return false;
        }
        return GsonEncoding.isTypeAdapterAvailable(gson, elementType.getRawClass());
    }

    @Override
    public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
                         @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {

        return decodeInternal(dataBuffer, targetType, hints);
    }

    private Object decodeInternal(final DataBuffer dataBuffer, final ResolvableType targetType, @Nullable Map<String, Object> hints) {
        try {
            final Object value = gson.fromJson(new InputStreamReader(dataBuffer.asInputStream()), targetType.getRawClass());
            GsonEncoding.logValue(log, hints, value);
            return value;
        } finally {
            DataBufferUtils.release(dataBuffer);
        }
    }


    @Override
    public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
                               @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
        return Flux.from(input).map(d -> decodeInternal(d, elementType, hints));
    }

    @Override
    public Mono<Object> decodeToMono(final Publisher<DataBuffer> inputStream, final ResolvableType elementType, final MimeType mimeType, final Map<String, Object> hints) {
        return DataBufferUtils.join(inputStream, MAX_IN_MEMORY_SIZE)
                              .flatMap(dataBuffer -> Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hints)));
    }

    @Override
    public List<MimeType> getDecodableMimeTypes() {
        return GsonEncoding.mimeTypes;
    }
}

Config for app:

@Configuration
public class ApplicationConfiguration {

    @Bean
    public Gson gson(){
        final GsonBuilder gsonBuilder = new GsonBuilder();
// for each of your TypeAdapters here call gsonBuilder.registerTypeAdapter()
        return gsonBuilder.create();
    }
}

And initialisation of my webclient:

@Service
@RequiredArgsConstructor
@Log
public class MyApiClient {


    private final GsonEncoder encoder;
    private final GsonDecoder decoder;

    private static final int CONNECTION_TIMEOUT = 5000;

    @PostConstruct
    public void init() {
        client = WebClient.builder()
                          .baseUrl("http://myresource.com")
                          .clientConnector(new ReactorClientHttpConnector(HttpClient.from(TcpClient.create()
                                                                                                   .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECTION_TIMEOUT)
                                                                                                   .doOnConnected(connection -> {
                                                                                                       connection.addHandlerLast(new ReadTimeoutHandler(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS));
                                                                                                       connection.addHandlerLast(new WriteTimeoutHandler(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS));
                                                                                                   })
                          )))
                          .defaultHeaders(h -> h.setBasicAuth(username, password))
                          .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
                          .defaultHeader(HttpHeaders.ACCEPT, "application/json")
                          .defaultHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8")
                          .codecs(clientCodecConfigurer -> {
                              clientCodecConfigurer.customCodecs().register(encoder);
                              clientCodecConfigurer.customCodecs().register(decoder);
                          })
                          .build();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM