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 serviceController
    @Autowired
    PayFeign payFeign;
    
    @PostMapping("/uploadFile")
    public void upload(@RequestParam MultipartFile multipartFile,String title){
        payFeign.uploadFile(multipartFile,title);
    }
Copy the code
  • A ServiceFeign
@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:

  • FeignSpecified in theconsumesThe format forMULTIPART_FORM_DATA_VALUETo 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 itRequestParamAnnotation.

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 serviceControllerIn theMultipartFileMust have the same name. As for the “A” serviceFeignYou 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 inSpringEncoderIn the classencodeMethod?EncoderThere 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.

callgetMethods. And then there’s the basisEncoderType toSpringLook for beans in. The value you get is atFeignClientsConfigurationIn. Talk so much, then how to solve! sinceSpringEncoderCan’t solve our situation, so let’s move onEncoderIt’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.