Загружайте файлы через Feign | Практика разработки Java

Spring Cloud
Загружайте файлы через Feign | Практика разработки Java

Это первый раз, когда я участвую в Gengwen Challenge.10День, подробности о событии уточняйте:Обновить вызов

Эта статья участвует в "Java Theme Month - Java Development Practice". Подробнее см.:Ссылка на мероприятие

Со временем капли воды и камни изнашиваются 😄

Недавно столкнулся с необходимостью пройтиFeignПередача файлов. Я думал, что это просто, но я не ожидал столкнуться со многими проблемами. Давайте посмотрим вместе с автором!

импортировать зависимости

<!--boot 版本为 2.2.2 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

image-20210606134643243.png

Этот модуль добавляет пару кодировокapplication/x-www-form-urlencodedа такжеmultipart/form-dataПоддержка формы.

image.png

Дальше кодирование.

первое издание

Сервис

  • порцияController
    @Autowired
    PayFeign payFeign;
    
    @PostMapping("/uploadFile")
    public void upload(@RequestParam MultipartFile multipartFile,String title){
        payFeign.uploadFile(multipartFile,title);
    }
  • Сервис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);

}

будь осторожен:

  • Feignуказано вconsumesФорматMULTIPART_FORM_DATA_VALUE, который указывает отправленный тип контента для запроса.
  • Типы файлов могут использовать аннотации@RequestPart, можно и не писать, пользоваться все равно нельзяRequestParamаннотация.

Б сервис

@PostMapping(value="/uploadFile")
void uploadFile(@RequestParam("multipartFile") MultipartFile multipartFile, 
@RequestParam("title") String title){

    System.out.println(multipartFile.getOriginalFilename() + "=====" + title);

}

Примечание: сервис A, сервис BControllerвнутреннийMultipartFileИмена должны совпадать. Что касается услуги АFeignИмя можно играть по желанию, конечно, старайтесь, чтобы оно соответствовало.

Это нормально писать так. да черезFeignпередача файлов. Но позже требования изменились. Способ приема параметров на стороне службы B изменился, и для приема параметров используется класс сущности. Поскольку я чувствую, что предыдущий метод передачи параметров все еще используется, если будет четыре или пять параметров, а то и больше, читабельность кода снизится.

второе издание

Б сервис

добавить интерфейсuploadFile2, содержание следующее:

  @PostMapping(value="/uploadFile2")
    void uploadFile(FileInfoDTO fileInfo){
        System.out.println(fileInfo.getMultipartFile().getOriginalFilename() 
        + "=====" + fileInfo.getTitle());

    }

Использование женьшеняFileInfoDTOперенимать.FileInfoDTOСодержание следующее:

public class FileInfoDTO {

    private MultipartFile multipartFile;
    private String title;
	// 省略get/set
}

Сервис

Запрос на обслуживание также изменяется соответственно, изменения заключаются в следующем:

  • Сервисный контроллер
@PostMapping("/uploadFile2")
public void upload(FileInfo fileInfo){
	payFeign.uploadFile2(fileInfo);
}	
  • Симуляция службы
@PostMapping(value="/api/pay/uploadFile2",
		consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadFile2(FileInfoDTO fileInfo);    

Через Postman запросить сервисный интерфейс:image-20210606180637468.png

Возникает следующее исключение:

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]

image-20210606181203604.png

Далее анализ исходного кода! ! !

Мы можем сделать вывод из информации о стеке, напечатанной на консоли:

он находится вSpringEncoderКатегорияencodeМетод имеет исключение! Итак, давайте посмотрим на содержимое этого метода:

