Skip to content

Commit 3012cfa

Browse files
authored
Updates for HttpMessageConverters changes in framework 7/boot 4. (#1277)
Since HttpMessageConverter instances are no longer registered as beans, moved to a new strategy backed by FeignHttpMessageConverters which is mostly copied from boot. Fixes gh-1269
1 parent 5e4e00a commit 3012cfa

File tree

8 files changed

+172
-76
lines changed

8 files changed

+172
-76
lines changed

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
4848
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
4949
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
50+
import org.springframework.cloud.openfeign.support.FeignHttpMessageConverters;
5051
import org.springframework.cloud.openfeign.support.HttpMessageConverterCustomizer;
5152
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
5253
import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder;
@@ -98,16 +99,24 @@ public class FeignClientsConfiguration {
9899

99100
@Bean
100101
@ConditionalOnMissingBean
101-
public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
102-
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
102+
public FeignHttpMessageConverters feignHttpMessageConverters(
103+
ObjectProvider<HttpMessageConverter<?>> messageConverters,
104+
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
105+
return new FeignHttpMessageConverters(messageConverters, customizers);
106+
}
107+
108+
@Bean
109+
@ConditionalOnMissingBean
110+
public Decoder feignDecoder(ObjectProvider<FeignHttpMessageConverters> messageConverters) {
111+
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters)));
103112
}
104113

105114
@Bean
106115
@ConditionalOnMissingBean
107116
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
108117
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
109-
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
110-
return springEncoder(formWriterProvider, messageConverters, encoderProperties, customizers);
118+
ObjectProvider<FeignHttpMessageConverters> feignHttpMessageConverters) {
119+
return springEncoder(formWriterProvider, encoderProperties, feignHttpMessageConverters);
111120
}
112121

113122
@Bean
@@ -145,16 +154,16 @@ public FeignClientConfigurer feignClientConfigurer() {
145154
}
146155

147156
private static Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
148-
ObjectProvider<HttpMessageConverter<?>> messageConverters, FeignEncoderProperties encoderProperties,
149-
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
157+
FeignEncoderProperties encoderProperties,
158+
ObjectProvider<FeignHttpMessageConverters> feignHttpMessageConverters) {
150159
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
151160

152161
if (formWriter != null) {
153-
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), messageConverters, encoderProperties,
154-
customizers);
162+
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), encoderProperties,
163+
feignHttpMessageConverters);
155164
}
156165
else {
157-
return new SpringEncoder(new SpringFormEncoder(), messageConverters, encoderProperties, customizers);
166+
return new SpringEncoder(new SpringFormEncoder(), encoderProperties, feignHttpMessageConverters);
158167
}
159168
}
160169

