java - Spring Boot 下 Undertow/Wildfly 上的文件上传和可接受的错误处理

标签 java spring-boot multipartform-data undertow

我们有一个在 Undertow 和 SpringBoot 上运行的项目,正在尝试添加文件上传。第一次尝试成功,文件已通过使用StandardServletMultipartResolver绑定(bind)到适当的Bean。并使用 application.properties 配置它。 然而,在错误处理方面我们遇到了可怕的困难。 我们找到了一个“解决方案”,将标准解析器配置为 100MB 并使用 CommonsMultipartResolver 。然后我们添加了一个像这样的过滤器

@Bean
public Filter filter() {
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            try {
                filterChain.doFilter(request, response);
            } catch (ServletException e) {
                if (e.getCause()
                        .getClass()
                        .equals(org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException.class)) {
                    int requestSize = request.getContentLength();
                    Collection<Part> parts = request.getParts();
                    List<String> oversizedFields = new LinkedList<>();
                    long uploadSize = 0;
                    for (Part part : new ArrayList<>(parts)) {
                        if (uploadSize + part.getSize() > MAX_UPLOAD_SIZE) {
                            requestSize -= part.getSize();
                            oversizedFields.add(part.getName());
                            request.getParameterMap()
                                    .remove(part.getName());
                            parts.remove(part);
                        } else {
                            uploadSize += part.getSize();
                        }
                    }
                    request.setAttribute("oversizedFields", oversizedFields);
                    SizeModifyingServletRequestWrapper requestWrapper = new SizeModifyingServletRequestWrapper(
                            request, requestSize, uploadSize);
                    filterChain.doFilter(requestWrapper, response);
                }
            }
        }
    };
}

请求包装器:

private static class SizeModifyingServletRequestWrapper extends
        HttpServletRequestWrapper {
    private int size;
    private long sizeLong;

    public SizeModifyingServletRequestWrapper(HttpServletRequest request,
            int size, long sizeLong) {
        super(request);
        this.size = size;
        this.sizeLong = sizeLong;
    }

    @Override
    public int getContentLength() {
        return size;
    }

    @Override
    public long getContentLengthLong() {
        return sizeLong;
    }

    @Override
    public String getHeader(String name) {
        if (FileUploadBase.CONTENT_LENGTH.equals(name)) {
            return Integer.toString(size);
        } else {
            return super.getHeader(name);
        }
    }
}

@Controller -method 然后检查超大文件并将结果添加到 BindingResult ,除了文件没有绑定(bind)到 bean 之外,效果很好。 事实证明CommonsMultipartResolver ,当尝试解析请求时,抛出 MalformedStreamExceptionItemInputStream.makeAvailable() ,它总是返回消息 String ended unexpectedly .

所以我们重新使用 StandardServletMultipartResolver ,并且能够捕获 RuntimeException它抛出得很好,但是当即使一个文件超出其大小边界时,它也绝对不会提供任何表单数据。

我们完全被难住了,因为无论解析器是否延迟工作。如果有人有任何进一步的想法如何解决这个问题,欢迎提出答案=)

更多引用代码:

摘自WebAppInitializer

@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver() {
    StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
    multipartResolver.setResolveLazily(true);
    return multipartResolver;
}

@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxFileSize("2MB");
    factory.setMaxRequestSize("100MB");
    return factory.createMultipartConfig();
}

从 Controller 摘录:

@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public String saveOrganizationDetails(
        @PathVariable(PATH_VARIABLE_ORGANIZATION_ID) String organizationId,
        @ModelAttribute @Valid Organization organization,
        BindingResult bindingResult, Model model,
        RedirectAttributes redirectAttributes, WebRequest request) {
checkForOversizedFiles(request, bindingResult);
    Map<String, MultipartFile> files = organization.getStyle().whichFiles();
}

private boolean checkForOversizedFiles(WebRequest request,
        BindingResult bindingResult) {
    if (request.getAttribute("oversizedFields", WebRequest.SCOPE_REQUEST) instanceof LinkedList) {
        @SuppressWarnings("unchecked")
        LinkedList<String> oversizedFiles = (LinkedList<String>) request
                .getAttribute("oversizedFields", WebRequest.SCOPE_REQUEST);
        for (String s : oversizedFiles) {
            String errorCode = KEY_ORGANIZATION_LOGO_OVERSIZED_FILE + s;
            bindingResult.rejectValue(s,
                    errorCode);
        }
        return true;
    } else {
        return false;
    }
}