@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request)
	throws EncodeException {
// template.body(conversionService.convert(object, String.class));
// 请求的主体信息不为 null
if (requestBody != null) {
	// 获得请求的 Class
	Class<?> requestType = requestBody.getClass();
	//获得 Content-Type  也就是我们所指定 consumes 的值
	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);
	}
	// 主体的类型不为 null 并且类型为 MultipartFile
	if (bodyType != null && bodyType.equals(MultipartFile.class)) {
		// Content-Type 为 multipart/form-data
		if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
			// 调用 SpringFormEncoder 的 encode 方法
			this.springFormEncoder.encode(requestBody, bodyType, request);
			return;
		}else {
			// 如果主体的类型是 MultipartFile,但Content-Type 不为 multipart/form-data
			// 则抛出异常
			String message = "Content-Type \"" + MediaType.MULTIPART_FORM_DATA
					+ "\" not set for request body of type "
					+ requestBody.getClass().getSimpleName();
			throw new EncodeException(message);
		}
	}
	// 我们请求进入到了这里,进行转换
	// 主体的类型不为 MultipartFile 就通过 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;
		}
	}
	//没有转换成功 抛出异常 我们的那种写法就是进入到了这里
	String message = "Could not write request: no suitable HttpMessageConverter "
			+ "found for request type [" + requestType.getName() + "]";
	if (requestContentType != null) {
		message += " and content type [" + requestContentType + "]";
	}
	throw new EncodeException(message);
}
}

Этот метод имеет три параметра, а именно:

image-20210606182303060.png

  • requestBody: информация тела запроса
  • bodyType: тип тела
  • запрос: запрошенная информация, такая как: адрес запроса, метод запроса, код запроса

Давайте сначала определим проблему, почему она входит вSpringEncoderв классеencodeметод?EncoderЕсть несколько реализаций, где указано для реализации?image-20210606214220452.png

Давайте сначала посмотримFeignПри инициализации указанный класс реализацииDefaultДа зачем ты вошелSpringEncoderКуда ты ушел?

image-20210608220254970.png

Конкретная логика вFeignClientsConfigurationкласс, обеспечиваетEncoder Bean

@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder() {
	return new SpringEncoder(this.messageConverters);
}

Представьте значение двух аннотаций:

  • CONDITIONALONMISSINGCLASS: если класс не существует в пути к классам, создайте экземпляр текущего компонента.

  • ConditionalOnMissingBean: если данный bean-компонент не существует, создайте экземпляр текущего bean-компонента.

Где это было назначено? еще вFeignв абстрактном классе

image-20210606215224433.png

который имеет статический внутренний классBuilder,BuilderEстьencoderметод.

public Builder encoder(Encoder encoder) {
      this.encoder = encoder;
      return this;
}

image-20210606215407833.png

ДолженencoderМетод вызывается из двух мест. Посмотрите на первый пункт вызова здесь, второй — это чтение значения файла конфигурации, сначала игнорируйте его, здесь не используется никакая конфигурация.

image-20210606215607521.pngпередачаgetметод. После этого на его основеEncoderтипа идтиSpringНайдите бобы в . Полученное значение равноFeignClientsConfiguration中配置的。讲了这么多,那怎么解决呢! теперь, когдаSpringEncoderНе можем решить этот сценарий для нас, тогда мы изменим другойEncoderДостаточно.

@Configuration
public class FeignConfig {

//    @Bean
//    public Encoder multipartFormEncoder() {
//        return new SpringFormEncoder();
//    }
    
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }
}

Оба вышеперечисленных метода возможны, один с параметрами, и параметры указываются какSpringEncoder, без аргументов, по умолчаниюnew Encoder.Default(). обеспечитьFeignConfigкласс конфигурации, который предоставляетEncoderBean, но его конкретная реализацияSpringFormEncoder.既然我们提供了一个EncoderБин, чтоSpringBootбудет использовать то, что мы настроили, и конкретная логика войдет вSpringFormEncoderизencoderметод.

image-20210606220634608.pngДавайте посмотрим на конкретный исходный код:

