解决SpringCloud的Feign的post不能传输自定义对象

原因

项目中,发现feign只能传输json字符串,也是就content-type为application/json,但是我想直接post参数为Map或者自定义bean,
于是我把content-type改为application/x-www-form-urlencoded,但是出现异常数据类别不支持, 于是我就挖掘一下源码:

发现

AbstractMessageConverterMethodArgumentResolver的这个类会在拦截器之后,验证参数类型
在这里插入图片描述
在这里插入图片描述
然后断点发现application/x-www-form-urlencoded是由AllEncompassingFormHttpMessageConverter处理的,
在它的父类,看到它要求参数为MulitValueMap,所以我设置为
Map, 它不支持,看了canread方法和read方法,了解了它的转换方式

在这里插入图片描述
在这里插入图片描述

查看一下,这是一个list, 实现HttpMessageConverter,可以自定义转换器,于是参考AllEncompassingFormHttpMessageConverter,自定义一个Map的
在这里插入图片描述
大部分我是拷贝AllEncompassingFormHttpMessageConverter,然后修改的

@Slf4j
public class CustomHttpMessageConverter implements HttpMessageConverter<Map<String,  Object>> {

    private List<MediaType> supportedMediaTypes = new ArrayList<>();

    private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();

    private Charset charset = FormHttpMessageConverter.DEFAULT_CHARSET;

    @Nullable
    private Charset multipartCharset = FormHttpMessageConverter.DEFAULT_CHARSET;

    private static final MediaType DEFAULT_FORM_DATA_MEDIA_TYPE =
            new MediaType(MediaType.APPLICATION_FORM_URLENCODED, FormHttpMessageConverter.DEFAULT_CHARSET);

    public CustomHttpMessageConverter() {
        this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        this.partConverters.add(new ByteArrayHttpMessageConverter());
    }

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        if (!Map.class.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.getType().equalsIgnoreCase("multipart")) {
                // We can't read multipart, so skip this supported media type.
                continue;
            }
            if (supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        if (!Map.class.isAssignableFrom(clazz)) {
            return false;
        }
        if (mediaType == null || MediaType.ALL.equals(mediaType)) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.isCompatibleWith(mediaType)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.unmodifiableList(this.supportedMediaTypes);
    }

    @Override
    public Map<String, Object> read(Class<? extends Map<String, Object>> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = (contentType != null && contentType.getCharset() != null ?
                contentType.getCharset() : this.charset);
        String body = URLDecoder.decode(StreamUtils.copyToString(inputMessage.getBody(), charset));

        String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
        Map<String, Object> result = new LinkedHashMap<>(pairs.length);
        for (String pair : pairs) {
            if(pair.startsWith("table")){
                int idx = pair.indexOf('=');
                if (idx == -1) {
                    result.put(URLDecoder.decode(pair, charset.name()), null);
                }
                else {
                    pair = pair.replace("table=", "");
                    String[] pairArr = pair.split("=");
                    String name = pairArr[0];
                    String value =  "null".equals(pairArr[1]) ? null : pairArr[1];
                    result.put(name, value);
                }
            }
        }
        return result;
    }
    @Override
    public void write(Map<String, Object> map, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        try {
            if (isMap(map, contentType)) {
                writeMappart(map, contentType, outputMessage);
            }
            else {
                writeMapForm((Map<String, Object>) map, contentType, outputMessage);
            }
        } catch (IOException e) {
           log.error("write error", e);
        }
    }

    private Boolean isMap(Map<String, Object> map, @Nullable MediaType contentType){
        if (contentType != null) {
            return contentType.getType().equalsIgnoreCase("multipart");
        }
        for (Object value : map.values()) {
            if (value != null && !(value instanceof String)) {
                return true;
            }
        }
        return false;
    }