private void handleUpload(Map<String, MultipartFile> files,
        OrganizationStyle style, BindingResult result) {
    for (String filename : files.keySet()) {
        if (processUpload(files.get(filename), filename)) {
            style.setLogoFlag(filename);
        } else {
            result.reject(KEY_ORGANIZATION_LOGO_UPLOAD_FAILURE);
        }
    }
}

processUpload()到目前为止还没有任何功能,这就是为什么我没有将它包含在这里。

从成型 bean 中提取:

public class OrganizationStyle {
@Transient
private MultipartFile logoPdf;
@Transient
private MultipartFile logoCustomerArea;
@Transient
private MultipartFile logoAssistant;
@Transient
private MultipartFile logoIdentityArea;

<omitting Getters and setters>

private Map<String, MultipartFile> getAllFiles() {
    Map<String, MultipartFile> files = new HashMap<>();
    files.put("logoPdf", logoPdf);
    files.put("logoCustomerArea", logoCustomerArea);
    files.put("logoAssistant", logoAssistant);
    files.put("logoIdentityArea", logoIdentityArea);
    return files;
}

public Map<String, MultipartFile> whichFiles() {
    Map<String, MultipartFile> whichFiles = new HashMap<>();
    for (String name : getAllFiles().keySet()) {
        MultipartFile file = getAllFiles().get(name);
        if (file != null && !file.isEmpty()) {
            whichFiles.put(name, file);
        }
    }
    return whichFiles;
}
}

如上所述,这不是完整的代码,而是解决这个特定问题的必要代码。 上传超大文件时抛出的异常是:

(java.io.IOException) java.io.IOException: UT000054: The maximum size 2097152 for an individual file in a multipart request was exceeded

或提到的FileUploadBase.FileSizeLimitExceedeException

最后但并非最不重要的一点是表单页面的摘录

<div id="layoutOne" class="panel-collapse collapse">
    <div class="panel-body">
        <div class="form-group">
            <label for="logoPdf" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.form.label}">LOGO-FORM</label>
            <input type="file" th:field="*{style.logoPdf}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoCustomerArea" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.customer.label}">LOGO-ORGANIZATION</label>
            <input type="file" th:field="*{style.logoCustomerArea}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoAssistant" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.assistant.label}">LOGO-ASSISTANT</label>
            <input type="file" th:field="*{style.logoAssistant}" accept="image/*" />
        </div>
        <div class="form-group">
            <label for="logoIdentityArea" class="control-label" th:text="#{organizationcontext.groups.addmodal.logo.id.label}">LOGO-ID</label>
            <input type="file" th:field="*{style.logoIdentityArea}" accept="image/*" />
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('style.cssUrl')}? has-error">
            <label for="style.cssUrl" class="control-label" th:text="#{organizationcontext.groups.addmodal.css.external.label}">CSS-EXTERNAL</label>
            <input th:field="*{style.cssUrl}" class="form-control" type="text" th:placeholder="#{placeholder.css.external}" />
        </div>
        <div class="form-group" th:classappend="${#fields.hasErrors('style.cssCode')}? has-error">
            <label for="style.cssCode" class="control-label" th:text="#{organizationcontext.groups.addmodal.css.input.label}">CSS</label>
            <textarea th:field="*{style.cssCode}" class="form-control" th:placeholder="#{placeholder.css.input}"></textarea>
        </div>
    </div>
</div>

如果您关注此处的问题,您应该已经意识到我们已经尝试了几种可能的解决方案,其中大部分来自此处。现在,过滤器捕获 RuntimeException并检查 IOException因此,尺寸也不再在 application.properties 中设置。

任何帮助或建议都将非常感激。

更多信息

所以,我调试了 StandardServletMultipartResolver并发现它使用 ISO-8859-1-charset 进行解析。即使页面是 UTF-8 编码并且请求对象也具有 UTF-8-Charset,这确实会产生所需的效果。我一直在尝试使用像这样的过滤器强制 ISO-Charset

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Filter characterEncodingFilter() {
    CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
    characterEncodingFilter.setEncoding("ISO-8859-1");
    characterEncodingFilter.setForceEncoding(true);
    return characterEncodingFilter;
}