@@ -182,11 +191,10 @@ static class SpringDataConfiguration {
182191
@Bean
183192
@ConditionalOnMissingBean
184193
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider,
185-
ObjectProvider<HttpMessageConverter<?>> messageConverters,
186194
ObjectProvider<FeignEncoderProperties> encoderProperties,
187-
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
188-
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider,
189-
messageConverters, encoderProperties.getIfAvailable(), customizers));
195+
ObjectProvider<FeignHttpMessageConverters> feignHttpMessageConverters) {
196+
PageableSpringEncoder encoder = new PageableSpringEncoder(
197+
springEncoder(formWriterProvider, encoderProperties.getIfAvailable(), feignHttpMessageConverters));
190198

191199
if (dataWebProperties != null) {
192200
encoder.setPageParameter(dataWebProperties.getPageable().getPageParameter());
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2013-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.support;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.beans.factory.ObjectProvider;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.converter.HttpMessageConverter;
25+
import org.springframework.http.converter.HttpMessageConverters;
26+
import org.springframework.http.converter.StringHttpMessageConverter;
27+
28+
/**
29+
* Class that mimics {@link HttpMessageConverters} and the default implementation there.
30+
* Applies the {@link HttpMessageConverterCustomizer}s and gathers all the converters into
31+
* a {@link List}.
32+
*/
33+
public class FeignHttpMessageConverters {
34+
35+
private final ObjectProvider<HttpMessageConverter<?>> messageConverters;
36+
37+
private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
38+
39+
private List<HttpMessageConverter<?>> converters;
40+
41+
public FeignHttpMessageConverters(ObjectProvider<HttpMessageConverter<?>> messageConverters,
42+
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
43+
this.messageConverters = messageConverters;
44+
this.customizers = customizers;
45+
}
46+
47+
public List<HttpMessageConverter<?>> getConverters() {
48+
initConvertersIfRequired();
49+
return converters;
50+
}
51+
52+
private void initConvertersIfRequired() {
53+
if (this.converters == null) {
54+
this.converters = new ArrayList<>();
55+
HttpMessageConverters.ClientBuilder builder = HttpMessageConverters.forClient();
56+
// TODO: allow disabling of registerDefaults
57+
builder.registerDefaults();
58+
// TODO: check if already added? Howto order?
59+
this.messageConverters.forEach((converter) -> {
60+
if (converter instanceof StringHttpMessageConverter) {
61+
builder.withStringConverter(converter);
62+
}
63+
/*
64+
* else if (converter instanceof
65+
* KotlinSerializationJsonHttpMessageConverter) {
66+
* builder.withKotlinSerializationJsonConverter(converter); }
67+
*/
68+
else if (supportsMediaType(converter, MediaType.APPLICATION_JSON)) {
69+
builder.withJsonConverter(converter);
70+
}
71+
else if (supportsMediaType(converter, MediaType.APPLICATION_XML)) {
72+
builder.withXmlConverter(converter);
73+
}
74+
else {
75+
builder.addCustomConverter(converter);
76+
}
77+
});
78+
HttpMessageConverters hmc = builder.build();
79+
hmc.forEach(converter -> converters.add(converter));
80+
customizers.forEach(customizer -> customizer.accept(this.converters));
81+
}
82+
}
83+
84+
private static boolean supportsMediaType(HttpMessageConverter<?> converter, MediaType mediaType) {
85+
for (MediaType supportedMediaType : converter.getSupportedMediaTypes()) {
86+
if (supportedMediaType.equalsTypeAndSubtype(mediaType)) {
87+
return true;
88+
}
89+
}
90+
return false;
91+
}
92+
93+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringDecoder.java

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.lang.reflect.ParameterizedType;
2222
import java.lang.reflect.Type;
2323
import java.lang.reflect.WildcardType;
24-
import java.util.List;
2524

2625
import feign.FeignException;
2726
import feign.Response;
@@ -32,7 +31,6 @@
3231
import org.springframework.http.HttpHeaders;
3332
import org.springframework.http.HttpStatusCode;
3433
import org.springframework.http.client.ClientHttpResponse;
35-
import org.springframework.http.converter.HttpMessageConverter;
3634
import org.springframework.web.client.HttpMessageConverterExtractor;
3735

3836
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
@@ -44,27 +42,18 @@
4442
*/
4543
public class SpringDecoder implements Decoder {
4644

47-
private final ObjectProvider<HttpMessageConverter<?>> messageConverters;
45+
private final ObjectProvider<FeignHttpMessageConverters> converters;
4846

49-
private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
50-
51-
public SpringDecoder(ObjectProvider<HttpMessageConverter<?>> messageConverters) {
52-
this(messageConverters, new EmptyObjectProvider<>());
53-
}
54-
55-
public SpringDecoder(ObjectProvider<HttpMessageConverter<?>> messageConverters,
56-
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
57-
this.messageConverters = messageConverters;
58-
this.customizers = customizers;
47+
public SpringDecoder(ObjectProvider<FeignHttpMessageConverters> converters) {
48+
this.converters = converters;
5949
}
6050

6151
@Override
6252
public Object decode(final Response response, Type type) throws IOException, FeignException {
6353
if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
64-
List<HttpMessageConverter<?>> converters = messageConverters.orderedStream().toList();
65-
customizers.forEach(customizer -> customizer.accept(converters));
6654
@SuppressWarnings({ "unchecked", "rawtypes" })
67-
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, converters);
55+
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type,
56+
converters.getObject().getConverters());
6857

6958
return extractor.extractData(new FeignResponseAdapter(response));
7059
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.util.Arrays;
2626
import java.util.Collection;
2727
import java.util.LinkedHashMap;
28-
import java.util.List;
2928
import java.util.Objects;
3029
import java.util.stream.Stream;
3130

@@ -72,29 +71,23 @@ public class SpringEncoder implements Encoder {
7271

7372
private final SpringFormEncoder springFormEncoder;
7473

75-
private final ObjectProvider<HttpMessageConverter<?>> messageConverters;
74+
private final ObjectProvider<FeignHttpMessageConverters> converters;
7675

7776
private final FeignEncoderProperties encoderProperties;
7877

79-
private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
80-
81-
private List<HttpMessageConverter<?>> converters;
82-
83-
public SpringEncoder(ObjectProvider<HttpMessageConverter<?>> messageConverters) {
84-
this(new SpringFormEncoder(), messageConverters, new FeignEncoderProperties(), new EmptyObjectProvider<>());
78+
public SpringEncoder(ObjectProvider<FeignHttpMessageConverters> converters) {
79+
this(new SpringFormEncoder(), new FeignEncoderProperties(), converters);
8580
}
8681

87-
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectProvider<HttpMessageConverter<?>> messageConverters,
88-
FeignEncoderProperties encoderProperties, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
82+
public SpringEncoder(SpringFormEncoder springFormEncoder, FeignEncoderProperties encoderProperties,
83+
ObjectProvider<FeignHttpMessageConverters> converters) {
8984
this.springFormEncoder = springFormEncoder;
90-
this.messageConverters = messageConverters;
9185
this.encoderProperties = encoderProperties;
92-
this.customizers = customizers;
86+
this.converters = converters;
9387
}
9488

9589
@Override
9690
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
97-
// template.body(conversionService.convert(object, String.class));
9891
if (requestBody != null) {
9992
Collection<String> contentTypes = request.headers().get(HttpEncoding.CONTENT_TYPE);
10093

@@ -120,8 +113,7 @@ public void encode(Object requestBody, Type bodyType, RequestTemplate request) t
120113

121114
private void encodeWithMessageConverter(Object requestBody, Type bodyType, RequestTemplate request,
122115
MediaType requestContentType) {
123-
initConvertersIfRequired();
124-
for (HttpMessageConverter messageConverter : converters) {
116+
for (HttpMessageConverter messageConverter : converters.getObject().getConverters()) {
125117
FeignOutputMessage outputMessage;
126118
try {
127119
if (messageConverter instanceof GenericHttpMessageConverter) {
@@ -176,13 +168,6 @@ else if (shouldHaveNullCharset(messageConverter, outputMessage)) {
176168
throw new EncodeException(message);
177169
}
178170

179-
private void initConvertersIfRequired() {
180-
if (converters == null) {
181-
converters = messageConverters.orderedStream().toList();
182-
customizers.forEach(customizer -> customizer.accept(converters));
183-
}
184-
}
185-
186171
private boolean shouldHaveNullCharset(HttpMessageConverter messageConverter, FeignOutputMessage outputMessage) {
187172
return binaryContentType(outputMessage) || messageConverter instanceof ByteArrayHttpMessageConverter
188173
|| messageConverter instanceof ProtobufHttpMessageConverter && ProtobufHttpMessageConverter.PROTOBUF

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
import org.junit.jupiter.api.Test;
2323

2424
import org.springframework.beans.factory.ObjectProvider;
25-
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConverters;
25+
import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider;
26+
import org.springframework.cloud.openfeign.support.FeignHttpMessageConverters;
2627
import org.springframework.cloud.openfeign.support.SpringDecoder;
27-
import org.springframework.http.converter.HttpMessageConverter;
2828

2929
import static org.assertj.core.api.Assertions.assertThatCode;
3030
import static org.mockito.Mockito.mock;
31-
import static org.mockito.Mockito.when;
3231

3332
/**
3433
* Tests for {@link SpringDecoder}.
@@ -41,9 +40,9 @@ class SpringDecoderTests {
4140

4241
@BeforeEach
4342
void setUp() {
44-
ObjectProvider<HttpMessageConverter<?>> factory = mock();
45-
when(factory.orderedStream()).thenReturn(new HttpMessageConverters().getConverters().stream());
46-
decoder = new SpringDecoder(factory);
43+
ObjectProvider<FeignHttpMessageConverters> converters = new SimpleObjectProvider<>(
44+
new FeignHttpMessageConverters(mock(), mock()));
45+
decoder = new SpringDecoder(converters);
4746
}
4847

4948
// Issue: https://github.com/spring-cloud/spring-cloud-openfeign/issues/972

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufNotInClasspathTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.junit.jupiter.api.Test;
2323

2424
import org.springframework.beans.factory.ObjectProvider;
25+
import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider;
26+
import org.springframework.cloud.openfeign.support.FeignHttpMessageConverters;
2527
import org.springframework.cloud.openfeign.support.SpringEncoder;
2628
import org.springframework.cloud.test.ClassPathExclusions;
2729
import org.springframework.http.converter.HttpMessageConverter;
@@ -46,7 +48,8 @@ void testEncodeWhenProtobufNotInClasspath() {
4648
when(factory.orderedStream()).thenReturn(protobufHttpMessageConverters.stream());
4749
RequestTemplate requestTemplate = new RequestTemplate();
4850
requestTemplate.method(POST);
49-
new SpringEncoder(factory).encode("a=b", String.class, requestTemplate);
51+
new SpringEncoder(new SimpleObjectProvider<>(new FeignHttpMessageConverters(factory, mock()))).encode("a=b",
52+
String.class, requestTemplate);
5053
}
5154

5255
}

spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/encoding/proto/ProtobufSpringEncoderTests.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.ArrayList;
2323
import java.util.HashMap;
2424
import java.util.List;
25+
import java.util.stream.Stream;
2526

2627
import com.google.protobuf.InvalidProtocolBufferException;
2728
import feign.RequestTemplate;
@@ -43,6 +44,8 @@
4344
import org.mockito.stubbing.Answer;
4445

4546
import org.springframework.beans.factory.ObjectProvider;
47+
import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider;
48+
import org.springframework.cloud.openfeign.support.FeignHttpMessageConverters;
4649
import org.springframework.cloud.openfeign.support.SpringEncoder;
4750
import org.springframework.http.converter.HttpMessageConverter;
4851
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
@@ -51,7 +54,6 @@
5154
import static org.assertj.core.api.Assertions.assertThat;
5255
import static org.assertj.core.api.Assertions.fail;
5356
import static org.mockito.Mockito.mock;
54-
import static org.mockito.Mockito.when;
5557

5658
/**
5759
* Test {@link SpringEncoder} with {@link ProtobufHttpMessageConverter}
@@ -111,10 +113,14 @@ void testProtobufWithCharsetWillFail() throws IOException {
111113
}
112114

113115
private SpringEncoder newEncoder() {
114-
ObjectProvider<HttpMessageConverter<?>> factory = mock();
115-
List<HttpMessageConverter<?>> protobufHttpMessageConverters = List.of(new ProtobufHttpMessageConverter());
116-
when(factory.orderedStream()).thenReturn(protobufHttpMessageConverters.stream());
117-
return new SpringEncoder(factory);
116+
ObjectProvider<HttpMessageConverter<?>> factory = new SimpleObjectProvider<>(
117+
new ProtobufHttpMessageConverter()) {
118+
@Override
119+
public Stream<HttpMessageConverter<?>> stream() {
120+
return Stream.of(getObject());
121+
}
122+
};
123+
return new SpringEncoder(new SimpleObjectProvider<>(new FeignHttpMessageConverters(factory, mock())));
118124
}
119125

120126
private RequestTemplate newRequestTemplate() {

0 commit comments

Comments
 (0)