Spring feign cloud支持表单等数据类型

Spring Cloud FeignClient支持表单等数据

Spring Cloud Feign Client默认仅仅提供了application/json格式的支持,当然,Spring cloud feign client支持自定规则,如contract,encoder,decoder等等,通常我们会自定义个业务feignClient

  1. 自定义Feignclient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@FeignClient(
    name = "${api.project}",
    configuration = ApiClientConfiguration.class
)
public @interface ApiClient {

    String path() default "";

}
  1. 自定义ApiClientConfiguration 配置
class ApiClientConfiguration extends BaseFeignClientConfiguration {

    @Bean
    public Contract feignContract(ObjectProvider<ConversionService> 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类型

multipart/form-data 通常表现在文件上传等业务场景,为了支持这个类型数据,我们需要做如下工作

  1. 引入依赖:
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
            <version>3.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>3.8.0</version>
        </dependency>

注意: 上面的需要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类型

application/x-www-form-urlencoded类型通常用来在url加入格式如 http://xxx.com?key1=val2&key2=val2。为了支持该类型,在基于上面的操作外,我们需要做如下工作:

  1. 增加一个自定义encoder DfsEncoder (需要lombok支持)
/**
 * @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<ContentType, IContentProcessor> 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<String, Object> data;
        if (MAP_STRING_WILDCARD.equals(bodyType)) {
            data = (Map<String, Object>) 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<String, Collection<String>> 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<ContentType, IContentProcessor> 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<String, Object> 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<String, Object> data) throws EncodeException {
        StringBuilder bodyData = new StringBuilder();
        for (Entry<String, Object> 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<String, Object> 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<ContentType, IContentProcessor> 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<String, Object> data;
        if (MAP_STRING_WILDCARD.equals(bodyType)) {
            data = (Map<String, Object>) 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<String, Collection<String>> 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<HttpMessageConverters> messageConverters;

    @Bean
    public Contract feignContract(ObjectProvider<ConversionService> 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值