但是,由于某种原因,CommonsMultipartResolver找到一个 UTF-8 编码的请求对象,因此要么此编码不起作用,要么我犯了另一个我看不到的错误。

我还尝试找到抛出异常的确切时刻,也许可以自己扩展该类并确保保留已解析的表单数据,但到目前为止无济于事。

更多信息

根据此处另一个线程的建议,我尝试在请求上强制使用 ISO-8859-1 字符集。起初,这完全绕过了CommonsMultipartResolver并弄乱了我的文本,现在它过滤到正确的解析器,但这仍然表明多部分数据中没有文件。仅供引用,我使用的Filterclass:

private class MyMultiPartFilter extends MultipartFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setCharacterEncoding("ISO-8859-1");
        request.getParameterNames();
        super.doFilterInternal(request, response, filterChain);
    }
}

从中创建了一个 Bean,并将 multipartResolver()-Bean 的名称更改为 filterMultipartResolver()

最佳答案

问题的解决方案大致是在我寻找的时候找到的。已发布here .

由于 WildFly 和 Undertow 在处理 StandardServletMultipartResolver 方面存在困难,因此使用 CommonsMultipartResolver 更有效(甚至可能是必要的)。但是,必须在处理其余 POST 数据之前调用此方法。
为了确保这一点,需要调用 MultipartFilter 并创建一个 filterMultipartResolver-Bean,如下所示:

@Bean
public CommonsMultipartResolver filterMultipartResolver() {
    return new CommonsMultipartResolver();
}

@Bean
@Order(0)
public MultipartFilter multipartFilter() {
    return new MultipartFilter();
}

这确保首先调用过滤器,然后再调用解析器。唯一的缺点是没有开箱即用的方法来限制上传的单个文件大小。这可以通过设置 maxUploadSize(value) 来完成,这会限制总体请求大小。

最终编辑

所以,这就是我最终使用的,它可以有效上传和处理超大文件。我不确定这在上传大文件时是否有效,因为这会在将请求转换为 FileItems 之后但在解析所述 FileItems 之前处理超大文件。

我扩展了 CommonsMultipartResolver 来覆盖 parseRequest,如下所示:

@Override
protected MultipartParsingResult parseRequest(HttpServletRequest request) {

    String encoding = determineEncoding(request);
    FileUpload fileUpload = prepareFileUpload(encoding);

    List<FileItem> fileItems;
    List<String> oversizedFields = new LinkedList<>();

    try {
        fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
    } catch (FileUploadBase.SizeLimitExceededException ex) {
        fileItems = Collections.emptyList();
        request.setAttribute(ATTR_REQUEST_SIZE_EXCEEDED,
                KEY_REQUEST_SIZE_EXCEEDED);
    } catch (FileUploadException ex) {
        throw new MultipartException(MULTIPART_UPLOAD_ERROR, ex);
    }
    if (maxFileSize > -1) {
        for (FileItem fileItem : fileItems) {
            if (fileItem.getSize() > maxFileSize) {
                oversizedFields.add(fileItem.getFieldName());
                fileItem.delete();
            }
        }
    }
    if (!oversizedFields.isEmpty()) {
        request.setAttribute(ATTR_FIELDS_OVERSIZED, oversizedFields);
    }
    return parseFileItems((List<FileItem>) fileItems, encoding);
}

并添加了通过 bean 配置设置 maxFileSize 的方法。如果超过请求大小,所有值都将被删除,因此请小心,尤其是在使用 _csrf-token 或类似内容时。

在 Controller 中,现在可以轻松检查添加的属性并将错误消息放置在页面上。

关于java - Spring Boot 下 Undertow/Wildfly 上的文件上传和可接受的错误处理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30352992/

相关文章:

spring - 带有 Spring Integration 的 SOAP 代理

java - Spring boot webflux应用程序不会启动IllegalStateException

java - Spring REST 多部分错误处理

scala - Lagom 中的多部分表单数据

python - 如何使用附加字段 react.js flask 上传多个文件

java - Mapstruct:使用加法器时清除更新集合

java - 如何使用 Gradle 将 jar 文件添加到 libgdx 项目

Spring 贴方法 "Required request body is missing"

java - 如何安装和使用具有受限访问权限的 Apache Spark?

java - 从 camel xmpp 发送消息时出现问题,jid 格式错误