    private boolean isMultipart(MultiValueMap<String, ?> map, @Nullable MediaType contentType) {
        if (contentType != null) {
            return contentType.getType().equalsIgnoreCase("multipart");
        }
        for (List<?> values : map.values()) {
            for (Object value : values) {
                if (value != null && !(value instanceof String)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void writeMapForm(Map<String, Object> formData, @Nullable MediaType contentType,
                           HttpOutputMessage outputMessage) throws IOException {

        contentType = getFormContentType(contentType);
        outputMessage.getHeaders().setContentType(contentType);

        Charset charset = contentType.getCharset();
        Assert.notNull(charset, "No charset"); // should never occur

        byte[] bytes = serializeForm(formData, charset).getBytes(charset);
        outputMessage.getHeaders().setContentLength(bytes.length);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
        }
        else {
            StreamUtils.copy(bytes, outputMessage.getBody());
        }
    }

    private void writeForm(Map<String, Object> formData, @Nullable MediaType contentType,
                           HttpOutputMessage outputMessage) throws IOException {

        contentType = getFormContentType(contentType);
        outputMessage.getHeaders().setContentType(contentType);

        Charset charset = contentType.getCharset();
        Assert.notNull(charset, "No charset"); // should never occur

        byte[] bytes = serializeForm(formData, charset).getBytes(charset);
        outputMessage.getHeaders().setContentLength(bytes.length);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(bytes, outputStream));
        }
        else {
            StreamUtils.copy(bytes, outputMessage.getBody());
        }
    }

    protected MediaType getFormContentType(@Nullable MediaType contentType) {
        if (contentType == null) {
            return DEFAULT_FORM_DATA_MEDIA_TYPE;
        }
        else if (contentType.getCharset() == null) {
            return new MediaType(contentType, this.charset);
        }
        else {
            return contentType;
        }
    }

    protected String serializeForm(Map<String, Object> formData, Charset charset) {
        StringBuilder builder = new StringBuilder();
        formData.forEach((name, value) -> {
            if (name == null) {
                //Assert.notNull(value, "Null name in form data: " + formData);
                Assert.notNull(name, "Null name in form data: " + formData);
                return;
            }
        });

        return builder.toString();
    }



    private boolean isFilenameCharsetSet() {
        return (this.multipartCharset != null);
    }


    private void writeMappart(
            Map<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException {

        if (contentType == null) {
            contentType = MediaType.MULTIPART_FORM_DATA;
        }
        byte[] boundary = generateMultipartBoundary();
        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(outputStream -> {
                writeMapParts(outputStream, parts, boundary);
                writeEnd(outputStream, boundary);
            });
        }
        else {
            writeMapParts(outputMessage.getBody(), parts, boundary);
            writeEnd(outputMessage.getBody(), boundary);
        }

    }

    private void writeMultipart(
            MultiValueMap<String, Object> parts, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException {
        if (contentType == null) {
            contentType = MediaType.MULTIPART_FORM_DATA;
        }

        byte[] boundary = generateMultipartBoundary();
        Map<String, String> parameters = new LinkedHashMap<>(2);
        if (!isFilenameCharsetSet()) {
            parameters.put("charset", this.charset.name());
        }
        parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));

        contentType = new MediaType(contentType, parameters);
        outputMessage.getHeaders().setContentType(contentType);

        if (outputMessage instanceof StreamingHttpOutputMessage) {
            StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
            streamingOutputMessage.setBody(outputStream -> {
                writeParts(outputStream, parts, boundary);
                writeEnd(outputStream, boundary);
            });
        }
        else {
            writeParts(outputMessage.getBody(), parts, boundary);
            writeEnd(outputMessage.getBody(), boundary);
        }
    }

    private void writeMapParts(OutputStream os, Map<String, Object> parts, byte[] boundary) throws IOException {
        for (Map.Entry<String, Object> entry : parts.entrySet()) {
            String name = entry.getKey();
            String part = String.valueOf(entry.getValue());
            if (part != null) {
                writeBoundary(os, boundary);
                writePart(name, getHttpEntity(part), os);
                writeNewLine(os);
            }
        }
    }

