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.