簡體   English   中英

Spring Boot在Undertow / Wildfly上的文件上傳和可接受的錯誤處理

[英]File Upload and acceptable Error Handling on Undertow/Wildfly wth Spring Boot

我們有一個在Undertow和SpringBoot上運行的項目,正在嘗試添加文件上載。 第一次嘗試成功,通過使用StandardServletMultipartResolver並使用application.properties對其進行配置,將文件綁定到了適當的Bean。 但是,在錯誤處理方面,我們遇到了巨大的困難。 通過將標准解析器配置為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);
                }
            }
        }
    };
}

RequestWrapper此類:

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方法檢查文件是否過大,並將結果添加到BindingResult ,該方法非常BindingResult ,除了文件未綁定到Bean的事實。 事實證明, CommonsMultipartResolver在嘗試解析請求時,在ItemInputStream.makeAvailable()拋出MalformedStreamExceptionItemInputStream.makeAvailable()總是返回String ended unexpectedly的消息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();
}

從控制器中提取:

@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字符集

@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()

該問題的解決方案大約是在我尋找它的時候找到的。 它已經張貼在這里

由於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或類似值的情況下。

在控制器中,現在可以輕松檢查添加的屬性並將錯誤消息放置在頁面上。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM