Запишите исключение о multipartResolver, возникшее некоторое время назад, и процесс выяснения причины позже.
Анализ аномалий
Исключение составляет следующее:
2018-01-22 18:05:38.041 ERROR com.exception.ExceptionHandler.resolveException:22 -Could not Q multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null
at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:165) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1089) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:928) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:870) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) [servlet-api.jar:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [servlet-api.jar:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [catalina.jar:8.5.24]
at org.apache.catalina.core.A
Это исключение связано с тем, что в форме, передаваемой multipart/form-data, есть нулевое значение, и нет возможности прочитать определенное значение из формы запроса.
После подтверждения того, что сам запрос не нуль, перейдите к тому, что есть проблема в процессе SpringMVC, получала запрос и прочитав параметры из запроса.
Итак, как SpringMVC обрабатывает файлы, переданные по запросу?
Процесс multipartResolver, обрабатывающий запрос
Переадресация DispatcherServlet
Прежде всего, Spring обеспечивает поддержку многократной загрузки файлов.Пока зарегистрирован bean-компонент с именем «multipartResolver», DispatcherServlet SpringMVC будет определять, является ли запрос составным файлом, когда он получает запрос. Если это так, то вызывается «multipartResolver», оборачивающий запрос в объект MultipartHttpServletRequest, после чего файл может быть извлечен из этого объекта для обработки.
Загрузка multipartResolver
Spring предоставляет реализацию интерфейса MultipartResolver: org.springframework.web.multipart.commons.CommonsMultipartResolver. Взгляните на исходный код:
public class CommonsMultipartResolver extends CommonsFileUploadSupport
implements MultipartResolver, ServletContextAware {
...
}
CommonsFileUploadSupport предназначен для поддержки конфигурации XML «multipartResolver». Конфигурация при настройке multipartResolver в XML выглядит следующим образом:
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 默认编码 -->
<property name="defaultEncoding" value="utf-8" />
<!-- 设置multipart请求所允许的最大大小,默认不限制 -->
<property name="maxUploadSize" value="10485760000" />
<!-- 设置一个大小,multipart请求小于这个大小时会存到内存中,大于这个内存会存到硬盘中 -->
<property name="maxInMemorySize" value="40960" />
</bean>
Эти конфигурации свойств будут загружены в CommonsFileUploadSupport, а затем унаследованы CommonsMultipartResolver.
Обработка CommonsMultipartResolver
Тогда, фактически, CommonsMultipartResolver полагается на пакет jar Apache для достижения: common-fileupload.
После того, как CommonsMultipartResolver получает запрос, он обрабатывает HttpServletRequests следующим образом:
(CommonsMultipartResolver文件)
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
//懒加载
if (this.resolveLazily) {
return new DefaultMultipartHttpServletRequest(request) {
@Override
protected void initializeMultipart() {
MultipartParsingResult parsingResult = parseRequest(request);
setMultipartFiles(parsingResult.getMultipartFiles());
setMultipartParameters(parsingResult.getMultipartParameters());
setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
}
};
}
else {
//这里对request进行了解析
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
}
this.resolveLazily — отложенная загрузка, если оно равно true, то будет инкапсулировать DefaultMultipartHttpServletRequest при вызове initializeMultipart(), то есть когда инициируется получение информации о документе, если false — DefaultMultipartHttpServletRequest будет инкапсулировано немедленно.
resolveLazily по умолчанию имеет значение false.
Затем взгляните на разбор parseRequest(request):
(CommonsMultipartResolver文件)
/**
* Parse the given servlet request, resolving its multipart elements.
* 对servlet请求进行处理,转成multipart结构
* @param request the request to parse
* @return the parsing result
* @throws MultipartException if multipart resolution failed.
*/
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
//从请求中读出这个请求的编码
String encoding = determineEncoding(request);
//按照请求的编码,获取一个FileUpload对象,装载到CommonsFileUploadSupport的property属性都会被装入这个对象中
//prepareFileUpload是继承自CommonsFileUploadSupport的函数,会比较请求的编码和XML中配置的编码,如果不一样,会拒绝处理
FileUpload fileUpload = prepareFileUpload(encoding);
try {
//对请求中的multipart文件进行具体的处理
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
}
catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
}
catch (FileUploadException ex) {
throw new MultipartException("Could not parse multipart servlet request", ex);
}
}
Приведенный выше анализ ((ServletFileUpload) fileUpload).parseRequest(request) реализован следующим образом:
(FileUploadBase文件)
/**
* Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a>
* compliant <code>multipart/form-data</code> stream.
*
* @param ctx The context for the request to be parsed.
*
* @return A list of <code>FileItem</code> instances parsed from the
* request, in the order that they were transmitted.
*
* @throws FileUploadException if there are problems reading/parsing
* the request or storing files.
*/
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
boolean successful = false;
try {
//从请求中取出multipart文件
FileItemFactoryFactoryFactoryator iter = getItemIterator(ctx);
//获得FileItemFactory工厂,实现类为DiskFileItemFactory
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对象,实现类是DiskFileItem
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(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 (Throwable e) {
// ignore it
}
}
}
}
}
Исключение, с которым мы столкнулись, генерируется в этой позиции, и мы углубимся в ошибку позже, но мы все равно сначала завершим весь процесс потока запросов.
На данный момент объект списка был обработан и возвращен, а затем продолжите видеть обработку списка.
(CommonsFileUploadSupport文件)
/**
* Parse the given List of Commons FileItems into a Spring MultipartParsingResult,
* containing Spring MultipartFile instances and a Map of multipart parameter.
* @param fileItems the Commons FileIterms to parse
* @param encoding the encoding to use for form fields
* @return the Spring MultipartParsingResult
* @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem)
*/
protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
Map<String, String[]> multipartParameters = new HashMap<String, String[]>();
Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();
// Extract multipart files and multipart parameters.
for (FileItem fileItem : fileItems) {
//如果fileItem是一个表单
if (fileItem.isFormField()) {
String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
if (partEncoding != null) {
try {
value = fileItem.getString(partEncoding);
}
catch (UnsupportedEncodingException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
"' with encoding '" + partEncoding + "': using platform default");
}
value = fileItem.getString();
}
}
else {
value = fileItem.getString();
}
String[] curParam = multipartParameters.get(fileItem.getFieldName());
if (curParam == null) {
// simple form field
multipartParameters.put(fileItem.getFieldName(), new String[] {value});
}
else {
// array of simple form fields
String[] newParam = StringUtils.addStringToArray(curParam, value);
multipartParameters.put(fileItem.getFieldName(), newParam);
}
multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
}
//如果fileItem是一个multipart文件
else {
// multipart file field
CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
multipartFiles.add(file.getName(), file);
if (logger.isDebugEnabled()) {
logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
" bytes with original filename [" + file.getOriginalFilename() + "], stored " +
file.getStorageDescription());
}
}
}
return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}
В этот момент обработка MultipartParsingResult заканчивается и возвращается, а затем resolveMultipart в CommonsMultipartResolver загружает его в DefaultMultipartHttpServletRequest и возвращает, обработка завершена.
DefaultmultiparthttpservletRequest - это класс реализации MultiParthtservletRequest.
О maxInMemorySize
Как упоминалось ранее, роль maxInMemorySize"Установите размер, если составной запрос меньше этого размера, он будет сохранен в памяти, а если больше этого размера, он будет сохранен на жестком диске". Давайте посмотрим на процесс установки maxInMemorySize в объект:
(CommonsFileUploadSupport文件)
/**
* Set the maximum allowed size (in bytes) before uploads are written to disk.
* Uploaded files will still be received past this amount, but they will not be
* stored in memory. Default is 10240, according to Commons FileUpload.
* @param maxInMemorySize the maximum in memory size allowed
* @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold
*/
public void setMaxInMemorySize(int maxInMemorySize) {
this.fileItemFactory.setSizeThreshold(maxInMemorySize);
}
В CommonsFileUploadSupport есть объект fileItemFactory, и maxInMemorySize имеет значение свойства SizeThreshold этого фабричного класса.
Этот фабричный класс fileItemFactory будет использоваться при создании объектов fileItem. В процессе создания этого объекта будет оцениваться maxInMemorySize, следует ли хранить его в памяти или на жестком диске.
Ранее упоминались хранимые процедуры:
...
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
Перейти к fileItem.getoutputtream (), чтобы увидеть:
/**
* Returns an {@link java.io.OutputStream OutputStream} that can
* be used for storing the contents of the file.
*
* @return An {@link java.io.OutputStream OutputStream} that can be used
* for storing the contensts of the file.
*
* @throws IOException if an error occurs.
*/
public OutputStream getOutputStream()
throws IOException {
if (dfos == null) {
File outputFile = getTempFile();
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
Затем перейдите в getTempFile():
/**
* Creates and returns a {@link java.io.File File} representing a uniquely
* named temporary file in the configured repository path. The lifetime of
* the file is tied to the lifetime of the <code>FileItem</code> instance;
* the file will be deleted when the instance is garbage collected.
*
* @return The {@link java.io.File File} to be used for temporary storage.
*/
protected File getTempFile() {
if (tempFile == null) {
File tempDir = repository;
if (tempDir == null) {
tempDir = new File(System.getProperty("java.io.tmpdir"));
}
String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());
tempFile = new File(tempDir, tempFileName);
}
return tempFile;
}
Когда свойство uploadTempDir не задано, то есть репозиторий в FileItemFactory, будет автоматически выбран путь к кэшу System.getProperty("java.io.tmpdir"), и запрос на загрузку попадет в это место на жестком диске.
Обратите внимание на это примечание:the file will be deleted when the instance is garbage collected.Здесь упоминается цикл объявления экземпляра FileItem.Когда выполняется GC, FileItem в памяти будет повторно использован GC. Вот почему нет способа прочитать объекты multipart/form-data.
Причины ошибок и решения
- Решить проблему частого GC. Слишком частый GC, очевидно, является проблемой, приводящей к повторному использованию файлов в запросе и выдаче отчета о нулевом указателе. (Это также мое решение проблемы здесь)
- Установите maxinmemorysize и uploadtempdir два свойства, чтобы убедиться, что загружая файлы в кеш жесткого диска, вы можете запросить General в памяти. Если он включает в себя большое количество загрузок файлов, это необходимо, или иначе, когда высокая параллелизм, файл памяти для заполнения. Затем запускает GC, FileItem восстановлен после выхода, он вернется к чтению, он был нулевым указателем исключение наших ошибок.
- Также существует вероятность того, что свойство uploadTempDir не задано при настройке multipartResolver. Само собой разумеется, что это не проблема, потому что это поможет вам установить путь к кешу системы по умолчанию.Этот путь обычно /tmp, и все пользователи имеют разрешение на чтение этого каталога. Но если это производственная среда, путь к кэшу по умолчанию в системе, вероятно, будет изменен, местоположение или разрешения были изменены. Это тоже из соображений безопасности, но в процессе, о котором мы говорим, это вызовет ошибку нулевого указателя при последующем чтении.
Эти исключения нелегко устранить, поэтому легко найти проблему после того, как весь процесс будет ясен. Вы не можете сказать, просто взглянув на свой собственный код.Например, проблема разрешений встречается только в реальной производственной среде, и это совершенно беспомощно.
Почему я так усложняю этот вопрос?
Может показаться глупым писать так много об этой проблеме и так мало о решении в конце, но тому есть причина:
- Я не нашел полезного решения для этого исключения в Интернете, и я не видел ясной причины.
- Повторное прохождение процесса поможет решить аналогичные проблемы в будущем. Например, вместо сообщения о нулевом значении при сообщении о других проблемах.
- Самый важный момент заключается в том, что из-за операционной среды нет возможности воспроизвести, и в то время невозможно найти информацию о стеке, поэтому я должен пройти весь процесс...