前因

最近在自己的开源项目里集成了Minio这个分布式文件存储系统,需求支撑微服务内部的文件上传,例如在A服务中生成一个文件,然后把文件上传到文件存储系统。

一开始以为是个很简单的问题,然后一顿代码编写后,启动、调用,发现接口报错,上网搜索一下发现是openFeign的默许编码器不支撑传输文件,那么我们就要自己去重写编码器,让他能支撑文件传输。


解决方案

1、编写解码器

package com.maochd.cloud.api.file.config;
import feign.RequestTemplate;  
import feign.codec.EncodeException;  
import feign.codec.Encoder;  
import lombok.SneakyThrows;  
import org.springframework.core.io.InputStreamResource;  
import org.springframework.core.io.Resource;  
import org.springframework.http.HttpEntity;  
import org.springframework.http.HttpHeaders;  
import org.springframework.http.HttpOutputMessage;  
import org.springframework.http.MediaType;  
import org.springframework.http.converter.HttpMessageConverter;  
import org.springframework.util.LinkedMultiValueMap;  
import org.springframework.web.client.RestTemplate;  
import org.springframework.web.multipart.MultipartFile;  
import reactor.util.annotation.NonNull;  
import java.io.ByteArrayOutputStream;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.OutputStream;  
import java.lang.reflect.Type;  
import java.nio.charset.Charset;  
import java.nio.charset.StandardCharsets;  
import java.util.Arrays;  
import java.util.List;  
import java.util.Map;  
import java.util.Map.Entry;  
public class FeignSpringFormEncoder implements Encoder {  
    private final List<HttpMessageConverter<?>> converters = new RestTemplate().getMessageConverters();  
    public static final Charset UTF_8 = StandardCharsets.UTF_8;  
    public FeignSpringFormEncoder() {  
    }  
    /**  
    * 实现一个 HttpOutputMessage  
    */  
    private static class HttpOutputMessageImpl implements HttpOutputMessage {  
        /**  
        * 输出流,恳求体  
        */  
        private final OutputStream body;  
        /**  
        * 恳求头  
        */  
        private final HttpHeaders headers;  
        public HttpOutputMessageImpl(OutputStream body, HttpHeaders headers) {  
            this.body = body;  
            this.headers = headers;  
        }  
        @Override  
        @NonNull  
        @SneakyThrows  
        public OutputStream getBody() {  
            return body;  
        }  
        @Override  
        @NonNull  
        public HttpHeaders getHeaders() {  
            return headers;  
        }  
    }  
    /**  
    * 判别是否表单恳求  
    *  
    * @param type 类型  
    * @return bool  
    */  
    static boolean isFormRequest(Type type) {  
        return MAP_STRING_WILDCARD.equals(type);  
    }  
    /**  
    * 内部静态类,保存 MultipartFile 数据  
    */  
    static class MultipartFileResource extends InputStreamResource {  
        /**  
        * 文件名  
        */  
        private final String filename;  
        /**  
        * 文件巨细  
        */  
        private final long size;  
        /**  
        * 结构方法  
        *  
        * @param inputStream 输入流  
        * @param filename 文件名称  
        * @param size 文件巨细  
        */  
        public MultipartFileResource(InputStream inputStream, String filename, long size) {  
            super(inputStream);  
            this.filename = filename;  
            this.size = size;  
        }  
        @Override  
        public String getFilename() {  
            return this.filename;  
        }  
        @Override  
        @NonNull  
        @SneakyThrows  
        public InputStream getInputStream() {  
            return super.getInputStream();  
        }  
        @Override  
        @SneakyThrows  
        public long contentLength() {  
            return size;  
        }  
    }  
    /**  
    * 重写编码器  
    *  
    * @param object 目标  
    * @param bodyType 传输内容类型  
    * @param template 恳求模板  
    */  
    @Override  
    @SneakyThrows  
    public void encode(Object object, Type bodyType, RequestTemplate template) {  
        if (isFormRequest(bodyType)) {  
            final HttpHeaders multipartHeaders = new HttpHeaders();  
            multipartHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);  
            encodeMultipartFormRequest((Map<Object, ?>) object, multipartHeaders, template);  
        } else {  
            final HttpHeaders jsonHeaders = new HttpHeaders();  
            jsonHeaders.setContentType(MediaType.APPLICATION_JSON);  
            encodeRequest(object, jsonHeaders, template);  
        }  
    }  
    /**  
    * 对有文件、表单的进行编码  
    *  
    * @param formMap 表单目标  
    * @param multipartHeaders 文件恳求头  
    * @param template 恳求模板  
    */  
    private void encodeMultipartFormRequest(Map<Object, ?> formMap, HttpHeaders multipartHeaders, RequestTemplate template) {  
        if (formMap == null) {  
            throw new EncodeException("无法对格式为null的恳求进行编码。");  
        }  
        LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();  
        //对每个参数进行检查校验  
        for (Entry<Object, ?> entry : formMap.entrySet()) {  
            Object value = entry.getValue();  
            //不同的数据类型进行不同的编码逻辑处理  
            if (isMultipartFile(value)) {  
                //单个文件  
                map.add(entry.getKey(), encodeMultipartFile((MultipartFile) value));  
            } else if (isMultipartFileArray(value)) {  
                //多个文件  
                encodeMultipartFiles(map, (String) entry.getKey(), Arrays.asList((MultipartFile[]) value));  
            } else {  
                //一般恳求数据  
                map.add(entry.getKey(), encodeJsonObject(value));  
            }  
        }  
        encodeRequest(map, multipartHeaders, template);  
    }  
    /**  
    * 对恳求进行编码  
    *  
    * @param value 参数  
    * @param requestHeaders 恳求头  
    * @param template 恳求模板  
    */  
    private void encodeRequest(Object value, HttpHeaders requestHeaders, RequestTemplate template) {  
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();  
        HttpOutputMessage dummyRequest = new HttpOutputMessageImpl(outputStream, requestHeaders);  
        try {  
            Class<?> requestType = value.getClass();  
            MediaType requestContentType = requestHeaders.getContentType();  
            for (HttpMessageConverter<?> messageConverter : converters) {  
                if (messageConverter.canWrite(requestType, requestContentType)) {  
                    ((HttpMessageConverter<Object>) messageConverter).write(value, requestContentType, dummyRequest);  
                    break;  
                }  
            }  
        } catch (IOException e) {  
            throw new EncodeException("无法对恳求进行编码:", e);  
        }  
        HttpHeaders headers = dummyRequest.getHeaders();  
        for (Entry<String, List<String>> entry : headers.entrySet()) {  
            template.header(entry.getKey(), entry.getValue());  
        }  
        /*  
        请运用模板输出流。。。如果文件太大,这将导致问题,因为整个恳求都将在内存中。  
        */  
        template.body(outputStream.toByteArray(), UTF_8);  
    }  
    /**  
    * 编码为json目标  
    *  
    * @param obj 参数  
    * @return entity  
    */  
    private HttpEntity<?> encodeJsonObject(Object obj) {  
        HttpHeaders jsonPartHeaders = new HttpHeaders();  
        jsonPartHeaders.setContentType(MediaType.APPLICATION_JSON);  
        return new HttpEntity<>(obj, jsonPartHeaders);  
    }  
    /**  
    * 编码MultipartFile文件,将其转换为HttpEntity,一起设置 Content-type 为 application/octet-stream  
    *  
    * @param map 当前恳求 map.  
    * @param name 数组字段的名称  
    * @param fileList 要处理的文件  
    */  
    private void encodeMultipartFiles(LinkedMultiValueMap<Object, Object> map,  
        String name, List<? extends MultipartFile> fileList) {  
        HttpHeaders filePartHeaders = new HttpHeaders();  
        // 设置 Content-type  
        filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);  
        try {  
            for (MultipartFile file : fileList) {  
                Resource multipartFileResource =  
                    new MultipartFileResource(file.getInputStream(), file.getOriginalFilename(), file.getSize());  
                map.add(name, new HttpEntity<>(multipartFileResource, filePartHeaders));  
            }  
        } catch (IOException e) {  
            throw new EncodeException("无法对恳求进行编码:", e);  
        }  
    }  
    /**  
    * 编码MultipartFile文件,将其转换为HttpEntity,一起设置 Content-type 为 application/octet-stream  
    *  
    * @param file 要编码的文件  
    * @return entity  
    */  
    private HttpEntity<?> encodeMultipartFile(MultipartFile file) {  
        HttpHeaders filePartHeaders = new HttpHeaders();  
        // 设置 Content-type  
        filePartHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);  
        try {  
            Resource multipartFileResource =  
                new MultipartFileResource(file.getInputStream(), file.getOriginalFilename(), file.getSize());  
            return new HttpEntity<>(multipartFileResource, filePartHeaders);  
        } catch (IOException e) {  
            throw new EncodeException("无法对恳求进行编码:", e);  
        }  
    }  
    /**  
    * 判别是否多个 MultipartFile  
    *  
    * @param object 参数  
    * @return bool  
    */  
    private boolean isMultipartFileArray(Object object) {  
        return object != null  
                && object.getClass().isArray()  
                && MultipartFile.class.isAssignableFrom(object.getClass().getComponentType());  
    }  
    /**  
    * 判别是否MultipartFile文件  
    *  
    * @param object 要判别的目标  
    * @return bool  
    */  
    private boolean isMultipartFile(Object object) {  
        return object instanceof MultipartFile;  
    }  
}