    private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
        for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
            String name = entry.getKey();
            for (Object part : entry.getValue()) {
                if (part != null) {
                    writeBoundary(os, boundary);
                    writePart(name, getHttpEntity(part), os);
                    writeNewLine(os);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void writeMapPart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
        Object partBody = partEntity.getBody();
        if (partBody == null) {
            throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
        }
        Class<?> partType = partBody.getClass();
        HttpHeaders partHeaders = partEntity.getHeaders();
        MediaType partContentType = partHeaders.getContentType();
        for (HttpMessageConverter<?> messageConverter : this.partConverters) {
            if (messageConverter.canWrite(partType, partContentType)) {
                Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
                HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
                //multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
                if (!partHeaders.isEmpty()) {
                    multipartMessage.getHeaders().putAll(partHeaders);
                }
                ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
                return;
            }
        }
        throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
                "found for request type [" + partType.getName() + "]");
    }

    @SuppressWarnings("unchecked")
    private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
        Object partBody = partEntity.getBody();
        if (partBody == null) {
            throw new IllegalStateException("Empty body for part '" + name + "': " + partEntity);
        }
        Class<?> partType = partBody.getClass();
        HttpHeaders partHeaders = partEntity.getHeaders();
        MediaType partContentType = partHeaders.getContentType();
        for (HttpMessageConverter<?> messageConverter : this.partConverters) {
            if (messageConverter.canWrite(partType, partContentType)) {
                Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
                HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
               // multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
                if (!partHeaders.isEmpty()) {
                    multipartMessage.getHeaders().putAll(partHeaders);
                }
                ((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
                return;
            }
        }
        throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
                "found for request type [" + partType.getName() + "]");
    }

    /**
     * Generate a multipart boundary.
     * <p>This implementation delegates to
     * {@link MimeTypeUtils#generateMultipartBoundary()}.
     */
    protected byte[] generateMultipartBoundary() {
        return MimeTypeUtils.generateMultipartBoundary();
    }

    /**
     * Return an {@link HttpEntity} for the given part Object.
     * @param part the part to return an {@link HttpEntity} for
     * @return the part Object itself it is an {@link HttpEntity},
     * or a newly built {@link HttpEntity} wrapper for that part
     */
    protected HttpEntity<?> getHttpEntity(Object part) {
        return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<>(part));
    }



    private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        writeNewLine(os);
    }

    private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
        os.write('-');
        os.write('-');
        os.write(boundary);
        os.write('-');
        os.write('-');
        writeNewLine(os);
    }

    private static void writeNewLine(OutputStream os) throws IOException {
        os.write('\r');
        os.write('\n');
    }


    /**
     * Implementation of {@link org.springframework.http.HttpOutputMessage} used
     * to write a MIME multipart.
     */
    private static class MultipartHttpOutputMessage implements HttpOutputMessage {

        private final OutputStream outputStream;

        private final Charset charset;

        private final HttpHeaders headers = new HttpHeaders();

        private boolean headersWritten = false;

        public MultipartHttpOutputMessage(OutputStream outputStream, Charset charset) {
            this.outputStream = outputStream;
            this.charset = charset;
        }

        @Override
        public HttpHeaders getHeaders() {
            return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
        }

        @Override
        public OutputStream getBody() throws IOException {
            writeHeaders();
            return this.outputStream;
        }

        private void writeHeaders() throws IOException {
            if (!this.headersWritten) {
                for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
                    byte[] headerName = getBytes(entry.getKey());
                    for (String headerValueString : entry.getValue()) {
                        byte[] headerValue = getBytes(headerValueString);
                        this.outputStream.write(headerName);
                        this.outputStream.write(':');
                        this.outputStream.write(' ');
                        this.outputStream.write(headerValue);
                        writeNewLine(this.outputStream);
                    }
                }
                writeNewLine(this.outputStream);
                this.headersWritten = true;
            }
        }

        private byte[] getBytes(String name) {
            return name.getBytes(this.charset);
        }
    }

最后使用配置类方式追加就行了

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //加载自定义消息参数转换器
        converters.add(new CustomHttpMessageConverter());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值