оригинал:Анализ ненормального принципа загрузки файлов в Spring Series Tutorials of a Gray Blog
Приложение, созданное с помощью SpringBoot, работало хорошо, но внезапно обнаружило, что загрузка файла не удалась.org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not validКаталог является незаконным, на самом деле проверьте каталог, результата действительно нет, ниже приведена производительность этой проблемы, проанализируйте процесс обработки SpringBoot для загрузки файла.
I. Анализ проблемы
0. Анализ стека
Для локализации проблемы лучшим вспомогательным методом является анализ стека.Во-первых, узнайте информацию об основном стеке.
org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:122)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:113)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:86)
at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:93)
at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
Из содержимого стека проблема относительно ясна, каталог неправильный, по пути входим в каталог, получается, что если такого каталога нет, то ключ к проблеме в том, почему нет каталога, чтобы вызвать исключение, и какая польза от этого каталога?
Сначала кратко опишите вышеуказанные причины, загружаемые файлы будут кэшироваться на локальный диск, а путь кэширования указан выше/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT, то вводится вопрос:
- Почему загружаемые файлы кэшируются локально?
- Почему временный каталог не существует?
- Где реализовать кеширование файлов
1. Моделирование сценария
Чтобы подтвердить вышеуказанную проблему, наиболее интуитивный способ — прочитать исходный код.Немного больно смотреть на код напрямую.Затем используйте метод отладки, чтобы отслоить слой за слоем и посмотреть, где находится источник.
Во-первых, создайте простой тестовый проект для воспроизведения сцены.Во-первых, создайте контроллер, который получает загружаемые файлы, как показано ниже.
@RestController
@RequestMapping(path = "/file")
public class FileUploadRest {
/**
* 保存上传的文件
*
* @param file
* @return
*/
private String saveFileToLocal(MultipartFile file) {
try {
String name = "/tmp/out_" + System.currentTimeMillis() + file.getName();
FileOutputStream writer = new FileOutputStream(new File(name));
writer.write(file.getBytes());
writer.flush();
writer.close();
return name;
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
}
}
@PostMapping(path = "upload")
public String upload(@RequestParam("file") MultipartFile file) {
String ans = saveFileToLocal(file);
return ans;
}
}
Второй — использовать curl для загрузки файлов.
curl http://127.0.0.1:8080/file/upload -F "file=@/Users/user/Desktop/demo.jpg" -v
Затем откройте точку останова в методе получения загрузки файла, обратите внимание на красное поле нижеlocation, который является временным каталогом для загрузки файлов
2. Позиционирование исходного кода
Скриншот выше может подтвердить, что загруженный файл действительно сохранен во временном каталоге.Метод проверки заключается в том, чтобы войти в этот каталог для просмотра, и вы увидите файл tmp.Далее нам нужно определить, где кэшировать данные локально.
Обратите внимание на рисунок ниже, красное поле слева — это полная ссылка на этот запрос, мы можем изменить ссылку, чтобы найти место, где может быть реализовано кэширование файлов.
Если вы не знакомы с исходным кодом spring и tomcat, особого хорошего способа нет.По приведенной выше ссылке сделайте больше точек останова и используйте легендарный метод бинарного позиционирования, чтобы сузить область видимости.
В результате анализа исходного объекта запроса и последующего объекта запроса было обнаружено, что в качестве эталонного стандарта можно использовать красный прямоугольник справа на рисунке выше.request#partsАтрибут; в начале он нулевой, и данные будут после сохранения файла.Ниже показано движущееся изображение окончательного позиционирования
Итак, ключorg.springframework.web.filter.HiddenHttpMethodFilter#doFilterInternalсерединаString paramValue = request.getParameter(this.methodParam);эта строка кода
Идя сюда за один шаг, основное внимание будет уделеноorg.apache.catalina.connector.Request#parseParts
Войдя в логику вышеуказанного метода, легко найти конкретное место реализации.org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest, реализация этого метода интереснее, надо выложить
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<>();
boolean successful = false;
try {
FileItemIterator iter = getItemIterator(ctx);
// 注意这里,文件工厂类,里面保存了临时目录的地址
// 这个对象首次是在 org.apache.catalina.connector.Request#parseParts 方法的
FileItemFactory fac = getFileItemFactory();
if (fac == null) {
throw new NullPointerException("No FileItemFactory has been set.");
}
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
// 创建一个临时文件对象
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
// 流的拷贝,这块代码也挺有意思,将输入流数据写入输出流
// 后面会贴出源码,看下开源大佬们的玩法,和我们自己写的有啥区别
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
fileItem.setHeaders(fih);
}
successful = true;
return items;
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
} finally {
if (!successful) {
for (FileItem fileItem : items) {
try {
fileItem.delete();
} catch (Exception ignored) {
// ignored TODO perhaps add to tracker delete failure list somehow?
}
}
}
}
}
Основной код имеет две точки: одна — это класс фабрики файлов, а другая — потоковая копия; первая определяет наш каталог временных файлов, который также является ключом к решению предыдущей проблемы.Перейдите в пользовательский каталог и никогда не удаляйте его. , иначе этого можно избежать Решается ли вышеприведенная проблема, последняя касается мультиплексирования данных
Сначала посмотрите на место создания экземпляра FileItemFactory вorg.apache.catalina.connector.Request#parseParts, код выглядит следующим образом
Код создания конкретного местоположения
// TEMPDIR = "javax.servlet.context.tempdir";
location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));
3. Обзор проблемы
а) решить проблему
К сказанному выше, в основном найдена окончательная проблема, давайте сначала посмотрим, как решить эту проблему.
метод 1
- Перезапуск приложения
Способ 2
- Добавить конфигурацию службы, настроить baseDir
server.tomcat.basedir=/tmp/tomcat
Способ 3
- Внедрить bean-компонент, вручную настроить временный каталог
@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setLocation("/tmp/tomcat");
return factory.createMultipartConfig();
}
Способ 4
- Настройте, чтобы не удалять tomcat в каталоге tmp
vim /usr/lib/tmpfiles.d/tmp.conf
# 添加一行
x /tmp/tomcat.*
б. Потоковое копирование
Код копирования для реализации потока в tomcat выглядит следующим образом:org.apache.tomcat.util.http.fileupload.util.Streams#copy(java.io.InputStream, java.io.OutputStream, boolean, byte[]), Глядя на реализацию ниже, интуитивно понятно, что написание действительно строгое
public static long copy(InputStream inputStream,
OutputStream outputStream, boolean closeOutputStream,
byte[] buffer)
throws IOException {
OutputStream out = outputStream;
InputStream in = inputStream;
try {
long total = 0;
for (;;) {
int res = in.read(buffer);
if (res == -1) {
break;
}
if (res > 0) {
total += res;
if (out != null) {
out.write(buffer, 0, res);
}
}
}
if (out != null) {
if (closeOutputStream) {
out.close();
} else {
out.flush();
}
out = null;
}
in.close();
in = null;
return total;
} finally {
IOUtils.closeQuietly(in);
if (closeOutputStream) {
IOUtils.closeQuietly(out);
}
}
}
в) Самостоятельные вопросы и самоответы
Ранее поднималось несколько вопросов, и теперь я дам простой ответ: из-за проблем с местом я позже напишу отдельную статью, чтобы подробно объяснить.
куда кэшировать файлы
Приведенный выше процесс позиционирования дает ответ, конкретная логика реализации находится вorg.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest
Почему каталог не существует
При запуске springboot временный каталог /tmp/tomcat.*/work/Tomcat/localhost/ROOT будет создан как временный каталог для загрузки файлов, но этот каталог будет автоматически очищен системой через n дней. выполняется linux Система завершена, конкретная конфигурация выглядит следующим образомvim /usr/lib/tmpfiles.d/tmp.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
# See tmpfiles.d(5) for details
# Clear tmp directories separately, to make them easier to override
v /tmp 1777 root root 10d
v /var/tmp 1777 root root 30d
# Exclude namespace mountpoints created with PrivateTmp=yes
x /tmp/systemd-private-%b-*
X /tmp/systemd-private-%b-*/tmp
x /var/tmp/systemd-private-%b-*
X /var/tmp/systemd-private-%b-*/tmp
Зачем кешировать файлы
Поскольку после того, как поток будет использован один раз, данные из потока не могут быть получены позже, поэтому кеш удобен для последующего повторного использования, этот раздел будет подробно объяснен позже.
4. Резюме
Ощущение от обнаружения этой проблемы в том, что я действительно не знаком с нижним слоем SpringBoot и tomcat.Как фермер кода, который ест с Spring и tomcat, если вы обнаружите проблему, вам нужно исправить ее и перечислить в списке дел. список.
II. Другое
0. Проект
- проект:spring-boot-demo
1. Серый блог
- One Grey BlogПерсональный блогblog.hhui.top
- Серый блог - специальный весенний блогspring.hhui.top
Серый личный блог, записывающий все посты блога по учебе и работе, приглашаю всех в гости
2. Заявление
Это не так хорошо, как письмо. Вышеупомянутое содержание чисто из семьи. Из-за ограниченных личных способностей неизбежно есть упущения и ошибки. Если вы найдете ошибки или у вас есть лучшие предложения, вы можете критиковать и исправлять их. Спасибо ты очень.
- Адрес вейбо:Маленький серый блог
- QQ: серо-серый / 3302797840
3. Сканируйте внимание
серый блог
планета знаний