@Override
public void encode (Object object, Type bodyType, RequestTemplate template) 
throws EncodeException {
    // 主体类型为 MultipartFile 数组
	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() 获取的属性名称
		data.put(file.getName(), file);
	  }
        // 调用父类方法
	  super.encode(data, MAP_STRING_WILDCARD, template);
	} else if (bodyType.equals(MultipartFile.class)) {
        //  主体类型为 MultipartFile
	  val file = (MultipartFile) object;
	  val data = singletonMap(file.getName(), object);
         // 调用父类方法
	  super.encode(data, MAP_STRING_WILDCARD, template);
	} else if (isMultipartFileCollection(object)) {
         //  主体类型为 MultipartFile集合
	  val iterable = (Iterable<?>) object;
	  val data = new HashMap<String, Object>();
	  for (val item : iterable) {
		val file = (MultipartFile) item;
          // file.getName() 获取的属性名称
		data.put(file.getName(), file);
	  }
         // 调用父类方法
	  super.encode(data, MAP_STRING_WILDCARD, template);
	} else {
      //其他类型  还是调用父类方法
	  super.encode(object, bodyType, template);
	}	
}

Вы можете видеть, что существует много поддерживаемых форматов, но на самом деле все они вызывают родительский классencodeМетоды просто разные. Следующий посмотрите на родительский классFormEncoderкод,

public void encode (Object object, Type bodyType, RequestTemplate template) 
throws EncodeException {
    // Content-Type的值
	String contentTypeValue = getContentTypeValue(template.headers());
    // 进行转换 比如:multipart/form-data 会被转为 MULTIPART
	val contentType = ContentType.of(contentTypeValue);
    if (!processors.containsKey(contentType)) {
      delegate.encode(object, bodyType, template);
      return;
    }

    Map<String, Object> data;
	// 判断 bodyType 的类型是否是 Map
    if (MAP_STRING_WILDCARD.equals(bodyType)) {
      data = (Map<String, Object>) object;
    } else if (isUserPojo(bodyType)) {
	 //  判断 bodyType 的名称是否以 class java.开头 如果不是,将类对象转换为 Map
        // 我们也就是属于这种情况。
      data = toMap(object);
    } else {
      delegate.encode(object, bodyType, template);
      return;
    }

    val charset = getCharset(contentTypeValue);
    // 根据不同的 contentType 走不同的流程
    processors.get(contentType).process(template, charset, data);
  }

processorsЯвляетсяMap, который имеет два значения, которые

  • MULTIPART:MultipartFormContentProcessor,

  • URLENCODED:UrlencodedFormContentProcessor

Вышеупомянутый метод решил проблему, с которой мы столкнулись, и поддерживаются как метод запроса первой версии, так и метод запроса второй версии.

Сколько существует вариантов написания?

image-20210606235717960.png

Можно ли пройти более одного?

Нет, ключом собранной Карты является имя свойства, даже если вы передаете несколько файлов, последний файл будет основным.

image-20210608230201694.png

Вы можете задаться вопросом, можете ли вы написать это так, передав несколькоMultipartFileобъект.

 @PostMapping(value="/api/pay/uploadFile5",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
void uploadFile5(@RequestPart("multipartFile")MultipartFile multipartFile, 
@RequestPart("multipartFile2")MultipartFile multipartFile2);

Извините, нет, такой способ написания выдаст ошибку при запуске:Method has too many Body parameters.

Другие ситуации

Также существует ситуация, когда файл создается службой A внутри, а не передается извне. Тип файла, который мы генерируем сами, это File, а неMultipartFileтип, вам нужно преобразовать его в это время.

добавить зависимости

<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.3.3</version>
</dependency>

написать класс инструмента

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) {
    //重点  这个名字需要和你对接的MultipartFil的名称一样
    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 new CommonsMultipartFile(item);
}
}

Метод добавления услуги

@PostMapping("/uploadFile5")
public void upload5(){
    File file = new File("D:\\gongj\\log\\product-2021-05-12.log");
    MultipartFile multipartFile = FileUtil.fileToMultipartFile(file);
    payFeign.uploadFile(multipartFile,"上传文件");
}

пройти черезpostmanпроситьuploadFile5метод, консоль службы B выводит результат следующим образом:

image-20210609213652442.png

  • Если у вас есть какие-либо вопросы по этой статье или есть ошибки в этой статье, пожалуйста, оставьте комментарий. Если вы считаете, что эта статья была вам полезна, ставьте лайк и подписывайтесь на нее.