spring cloud feign support file upload

phenomenon

Spring Cloud Feign component not support file upload between micron service.
it may throw exception like this:

1
2
3
4
feign.codec.EncodeException: class [Lorg.springframework.web.multipart.MultipartFile; is not a type supported by this encoder.
at feign.codec.Encoder$Default.encode(Encoder.java:90) ~[feign-core-9.5.1.jar:na]
at feign.form.FormEncoder.encode(FormEncoder.java:87) ~[feign-form-3.3.0.jar:3.3.0]
at feign.form.spring.SpringFormEncoder.encode(SpringFormEncoder.java:64) ~[feign-form-spring-3.3.0.jar:3.3.0]

add dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>

https://mvnrepository.com/artifact/io.github.openfeign.form/feign-form-spring

be careful the feign-form-spring depend on spring-web it may have conflict with your project.
for example in spring-boot:2.0.5.RELEASE spring-cloud:Finchley.SR1 project has already import
spring-web:5.0.9.RELEASE, if import feign-form-spring:3.3.0 it will import spring-web:4.3.18.RELEASE too,
it cause spring bean creation exception.

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.3.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>

file receive part

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ApiOperation("file upload receive part")
@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<XXX> uploadFile(
@RequestPart(value = "files")
MultipartFile[] files,
@RequestParam(name = "userId")
@ApiParam(name = "userId", value = "用户ID", required = true)
@NotNull(message = "用户ID不能为空")
Long userId,
@RequestParam(name = "userName")
@ApiParam(name = "userName", value = "用户名称", required = true)
@NotBlank(message = "用户名不能为空")
String userName){
...
}

be careful consumes property must add to the annotation RequestMapping
method property must be POST, RequestPart is working.

feign client part

client part is a little complex.

add configuration class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class MultipartSupportConfig {

@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
@Primary
@Scope("prototype")
public Encoder feignEncoder() {
// return new SpringFormEncoder();
// return new SpringFormEncoder(new SpringEncoder(messageConverters));
return new FeignSpringFormEncoder(new SpringEncoder(messageConverters));
}

@Bean
public feign.Logger.Level multipartLoggerLevel() {
return feign.Logger.Level.FULL;
}

}
  • SpringFormEncoder support single file upload.
  • FeignSpringFormEncoder support file array upload.
  • SpringEncoder avoid project make RequestBody encoding error.

add form encoder

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
public class FeignSpringFormEncoder extends FormEncoder {
/**
* Constructor with the default Feign's encoder as a delegate.
*/
public FeignSpringFormEncoder() {
this(new Default());
}

/**
* Constructor with specified delegate encoder.
*
* @param delegate delegate encoder, if this encoder couldn't encode object.
*/
public FeignSpringFormEncoder(Encoder delegate) {
super(delegate);
MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(ContentType.MULTIPART);
processor.addWriter(new SpringSingleMultipartFileWriter());
processor.addWriter(new SpringManyMultipartFilesWriter());
}

@Override
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
if (bodyType.equals(MultipartFile.class)) {
MultipartFile file = (MultipartFile) object;
Map data = Collections.singletonMap(file.getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
return;
} else if (bodyType.equals(MultipartFile[].class)) {
MultipartFile[] file = (MultipartFile[]) object;
if(file != null) {
Map data = Collections.singletonMap(file.length == 0 ? "" : file[0].getName(), object);
super.encode(data, MAP_STRING_WILDCARD, template);
return;
}
}
super.encode(object, bodyType, template);
}
}

add interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FeignClient(name = "upload-example", configuration = MultipartSupportConfig.class)
public interface CloudUploadService {

@ApiOperation("文件上传")
@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
List<XXX> uploadFile(
@RequestPart(value = "files")
MultipartFile[] files,
@RequestParam(name = "userId")
@ApiParam(name = "userId", value = "用户ID", required = true)
@NotNull(message = "用户ID不能为空")
Long userId,
@RequestParam(name = "userName")
@ApiParam(name = "userName", value = "用户名称", required = true)
@NotBlank(message = "用户名不能为空")
String userName);

}

in annotation FeignClient add configuration property, point to the encoding support class.

Testing

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
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class UploadTester {

@Autowired
private CloudUploadService uploadService;

@Test
@SneakyThrows
public void testHandleFileUpload() {

File file = new File("upload.txt");
DiskFileItem fileItem = (DiskFileItem) new DiskFileItemFactory().createItem("file",
MediaType.TEXT_PLAIN_VALUE, true, file.getName());

try (InputStream input = new FileInputStream(file); OutputStream os = fileItem.getOutputStream()) {
IOUtils.copy(input, os);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid file: " + e, e);
}

MultipartFile multi = new CommonsMultipartFile(fileItem);

log.info(uploadService.handleFileUpload(new MultipartFile[]{multi}, 1L, "tester"));
}

}

https://github.com/OpenFeign/feign-form
https://www.jianshu.com/p/4f4d9d084b1d
http://blog.didispace.com/spring-cloud-starter-dalston-2-4/
https://blog.csdn.net/gududedabai/article/details/79895893