2、编写Config文件

/**
* 这儿看情况运用@Configuration注解,
* 当你需求全局配置的时分,运用@Configuration
* 当你只想应用于当前FeignClient的时分,就不需求运用@Configuration
*/
// @Configuration
public class MultipartSupportConfig {  
    @Bean  
    public Encoder feignFormEncoder() {  
        return new FeignSpringFormEncoder();  
    }  
}

3、把配置文件放入指定的FeignClient中

package com.maochd.cloud.api.file.service;
import com.maochd.cloud.api.file.config.MultipartSupportConfig;  
import com.maochd.cloud.api.file.fallback.RemoteFileFallbackFactory;  
import com.maochd.cloud.common.core.constant.ServiceContextConstant;  
import com.maochd.cloud.common.core.constant.ServiceNameConstant;  
import com.maochd.cloud.common.core.domain.R;  
import io.swagger.annotations.ApiOperation;  
import org.springframework.cloud.openfeign.FeignClient;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RequestPart;  
import org.springframework.web.multipart.MultipartFile;  
/**  
* 这边供给了一个我自己项目的FeignClient  
* 只要在@FeignClient这个注解的configuration特点中添加config文件,就可以支撑文件传输  
* 这边用单文件的目的是为了接口的合理运用,保证内部服务文件上传的接口只有一个,单向数据流通,保证事务的清晰明了  
* 注意事项:文件上传必须用@RequestPart,不能用@RequestParam,亲测!!!  
*/
@FeignClient(contextId = ServiceContextConstant.FILE_CONTEXT_ID,  
value = ServiceNameConstant.FILE_SERVER,  
fallbackFactory = RemoteFileFallbackFactory.class, 
configuration = MultipartSupportConfig.class)  
public interface RemoteFileService {  
    @PostMapping("/upload/inner")  
    @ApiOperation(value = "内部上传(feign)", notes = "内部上传(feign)")  
    R<String> uploadInner(@RequestPart("file") MultipartFile file,  
    @RequestParam(value = "filename", required = false) String filename);  
}

最后

我没有供给专门的demo,只是从我的微服务项目里摘取了其中的一段,我们运用的时分,需求依据自己的项目进行改造,也可以参考我的微服务项目maochd-cloud

有不懂的同学可以私信我,谈论,看到了会逐个答复的。