This is the 10th day of my participation in Gwen Challenge
This article is participating in the “Java Theme Month – Java Development Practice”, for more details: activity link
Accumulate over a long period, constant dripping wears away a stone 😄
I recently encountered a requirement to transfer files through Feign. I thought it was simple, but I ran into a lot of problems. Let’s follow the author to see!
Import dependence
<! -- Boot version 2.2.2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Copy the code
This module adds support for encoding Application/X-www-form-urlencoded and multipart/form-data forms.
The next step is coding.
The first edition
A service
- A service
Controller
@Autowired
PayFeign payFeign;
@PostMapping("/uploadFile")
public void upload(@RequestParam MultipartFile multipartFile,String title){
payFeign.uploadFile(multipartFile,title);
}
Copy the code
- A Service
Feign
@FeignClient(name = "appPay")
public interface PayFeign {
@PostMapping(value="/api/pay/uploadFile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadFile(@RequestPart("multipartFile111") MultipartFile multipartFile,
@RequestParam("title") String title);
}
Copy the code
Note:
Feign
Specified in theconsumes
The format forMULTIPART_FORM_DATA_VALUE
To specify the submission type of the request.- File types can be annotated
@RequestPart
, you don’t have to write it, but you can’t use itRequestParam
Annotation.
B service
@PostMapping(value="/uploadFile")
void uploadFile(@RequestParam("multipartFile") MultipartFile multipartFile,
@RequestParam("title") String title){
System.out.println(multipartFile.getOriginalFilename() + "= = = = =" + title);
}
Copy the code
Note: A service, B serviceController
In theMultipartFile
Must have the same name. As for the “A” serviceFeign
You can use any name you like, but try to keep it the same.
I can write it this way. It is possible to transfer files through Feign. But later requirements changed. The way the B service accepts parameters has been changed to use an entity class to accept parameters. If there are four or five or more parameters, the code will become less readable.
The second edition
B service
Add interface uploadFile2 as follows:
@PostMapping(value="/uploadFile2")
void uploadFile(FileInfoDTO fileInfo){
System.out.println(fileInfo.getMultipartFile().getOriginalFilename()
+ "= = = = =" + fileInfo.getTitle());
}
Copy the code
The input parameter is received using FileInfoDTO. FileInfoDTO contains the following contents:
public class FileInfoDTO {
private MultipartFile multipartFile;
private String title;
Get/set / / ignore
}
Copy the code
A service
A service request also changes as follows:
- Controller of service A
@PostMapping("/uploadFile2")
public void upload(FileInfo fileInfo){
payFeign.uploadFile2(fileInfo);
}
Copy the code
- A service of Feign
@PostMapping(value="/api/pay/uploadFile2", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadFile2(FileInfoDTO fileInfo);
Copy the code
Request A service interface via Postman:
The following exception occurs:
feign.codec.EncodeException: Could not write request: no suitable
HttpMessageConverter found for request type [com.gongj.appuser.dto.FileInfoDTO]
and content type [multipart/form-data]
Copy the code
Next is the source code analysis!!
We can conclude from the stack information printed on the console:
It’s an exception in the Encode method of the SpringEncoder class! Let’s take a look at the contents of this method:
@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request)
throws EncodeException {
// template.body(conversionService.convert(object, String.class));
// The request body information is not null
if(requestBody ! =null) {
// Get the requested ClassClass<? > requestType = requestBody.getClass();// Gets Content-type, which is the consumes we specify
Collection<String> contentTypes = request.headers()
.get(HttpEncoding.CONTENT_TYPE);
MediaType requestContentType = null;
if(contentTypes ! =null && !contentTypes.isEmpty()) {
String type = contentTypes.iterator().next();
requestContentType = MediaType.valueOf(type);
}
// The type of the subject is not null and is MultipartFile
if(bodyType ! =null && bodyType.equals(MultipartFile.class)) {
/ / the content-type for multipart/form - the data
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
// Call the encode method of SpringFormEncoder
this.springFormEncoder.encode(requestBody, bodyType, request);
return;
}else {
// If the main Type is MultipartFile, but the content-type is not multipart/form-data
// Throws an exception
String message = "Content-Type \"" + MediaType.MULTIPART_FORM_DATA
+ "\" not set for request body of type "
+ requestBody.getClass().getSimpleName();
throw newEncodeException(message); }}// We requested to enter here and convert
// If the body type is not MultipartFile, type conversion is performed via HttpMessageConverter
for(HttpMessageConverter<? > messageConverter :this.messageConverters
.getObject().getConverters()) {
if (messageConverter.canWrite(requestType, requestContentType)) {
if (log.isDebugEnabled()) {
if(requestContentType ! =null) {
log.debug("Writing [" + requestBody + "] as \""
+ requestContentType + "\" using [" + messageConverter
+ "]");
}
else {
log.debug("Writing [" + requestBody + "] using ["
+ messageConverter + "]");
}
}
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
try {
@SuppressWarnings("unchecked")
HttpMessageConverter<Object> copy = (HttpMessageConverter<Object>) messageConverter;
copy.write(requestBody, requestContentType, outputMessage);
}
catch (IOException ex) {
throw new EncodeException("Error converting request body", ex);
}
// clear headers
request.headers(null);
// converters can modify headers, so update the request
// with the modified headers
request.headers(getHeaders(outputMessage.getHeaders()));
// do not use charset for binary data and protobuf
Charset charset;
if (messageConverter instanceof ByteArrayHttpMessageConverter) {
charset = null;
}
else if (messageConverter instanceof ProtobufHttpMessageConverter
&& ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith(
outputMessage.getHeaders().getContentType())) {
charset = null;
}
else {
charset = StandardCharsets.UTF_8;
}
request.body(Request.Body.encoded(
outputMessage.getOutputStream().toByteArray(), charset));
return; }}// No conversion succeeds in throwing an exception our way of writing is to enter here
String message = "Could not write request: no suitable HttpMessageConverter "
+ "found for request type [" + requestType.getName() + "]";
if(requestContentType ! =null) {
message += " and content type [" + requestContentType + "]";
}
throw newEncodeException(message); }}Copy the code
This method takes three arguments as follows:
- RequestBody: The body information of the request
- BodyType: The type of the body
- Request: Information about a request, such as the address, mode, and code
Let’s first determine the question, why does it go inSpringEncoder
In the classencode
Method?Encoder
There are several implementations. Where does it specify the implementation class?
SpringEncoder (Default) : SpringEncoder (Default) : SpringEncoder (Default)
Specific logic in FeignClientsConfiguration class, provides an Encoder Bean
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(a) {
return new SpringEncoder(this.messageConverters);
}
Copy the code
Here are the meanings of the two notes:
-
ConditionalOnMissingClass: a class in the class path does not exist, the current Bean is instantiated.
-
ConditionalOnMissingBean: Instantiates the current bean when the given one does not exist
So where does that get assigned? Again in Feign’s abstract class
There is a static inner class Builder, which has an Encoder method inside it.
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
Copy the code
The encoder method is called in two places. Here’s the first call point, and the second is to read the value of the configuration file, so ignore that for a second, there’s no configuration here.
callget
Methods. And then there’s the basisEncoder
Type toSpring
Look for beans in. The value you get is atFeignClientsConfiguration
In. Talk so much, then how to solve! sinceSpringEncoder
Can’t solve our situation, so let’s move onEncoder
It’ll be ok.
@Configuration
public class FeignConfig {
// @Bean
// public Encoder multipartFormEncoder() {
// return new SpringFormEncoder();
/ /}
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignFormEncoder(a) {
return new SpringFormEncoder(newSpringEncoder(messageConverters)); }}Copy the code
Both methods are available, one with arguments specified as SpringEncoder and one with no arguments specified as new encoder.default (). Provide a FeignConfig configuration class that provides an EncoderBean, but its concrete implementation is SpringFormEncoder. Now that we provide an EncoderBean, SpringBoot will use what we configured, and the logic will go into the Encoder method of SpringFormEncoder.
Let’s look at the specific source code:
@Override
public void encode (Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
// The body type is MultipartFile array
if (bodyType.equals(MultipartFile[].class)) {
val files = (MultipartFile[]) object;
val data = new HashMap<String, Object>(files.length, 1.F);
for (val file : files) {
// file.getName() Gets the property name
data.put(file.getName(), file);
}
// Call the parent method
super.encode(data, MAP_STRING_WILDCARD, template);
} else if (bodyType.equals(MultipartFile.class)) {
// The subject type is MultipartFile
val file = (MultipartFile) object;
val data = singletonMap(file.getName(), object);
// Call the parent method
super.encode(data, MAP_STRING_WILDCARD, template);
} else if (isMultipartFileCollection(object)) {
// The subject type is the MultipartFile collectionval iterable = (Iterable<? >) object; val data =new HashMap<String, Object>();
for (val item : iterable) {
val file = (MultipartFile) item;
// file.getName() Gets the property name
data.put(file.getName(), file);
}
// Call the parent method
super.encode(data, MAP_STRING_WILDCARD, template);
} else {
// The other types still call the superclass method
super.encode(object, bodyType, template); }}Copy the code
As you can see, there are many supported formats, but in fact, they are all called encode method of the parent class, but the parameters are different. Now let’s look at the code for the parent class FormEncoder,
public void encode (Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
/ / the value of the content-type
String contentTypeValue = getContentTypeValue(template.headers());
// Perform a conversion such as multipart/form-data is converted to multipart
val contentType = ContentType.of(contentTypeValue);
if(! processors.containsKey(contentType)) { delegate.encode(object, bodyType, template);return;
}
Map<String, Object> data;
// Check whether the bodyType is Map
if (MAP_STRING_WILDCARD.equals(bodyType)) {
data = (Map<String, Object>) object;
} else if (isUserPojo(bodyType)) {
// Check whether the bodyType name is class Java. If not, convert the class object to Map
// This is the case with us.
data = toMap(object);
} else {
delegate.encode(object, bodyType, template);
return;
}
val charset = getCharset(contentTypeValue);
// Go through different processes according to different contentTypes
processors.get(contentType).process(template, charset, data);
}
Copy the code
Processors is a Map that has two values, respectively
-
MULTIPART: MultipartFormContentProcessor.
-
URLENCODED: UrlencodedFormContentProcessor
The above approach solved our problem, and both version 1 and version 2 requests are supported.
How many ways can I write it?
Can you pass more than one?
No, the key of the assembled Map is the attribute name, and even if you pass multiple files, the last file will take precedence.
You might wonder if you can write it this way, passing multiple MultipartFile objects.
@PostMapping(value="/api/pay/uploadFile5", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadFile5(@RequestPart("multipartFile")MultipartFile multipartFile,
@RequestPart("multipartFile2")MultipartFile multipartFile2);
Copy the code
Method has too many Body parameters.
Other situations
There is also A case where the file is generated internally by the A service rather than being passed in externally. The generated File type is File, not MultipartFile, so we need to convert it.
Join the rely on
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
Copy the code
Write utility classes
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
public class FileUtil {
public static MultipartFile fileToMultipartFile(File file) {
// The key name needs to be the same as the name of the MultipartFil you are interconnecting with
String fieldName = "multipartFile";
FileItemFactory factory = new DiskFileItemFactory(16.null);
FileItem item = factory.createItem(fieldName, "multipart/form-data".true,
file.getName());
int bytesRead = 0;
byte[] buffer = new byte[8192];
try {
FileInputStream fis = new FileInputStream(file);
OutputStream os = item.getOutputStream();
while ((bytesRead = fis.read(buffer, 0.8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return newCommonsMultipartFile(item); }}Copy the code
A Service addition method
@PostMapping("/uploadFile5")
public void upload5(a){
File file = new File("D:\\gongj\\log\\product-2021-05-12.log");
MultipartFile multipartFile = FileUtil.fileToMultipartFile(file);
payFeign.uploadFile(multipartFile,"Upload file");
}
Copy the code
Request uploadFile5 method through postman, B service console prints the following result:
- If you have any questions or errors in this article, please feel free to comment. If you find this article helpful, please like it and follow it.