使用gson为WebClient设置自定义编码器/解码器或typeAdapter



我很难找到与这个特定场景相关的任何东西

我配置了spring-boot,在其中我使用反应式WebClient来使用RESTApi。我已经将其配置为使用gson,但我想知道如何为更复杂的对象添加自定义TypeAdapters

我所发现的只是对WebClient.Builder.codecs()的引用,它似乎只采用了使用ObjectMapper的Jackson转换器。

这根本不可能吗?

这似乎是对我有效的方法。它主要基于适用于Gson的Jackson代码。这并没有得到优化,可能会遗漏一些角落的情况,但它应该处理基本的json解析

助手类:

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

编码器:

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

解码器:

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

应用程序配置:

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

以及我的网络客户端的初始化:

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

相关内容

最新更新