前言
上文中讲到了 Feign
结合 SpringBoot
注解的具体用法以及 CircuitBreaker Fallbacks
的用法。具体可阅览 Feign Spring Cloud CircuitBreaker Fallbacks 。今天博主要总结的是如何运用 Spring Cloud Feign 来实现文件上传下载功能。还是老规矩直接上案例代码。
Serveer
pom.xml
和 Commons
类同上 Feign Spring Cloud CircuitBreaker Fallbacks 中一样,这里就不再赘述了。
Controller
ServerUpDownloadFileController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 * @author vincent */ @RestController @Slf4j @RequestMapping(value = "/file") public class ServerUpDownloadFileController { private final static String PATH = "/Users/vincent/IDEA_Project/my_project/calling-third-party-api/src/main/java/com/vincent/callingthirdpartyapi/open_feign/spring_cloud_open_feign" ; @PostMapping(value = "/upload") public ResponseDto<String> upload (@RequestParam("file") MultipartFile file) throws IOException { log.info("Upload Method Params MultipartFile file: {}..." , file); String name = file.getOriginalFilename(); log.info("File name: {}..." , name); file.transferTo(Paths.get(PATH).resolve("upload_file.txt" )); return ResponseDto.success(name + " upload success..." ); } * Spring 提供的类:FileSystemResource * * @param fileName 文件名 * @param response 响应 * @return FileSystemResource * @throws UnsupportedEncodingException 异常 */ @GetMapping(value = "/download") public FileSystemResource download (@RequestParam("fileName") String fileName, HttpServletResponse response) throws UnsupportedEncodingException { log.info("Download Method Params String fileName: {}..." , fileName); Path filePath = Paths.get(PATH).resolve(fileName); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode("download_file.txt" , StandardCharsets.UTF_8.toString())); return new FileSystemResource(filePath); } @GetMapping(value = "/download2") public void download2 (@RequestParam("fileName") String fileName, HttpServletResponse response) throws Exception { log.info("Download2 Method Params String fileName: {}..." , fileName); Path filePath = Paths.get(PATH).resolve(fileName); response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode("fileName.txt" , "UTF-8" )); response.getOutputStream().write(Files.readAllBytes(filePath)); } }
Client
Utils
ClassUtils
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 * @author vincent */ public class ClassUtils { public static <T> Class<T> getGenericBySuperClass (Class<?> clazz) { Type genericSuperclass = clazz.getGenericSuperclass(); return getGenericType(genericSuperclass); } public static <T> Class<T> getGenericByInterface (Class<?> clazz) { * getGenericInterfaces: 获取父接口的泛型 * 例: 父接口 -> interface A <T > * 子实现类 -> class B implements A <T > * Class<B> clazz = B.class * clazz.getGenericInterfaces(): 获取 A 接口的泛型(因为接口是多实现的, 所以该方法返回的是 Type[] ) */ Type[] genericInterfaces = clazz.getGenericInterfaces(); if (ArrayUtils.isEmpty(genericInterfaces)) { return null ; } Type genericInterface = genericInterfaces[0 ]; return getGenericType(genericInterface); } @SuppressWarnings("unchecked") private static <T> Class<T> getGenericType (Type genericSuperclass) { if (!(genericSuperclass instanceof ParameterizedType)) { return null ; } ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); if (ArrayUtils.isEmpty(actualTypeArguments)) { return null ; } Type actualTypeArgument = actualTypeArguments[0 ]; if (!(actualTypeArgument instanceof Class)) { return null ; } return (Class<T>) actualTypeArgument; } }
DefaultFallbackFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 * @author vincent * 熔断器工厂 */ public interface DefaultFallbackFactory <T > extends FallbackFactory <T > { Map<Class<?>, Function<Throwable, Object>> wrapperException(); static ResponseDto<?> simpleFailResponseDto(Throwable cause) { ResponseDto<?> responseDto = new ResponseDto<>(); responseDto.setStatus(-100 ); responseDto.setMsg(cause.getClass().getName() + ": detailMessage[ " + Optional.ofNullable(cause.getMessage()).orElse("" ) + " ]" ); return responseDto; } @Override default T create (Throwable cause) { return simpleFailClient(cause); } @SuppressWarnings("unchecked") default T simpleFailClient (Throwable cause) { Class<Object> clazz = ClassUtils.getGenericByInterface(this .getClass()); Map<Class<?>, Function<Throwable, Object>> map = Optional.ofNullable(wrapperException()).orElse(Collections.emptyMap()); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(clazz); enhancer.setCallback((InvocationHandler) (o, method, objects) -> { Class<?> returnType = method.getReturnType(); if (map.containsKey(returnType)) { return map.get(returnType).apply(cause); } return new RuntimeException(cause); }); return (T) enhancer.create(); } }
Config
CircuitBreakerConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Slf4j @Configuration public class CircuitBreakerConfig { @Bean MyCircuitBreaker myCircuitBreaker () { return new MyCircuitBreaker(); } @SuppressWarnings("rawtypes") @Bean CircuitBreakerFactory circuitBreakerFactory (MyCircuitBreaker myCircuitBreaker) { return new CircuitBreakerFactory() { @Override public CircuitBreaker create (String id) { log.info("Creating a circuit breaker with id [" + id + "]" ); return myCircuitBreaker; } @Override protected ConfigBuilder configBuilder (String id) { return Object::new ; } @Override public void configureDefault (Function defaultConfiguration) { } }; } static class MyCircuitBreaker implements CircuitBreaker { AtomicBoolean runWasCalled = new AtomicBoolean(); @Override public <T> T run (Supplier<T> toRun) { try { this .runWasCalled.set(true ); return toRun.get(); } catch (Throwable throwable) { throw new NoFallbackAvailableException("No fallback available." , throwable); } } @Override public <T> T run (Supplier<T> toRun, Function<Throwable, T> fallback) { try { return run(toRun); } catch (Throwable throwable) { return fallback.apply(throwable); } } public void clear () { this .runWasCalled.set(false ); } } }
FeignConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Configuration public class FeignConfig { * doNotCloseAfterDecode(): 该构造方法的作用为,当响应返回的 response 进行解码器解析后,对其不进行关闭。 * 这里主要用于文件下载时,以流的形式成功返回后,防止流的关闭。 * 如果不设置该值,在调用第三方的下载接口时,会抛出 java.io.IOException: stream is closed 异常。 * * @return Feign.Builder */ @Bean public Feign.Builder doNotCloseAfterDecode () { return Feign.builder().doNotCloseAfterDecode(); } @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; * 编码器: 支持以下三种请求格式 * 1. application/json * 2. application/x-www-form-urlencoded * 3. multipart/form-data * * @return Encoder */ @Bean public Encoder feignFormEncoder () { return new SpringFormEncoder(new SpringEncoder(messageConverters)); } * 解码器: 支持两种响应格式 * 1. 返回值类型为 InputStream(主要用于文件下载) * 2. 其余返回值类型,以 json 数据格式进行解析 * * @return Decoder */ @Bean public Decoder feignFormDecoder () { return (response, type) -> { if (type == InputStream.class) { return response.body().asInputStream(); } return new JacksonDecoder().decode(response, type); }; } }
在里有一点需要注意:关于解码器 Decoder feignFormDecoder()
方法的配置,博主在这里是将 response
转换为流的形式进行相关处理,但并不是就这一种方式。大致可以分成三种方式:
InputStream
:转换为输入流,上述案例就是。
MultipartFile[]
:转换为 MultipartFile,官网 feign-form 给出了相关案例,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @FeignClient( name = "${feign.name}" , url = "${feign.url}" configuration = DownloadClient.ClientConfiguration.class ) public interface DownloadClient { @RequestMapping("/multipart/download/{fileId}") MultipartFile[] download(@PathVariable("fileId") String fileId); class ClientConfiguration { @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; @Bean public Decoder feignDecoder () { List<HttpMessageConverter<?>> springConverters = messageConverters.getObject().getConverters(); List<HttpMessageConverter<?>> decoderConverters = new ArrayList<HttpMessageConverter<?>>(springConverters.size() + 1 ); decoderConverters.addAll(springConverters); decoderConverters.add(new SpringManyMultipartFilesReader(4096 )); HttpMessageConverters httpMessageConverters = new HttpMessageConverters(decoderConverters); return new SpringDecoder(new ObjectFactory<HttpMessageConverters>() { @Override public HttpMessageConverters getObject () { return httpMessageConverters; } }); } } }
byte[]
:转换成 byte[] 来处理,这种案例网上有很多,可自行搜索。在 Spring Cloud Netflix Issues 中,我看到了这样一个案例写法,如下:
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > io.github.openfeign.form</groupId > <artifactId > feign-form</artifactId > <version > 2.2.1</version > </dependency > <dependency > <groupId > io.github.openfeign.form</groupId > <artifactId > feign-form-spring</artifactId > <version > 2.2.1</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import org.springframework.util.FileCopyUtils;import org.springframework.web.multipart.MultipartFile;import java.io.*;public class InMemoryMultipartFile implements MultipartFile { private final String name; private final String originalFileName; private final String contentType; private final byte [] payload; public InMemoryMultipartFile (File file) throws IOException { this .originalFileName = file.getName(); this .payload = FileCopyUtils.copyToByteArray(file); this .name = "file" ; this .contentType = "application/octet-stream" ; } public InMemoryMultipartFile (String originalFileName, byte [] payload) { this .originalFileName = originalFileName; this .payload = payload; this .name = "file" ; this .contentType = "application/octet-stream" ; } public InMemoryMultipartFile (String name, String originalFileName, String contentType, byte [] payload) { if (payload == null ) { throw new IllegalArgumentException("Payload cannot be null." ); } this .name = name; this .originalFileName = originalFileName; this .contentType = contentType; this .payload = payload; } @Override public String getName () { return name; } @Override public String getOriginalFilename () { return originalFileName; } @Override public String getContentType () { return contentType; } @Override public boolean isEmpty () { return payload.length == 0 ; } @Override public long getSize () { return payload.length; } @Override public byte [] getBytes() throws IOException { return payload; } @Override public InputStream getInputStream () throws IOException { return new ByteArrayInputStream(payload); } @Override public void transferTo (File dest) throws IOException, IllegalStateException { new FileOutputStream(dest).write(payload); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @FeignClient(value = "material", configuration = MaterialClient.MultipartSupportConfig.class) public interface MaterialClient { @PostMapping("/uploadFile") @Headers("Content-Type: multipart/form-data") ResponseMO uploadFile (@RequestPart("file") MultipartFile file) ; @GetMapping("/oss/downFile") MultipartFile downFile (@RequestParam("key") String key) ; class MultipartSupportConfig { @Autowired ObjectFactory<HttpMessageConverters> messageConverters; @Bean @Primary @Scope("prototype") public Encoder multipartFormEncoder () { return new SpringFormEncoder(new SpringEncoder(messageConverters)); } @Bean @Primary @Scope("prototype") public Decoder decoder () { Decoder decoder = (response, type) -> { if (type instanceof Class && MultipartFile.class.isAssignableFrom((Class) type)) { Collection<String> contentTypes = response.headers().get("content-type" ); String contentType = "application/octet-stream" ; if (contentTypes.size() > 0 ) { String[] temp = new String[contentTypes.size()]; contentTypes.toArray(temp); contentType = temp[0 ]; } byte [] bytes = StreamUtils.copyToByteArray(response.body().asInputStream()); InMemoryMultipartFile inMemoryMultipartFile = new InMemoryMultipartFile("file" ,"" , contentType,bytes); return inMemoryMultipartFile; } return new SpringDecoder(messageConverters).decode(response, type); }; return new ResponseEntityDecoder(decoder); } } }
注意: 这段代码的解码器配置,是将 response
转换为 MultipartFile 来处理,但其主要是通过 byte[]
进行相关处理的,所以我把它归在 byte[]
处理方式这类中。那么这个案例是否正确,博主本人并没有尝试过(应该是可行的),也并不想探讨这个问题。而是想说,如果将输入流转换为 byte[]
来进行文件的处理,会存在一个很大的风险。那就是 byte[]
可接收的字节是存在上线的,转换如果超出了这个上线,必然会抛出异常,甚至可能会造成项目崩溃。所以博主在这里不建议这么用。当然你可以通过限制被下载文件的大小来解决这个问题。
这里还有一个案例 spring cloud feign file upload and file download :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 * @author lr */ @FeignClient(name = ClientUrl.SYSTEM_NAME, fallbackFactory = FileTestClientFallbackFactory.class) @Component public interface FileTestClient { * Upload file test * * @return */ @PostMapping(value = ClientUrl.PRE_REQUEST_RUL + "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) Object upload (MultipartFile file) ; * Download File Test */ @RequestMapping(value = ClientUrl.PRE_REQUEST_RUL + "/file/download", method = RequestMethod.GET) Response download () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 * @author lr */ @Slf4j @Component public class FileTestClientFallbackFactory implements FallbackFactory <FileTestClient > { @Override public FileTestClient create (Throwable cause) { return new FileTestClient() { @Override public Object upload (MultipartFile file) { log.error("fallback; file upload reason was: " + cause.getMessage()); return null ; } @Override public Response download () { log.error("fallback; file download reason was: " + cause.getMessage()); return null ; } }; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @RestController @Slf4j public class FileController { @Autowired FileTestClient fileTestClient; @Log("File upload test") @PostMapping("/upload") public Object upload (MultipartFile file) { log.info("Use feign Call service, file upload" ); return fileTestClient.upload(file); } @Log("File download test") @RequestMapping(value = "/download", method = RequestMethod.GET) public ResponseEntity<byte []> downFile() { log.info("Use feign Call service file download" ); ResponseEntity<byte []> result = null ; InputStream inputStream = null ; try { Response response = fileTestClient.download(); Response.Body body = response.body(); inputStream = body.asInputStream(); byte [] b = new byte [inputStream.available()]; inputStream.read(b); HttpHeaders heads = new HttpHeaders(); heads.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=lr.xls" ); heads.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); result = new ResponseEntity<byte []>(b, heads, HttpStatus.OK); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null ) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return result; } }
这个案例的下载,是直接把 response
整体都进行了返回,然后再通过 byte[]
进行处理。
1 2 3 4 Response response = fileTestClient.download(); Response.Body body = response.body(); inputStream = body.asInputStream(); byte [] b = new byte [inputStream.available()];
所以我也把它归在 byte[]
处理方式这一类中。该案例也同样有上述所说的问题 byte[] b = new byte[inputStream.available()];
,当 inputStream.available()
超出 byte[]
容纳上线时就会出现问题。
ApiClient
UpDownloadFileApiClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 * @author vincent */ @FeignClient(name = "UpDownloadFileApiClient", url = "http://localhost:8080/file", configuration = FeignConfig.class, fallbackFactory = UpDownloadFileApiClient.UpDownloadFileApiClientFactory.class ) public interface UpDownloadFileApiClient { @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) ResponseDto<String> upload (@RequestPart("file") MultipartFile file) ; @GetMapping(value = "/download") InputStream download (@RequestParam("fileName") String fileName) ; @GetMapping(value = "/download2") InputStream download2 (@RequestParam("fileName") String fileName) ; @Component class UpDownloadFileApiClientFactory implements DefaultFallbackFactory <UpDownloadFileApiClient > { private static final Map<Class<?>, Function<Throwable, Object>> WRAPPER_EXCEPTION = ImmutableMap.of( InputStream.class, e -> { throw new RuntimeException(e); }, ResponseDto.class, DefaultFallbackFactory::simpleFailResponseDto ); @Override public Map<Class<?>, Function<Throwable, Object>> wrapperException() { return WRAPPER_EXCEPTION; } } }
Test
CloudFeignUpDownloadFileTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @SpringBootTest(classes = CallingThirdPartyApiApplicationTests.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, value = {"feign.circuitbreaker.enabled=true" } ) @Slf4j public class CloudFeignUpDownloadFileTest { private final static String PATH = "/Users/vincent/IDEA_Project/my_project/calling-third-party-api/src/test/java/com/vincent/callingthirdpartyapi/open_feign/spring_cloud_open_feign" ; @Autowired private UpDownloadFileApiClient upDownloadFileApiClient; @Test public void uploadTest () throws IOException { String name = "upload_test_file.txt" ; Path filePath = Paths.get(PATH).resolve(name); String originalFileName = "upload_test_file.txt" ; String contentType = "application/octet-stream" ; byte [] content = Files.readAllBytes(filePath); MultipartFile multipartFile = new MockMultipartFile(name, originalFileName, contentType, content); ResponseDto<String> responseDto = upDownloadFileApiClient.upload(multipartFile); log.info(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(responseDto)); } @Test public void downloadTest () throws IOException { Path path = Paths.get(PATH); InputStream inputStream = upDownloadFileApiClient.download("upload_file.txt" ); IOUtils.copy(inputStream, Files.newOutputStream(path.resolve("down_file.txt" ))); InputStream inputStream2 = upDownloadFileApiClient.download("upload_file.txt" ); IOUtils.copy(inputStream2, Files.newOutputStream(path.resolve("down_file2.txt" ))); } }
注意: 启动 CloudFeignUpDownloadFileTest
别忘了先创建一个 upload_test_file.txt
文本文件。
How to Download a File from a URL in Java
其实文件的下载还有更简单的方法,并不一定要用 Feign Spring Cloud
。我在这里简单的举个例子:
1 2 InputStream inputStream = new URL("http://example.com/my-file-path.txt" ).openStream(); IOUtils.copy(inpuStream, Files.newOutputStream(Paths.get("/Users/username/Documents" ).resolve("file_name.txt" )));
搞定,是不是很简单。更多方法可以阅读 How to Download a File from a URL in Java 。
Reference Resources:https://github.com/OpenFeign/feign-form
Reference Resources:https://github.com/spring-cloud/spring-cloud-netflix/issues/2246
Reference Resources:https://programming.vip/docs/spring-cloud-feign-file-upload-and-file-download.html
Case Source Code:https://github.com/V-Vincen/calling-third-party-api
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !