Spring Cloud Feign Client默认仅仅提供了application/json格式的支持,当然,Spring cloud feign client支持自定规则,如contract,encoder,decoder等等,通常我们会自定义个业务feignClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@FeignClient(
name = "${api.project}",
configuration = ApiClientConfiguration.class
)
public @interface ApiClient {
String path() default "";
}
class ApiClientConfiguration extends BaseFeignClientConfiguration {
@Bean
public Contract feignContract(ObjectProvider feignConversionService) {
return super.feignContract(feignConversionService.getIfAvailable());
}
@Bean
@Primary
@Override
public RequestInterceptor requestInterceptor() {
return super.requestInterceptor();
}
@Bean
@Override
public ErrorDecoder errorDecoder() {
return super.errorDecoder();
}
}
详细参考:https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html
默认使用SpringEncoder,仅仅支持application/json,但是如果我们需要支持application/x-www-form-urlencoded或者multipart/form-data呢?
multipart/form-data 通常表现在文件上传等业务场景,为了支持这个类型数据,我们需要做如下工作
io.github.openfeign.form
feign-form
3.8.0
io.github.openfeign.form
feign-form-spring
3.8.0
注意: 上面的需要spring版本支持9.5及以上
2. 在ApiClientConfiguration 中增加代码
@Bean
@Primary
// @Scope("prototype")
public Encoder multipartFormEncoder() {
return new SpringFormEncoder();
}
那么feignClient就支持multipart/form-data了,但是如果这么做的话,那么就会导致application/json格式的数据兼容不好
application/x-www-form-urlencoded类型通常用来在url加入格式如 http://xxx.com?key1=val2&key2=val2。为了支持该类型,在基于上面的操作外,我们需要做如下工作:
/**
* @author Artem Labazin
*/
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class DfsEncoder implements Encoder {
private static final String CONTENT_TYPE_HEADER;
private static final Pattern CHARSET_PATTERN;
static {
CONTENT_TYPE_HEADER = "Content-Type";
CHARSET_PATTERN = Pattern.compile("(?<=charset=)([\\w\\-]+)");
}
Encoder delegate;
Map processors;
/**
* Constructor with the default Feign's encoder as a delegate.
*/
public DfsEncoder() {
this(new Default());
}
/**
* Constructor with specified delegate encoder.
*
* @param delegate delegate encoder, if this encoder couldn't encode object.
*/
public DfsEncoder(Encoder delegate) {
this.delegate = delegate;
val list = Arrays.asList(
new DfsFormProcessor(), new UrlencodedProcessor()
);
processors = new HashMap<>(list.size(), 1.F);
for (IContentProcessor processor : list) {
processors.put(processor.getSupportedContentType(), processor);
}
}
@Override
@SuppressWarnings("unchecked")
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
String contentTypeValue = getContentTypeValue(template.headers());
ContentType contentType = ContentType.of(contentTypeValue);
if (!processors.containsKey(contentType)) {
delegate.encode(object, bodyType, template);
return;
}
final IContentProcessor processor = processors.get(contentType);
final ProcessorType processorType = processor.processType();
if (processorType == ProcessorType.ENCODER) {
processor.encode(object, bodyType, template);
return;
}
// 默认处理器处理
Map data;
if (MAP_STRING_WILDCARD.equals(bodyType)) {
data = (Map) object;
} else if (PojoUtil.isUserPojo(bodyType)) {
data = PojoUtil.toMap(object);
} else {
delegate.encode(object, bodyType, template);
return;
}
Charset charset = getCharset(contentTypeValue);
processor.process(template, charset, data);
}
public final IContentProcessor getContentProcessor(ContentType type) {
return processors.get(type);
}
@SuppressWarnings("PMD.AvoidBranchingStatementAsLastInLoop")
private String getContentTypeValue(Map> headers) {
for (val entry : headers.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(CONTENT_TYPE_HEADER)) {
continue;
}
for (val contentTypeValue : entry.getValue()) {
if (contentTypeValue == null) {
continue;
}
return contentTypeValue;
}
}
return null;
}
private Charset getCharset(String contentTypeValue) {
val matcher = CHARSET_PATTERN.matcher(contentTypeValue);
return matcher.find()
? Charset.forName(matcher.group(1))
: StandardCharsets.UTF_8;
}
}
在代码
Map processors;
// 很多代码
processors = new HashMap<>(list.size(), 1.F);
for (IContentProcessor processor : list) {
processors.put(processor.getSupportedContentType(), processor);
}
中,表示了我们需要定义支持的contentType对应的processor.
2. 定义application/x-www-form-urlencoded对应的processor,processor的接口定义如下:
public interface IContentProcessor extends Encoder {
String CONTENT_TYPE_HEADER = "Content-Type";
String CRLF = "\r\n";
/**
* Processes a request.
*
* @param template Feign's request template.
* @param charset request charset from 'Content-Type' header (UTF-8 by default).
* @param data reqeust data.
* @throws EncodeException in case of any encode exception
*/
default void process(RequestTemplate template, Charset charset, Map data) throws EncodeException {
}
/**
* Returns supported {@link ContentType} of this processor.
*
* @return supported content type enum value.
*/
ContentType getSupportedContentType();
@Override
default void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
}
/**
* 处理器类型
*
* @return 处理器类型,默认为processor
*/
default ProcessorType processType() {
return ProcessorType.PROCESSOR;
}
}
application/x-www-form-urlencoded的processor UrlencodedProcessor 如下:
public class UrlencodedProcessor implements IContentProcessor {
private static final char QUERY_DELIMITER = '&';
private static final char EQUAL_SIGN = '=';
@SneakyThrows
private static String encode(Object string, Charset charset) {
return URLEncoder.encode(string.toString(), charset.name());
}
@Override
public void process(RequestTemplate template, Charset charset, Map data) throws EncodeException {
StringBuilder bodyData = new StringBuilder();
for (Entry entry : data.entrySet()) {
if (entry == null || entry.getKey() == null) {
continue;
}
if (bodyData.length() > 0) {
bodyData.append(QUERY_DELIMITER);
}
bodyData.append(createKeyValuePair(entry, charset));
}
String contentTypeValue = new StringBuilder()
.append(getSupportedContentType().getHeader())
.append("; charset=").append(charset.name())
.toString();
byte[] bytes = bodyData.toString().getBytes(charset);
// reset header
template.header(CONTENT_TYPE_HEADER, Collections.emptyList());
template.header(CONTENT_TYPE_HEADER, contentTypeValue);
template.body(bytes, charset);
}
@Override
public ContentType getSupportedContentType() {
return ContentType.URLENCODED;
}
private String createKeyValuePair(Entry entry, Charset charset) {
String encodedKey = encode(entry.getKey(), charset);
Object value = entry.getValue();
if (value == null) {
return encodedKey;
} else if (value.getClass().isArray()) {
return createKeyValuePairFromArray(encodedKey, value, charset);
} else if (value instanceof Collection) {
return createKeyValuePairFromCollection(encodedKey, value, charset);
}
return new StringBuilder()
.append(encodedKey)
.append(EQUAL_SIGN)
.append(encode(value, charset))
.toString();
}
@SuppressWarnings("unchecked")
private String createKeyValuePairFromCollection(String key, Object values, Charset charset) {
val collection = (Collection) values;
val array = collection.toArray(new Object[0]);
return createKeyValuePairFromArray(key, array, charset);
}
private String createKeyValuePairFromArray(String key, Object values, Charset charset) {
val result = new StringBuilder();
val array = (Object[]) values;
for (int index = 0; index < array.length; index++) {
val value = array[index];
if (value == null) {
continue;
}
if (index > 0) {
result.append(QUERY_DELIMITER);
}
result.append(key)
.append(EQUAL_SIGN)
.append(encode(value, charset));
}
return result.toString();
}
}
在自定义的Encoder中DfsEncoder的构造函数中代码中注入了这个processor
val list = Arrays.asList(
new UrlencodedProcessor()
);
当你需要的时候,在ApiClientConfiguration中的自定义Encoder生命为
@Bean
@Primary
// @Scope("prototype")
public Encoder multipartFormEncoder() {
return new DfsEncoder();
}
然而,这仅仅支持 application/x-www-form-urlencoded
支持application/json,application/x-www-form-urlencoded和multipart/form-data,通常在实际开发,这三种类型都是存在的,那么如何才能都支持呢?
结合上面的描述,我们需要在自定义encoder中根据类型分配processor,首先参考如下结构
最后自定义Encoder-DfsEncoder 代码如下:
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import static lombok.AccessLevel.PRIVATE;
/**
* @author Artem Labazin
*/
@FieldDefaults(level = PRIVATE, makeFinal = true)
public class DfsEncoder implements Encoder {
private static final String CONTENT_TYPE_HEADER;
private static final Pattern CHARSET_PATTERN;
static {
CONTENT_TYPE_HEADER = "Content-Type";
CHARSET_PATTERN = Pattern.compile("(?<=charset=)([\\w\\-]+)");
}
Encoder delegate;
Map processors;
/**
* Constructor with the default Feign's encoder as a delegate.
*/
public DfsEncoder() {
this(new Default());
}
/**
* Constructor with specified delegate encoder.
*
* @param delegate delegate encoder, if this encoder couldn't encode object.
*/
public DfsEncoder(Encoder delegate) {
this.delegate = delegate;
val list = Arrays.asList(
new DfsFormProcessor(), new UrlencodedProcessor()
);
processors = new HashMap<>(list.size(), 1.F);
for (IContentProcessor processor : list) {
processors.put(processor.getSupportedContentType(), processor);
}
}
@Override
@SuppressWarnings("unchecked")
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
String contentTypeValue = getContentTypeValue(template.headers());
ContentType contentType = ContentType.of(contentTypeValue);
if (!processors.containsKey(contentType)) {
delegate.encode(object, bodyType, template);
return;
}
final IContentProcessor processor = processors.get(contentType);
final ProcessorType processorType = processor.processType();
if (processorType == ProcessorType.ENCODER) {
processor.encode(object, bodyType, template);
return;
}
// 默认处理器处理
Map data;
if (MAP_STRING_WILDCARD.equals(bodyType)) {
data = (Map) object;
} else if (PojoUtil.isUserPojo(bodyType)) {
data = PojoUtil.toMap(object);
} else {
delegate.encode(object, bodyType, template);
return;
}
Charset charset = getCharset(contentTypeValue);
processor.process(template, charset, data);
}
public final IContentProcessor getContentProcessor(ContentType type) {
return processors.get(type);
}
@SuppressWarnings("PMD.AvoidBranchingStatementAsLastInLoop")
private String getContentTypeValue(Map> headers) {
for (val entry : headers.entrySet()) {
if (!entry.getKey().equalsIgnoreCase(CONTENT_TYPE_HEADER)) {
continue;
}
for (val contentTypeValue : entry.getValue()) {
if (contentTypeValue == null) {
continue;
}
return contentTypeValue;
}
}
return null;
}
private Charset getCharset(String contentTypeValue) {
val matcher = CHARSET_PATTERN.matcher(contentTypeValue);
return matcher.find()
? Charset.forName(matcher.group(1))
: StandardCharsets.UTF_8;
}
}
ApiClientConfiguration定义如下:
class ApiClientConfiguration extends BaseFeignClientConfiguration {
@Autowired
private ObjectFactory messageConverters;
@Bean
public Contract feignContract(ObjectProvider feignConversionService) {
return super.feignContract(feignConversionService.getIfAvailable());
}
@Bean
@Primary
@Override
public RequestInterceptor requestInterceptor() {
return super.requestInterceptor();
}
@Bean
@Override
public ErrorDecoder errorDecoder() {
return super.errorDecoder();
}
@Bean
@Primary
// @Scope("prototype")
public Encoder multipartFormEncoder() {
final SpringEncoder encoder = new SpringEncoder(messageConverters);
return new DfsEncoder(encoder);
}
}
支持类如下:
ProcessorType
public enum ProcessorType {
/**
* 常规的处理
*/
PROCESSOR,
/**
* 编码器
*/
ENCODER;
}