Это первый раз, когда я участвую в 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>
Этот модуль добавляет пару кодировокapplication/x-www-form-urlencodedа такжеmultipart/form-dataПоддержка формы.
Дальше кодирование.
первое издание
Сервис
- порция
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 запросить сервисный интерфейс:
Возникает следующее исключение:
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]
Далее анализ исходного кода! ! !
Мы можем сделать вывод из информации о стеке, напечатанной на консоли:
он находится в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);
}
}
Этот метод имеет три параметра, а именно:
- requestBody: информация тела запроса
- bodyType: тип тела
- запрос: запрошенная информация, такая как: адрес запроса, метод запроса, код запроса
Давайте сначала определим проблему, почему она входит вSpringEncoder
в классеencode
метод?Encoder
Есть несколько реализаций, где указано для реализации?
Давайте сначала посмотримFeign
При инициализации указанный класс реализацииDefault
Да зачем ты вошелSpringEncoder
Куда ты ушел?
Конкретная логика вFeignClientsConfiguration
класс, обеспечиваетEncoder
Bean
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters);
}
Представьте значение двух аннотаций:
-
CONDITIONALONMISSINGCLASS: если класс не существует в пути к классам, создайте экземпляр текущего компонента.
-
ConditionalOnMissingBean: если данный bean-компонент не существует, создайте экземпляр текущего bean-компонента.
Где это было назначено? еще вFeign
в абстрактном классе
который имеет статический внутренний классBuilder
,Builder
Eстьencoder
метод.
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
Долженencoder
Метод вызывается из двух мест. Посмотрите на первый пункт вызова здесь, второй — это чтение значения файла конфигурации, сначала игнорируйте его, здесь не используется никакая конфигурация.
передача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
класс конфигурации, который предоставляетEncoder
Bean, но его конкретная реализацияSpringFormEncoder
.既然我们提供了一个Encoder
Бин, чтоSpringBoot
будет использовать то, что мы настроили, и конкретная логика войдет вSpringFormEncoder
изencoder
метод.
Давайте посмотрим на конкретный исходный код:
@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
Вышеупомянутый метод решил проблему, с которой мы столкнулись, и поддерживаются как метод запроса первой версии, так и метод запроса второй версии.
Сколько существует вариантов написания?
Можно ли пройти более одного?
Нет, ключом собранной Карты является имя свойства, даже если вы передаете несколько файлов, последний файл будет основным.
Вы можете задаться вопросом, можете ли вы написать это так, передав несколько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 выводит результат следующим образом:
- Если у вас есть какие-либо вопросы по этой статье или есть ошибки в этой статье, пожалуйста, оставьте комментарий. Если вы считаете, что эта статья была вам полезна, ставьте лайк и подписывайтесь на нее.