简体   繁体   English

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

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

We have a project running on Undertow & SpringBoot and are trying to add File Uploads. 我们有一个在Undertow和SpringBoot上运行的项目,正在尝试添加文件上载。 The first attempts were successful, the Files have been bound to the appropriate Beans by using the StandardServletMultipartResolver and configuring it using the application.properties . 第一次尝试成功,通过使用StandardServletMultipartResolver并使用application.properties对其进行配置,将文件绑定到了适当的Bean。 However, we ran into terrible difficulties when it came to Error Handling. 但是,在错误处理方面,我们遇到了巨大的困难。 We found a "solution" by configuring the standard resolver to 100MB and using CommonsMultipartResolver . 通过将标准解析器配置为100MB并使用CommonsMultipartResolver我们找到了一个“解决方案”。 We then added a filter like this 然后,我们添加了这样的过滤器

@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: 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);
        }
    }
}

The @Controller -method then checks for oversized Files and adds the result to the BindingResult , which works great, except for the fact that the files are not bound to the bean. 然后, @Controller方法检查文件是否过大,并将结果添加到BindingResult ,该方法非常BindingResult ,除了文件未绑定到Bean的事实。 It turns out that the CommonsMultipartResolver , when trying to parse the request, throws a MalformedStreamException in ItemInputStream.makeAvailable() , which always returns the Message String ended unexpectedly . 事实证明, CommonsMultipartResolver在尝试解析请求时,在ItemInputStream.makeAvailable()抛出MalformedStreamExceptionItemInputStream.makeAvailable()总是返回String ended unexpectedly的消息String ended unexpectedly

So we went back to using the StandardServletMultipartResolver , and were able to catch the RuntimeException it throws just fine, however it delivers absolutely no form data when even one file exceeds its size boundaries. 因此,我们重新使用了StandardServletMultipartResolver ,并且能够捕获它抛出的RuntimeException ,但是当一个文件超出其大小界限时,它绝对不会提供任何表单数据。

We are absolutely stumped as it is no matter if the Resolver works lazily or not. 我们绝对很沮丧,因为无论解析程序是否工作延迟。 If anyone has any further ideas how to resolve this matter, be welcome to suggest answers =) 如果有人对解决此问题有任何进一步的想法,欢迎提出答案=)

Further Code for reference: 进一步的代码供参考:

Extract from WebAppInitializer 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();
}

Extract from 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() so far has no functionality, which is why I'm not including it here. 到目前为止, processUpload()没有任何功能,这就是为什么我不在此包括它的原因。

Extract from Form-Backing Bean: 从表单支持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;
}
}

This is, as stated, not the entire code, but the necessary code for this particular problem. 如前所述,这不是整个代码,而是解决此特定问题的必要代码。 The Exception thrown when uploading oversized files is either: 上载超大文件时抛出的异常是:

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

or the mentioned FileUploadBase.FileSizeLimitExceedeException 或提到的FileUploadBase.FileSizeLimitExceedeException

And last but not least, an extract of the form-page 最后但并非最不重要的一点是表单页面的摘录

<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>

If you followed the problems down here, you should have realized that we already tried several possible solutions, most being from here. 如果您在这里跟踪问题,那么您应该已经意识到我们已经尝试了几种可能的解决方案,其中大多数是从这里开始的。 Right now, the Filter catches RuntimeException and checks for IOException as cause, also, the sizes are no longer set within the application.properties 现在,筛选器捕获RuntimeException并检查IOException作为原因,并且不再在application.properties设置大小。

Any help or suggestions at all would be very much appreciated. 任何帮助或建议都将不胜感激。

More Information 更多信息

So, I debugged the StandardServletMultipartResolver and found that it uses the ISO-8859-1-charset for its parsing. 因此,我调试了StandardServletMultipartResolver ,发现它使用ISO-8859-1-charset进行解析。 This does produce the desired effects, even though the pages are UTF-8 encoded and the request object also hast UTF-8-Charset. 即使页面是UTF-8编码的,并且请求对象也具有UTF-8-Charset,这确实会产生所需的效果。 I have been trying to force the ISO-Charset with a Filter like so 我一直在尝试用这样的过滤器强制ISO字符集

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

but, for some reason, the CommonsMultipartResolver finds a UTF-8 Encoded request object, so either this encoding does not work, or I have made another mistake I don't see. 但是由于某种原因, CommonsMultipartResolver找到一个UTF-8编码的请求对象,因此该编码无法正常工作,或者我犯了另一个我看不到的错误。

I have also tried to find the exact moment of the thrown Exception, to maybe extend the class myself and make sure the already resolved form-data is kept, so far to no avail. 我还试图找到抛出异常的确切时间,也许是我自己扩展该类,并确保保留已经解析的表单数据,但到目前为止仍然无济于事。

Even more Information 更多信息

As suggested by another thread here, I tried to force the ISO-8859-1 charset on the request. 正如此处另一个线程所建议的那样,我尝试对请求强制使用ISO-8859-1字符集。 At first, this completely bypassed the CommonsMultipartResolver and messed up my text, now it filters to the correct resolver, but this one still states that there are no files within the multipart data. 最初,它完全绕过了CommonsMultipartResolver并弄乱了我的文本,现在它过滤到正确的解析器,但是这个仍然指出多部分数据中没有文件。 Just for reference, the Filterclass I used: 仅供参考,我使用了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);
    }
}

Made a Bean from it and changed name of multipartResolver()-Bean to filterMultipartResolver() 用它制作一个Bean,并将multipartResolver()-Bean的名称更改为filterMultipartResolver()

The solution to the problem was found roughly at the time I was looking for it. 该问题的解决方案大约是在我寻找它的时候找到的。 It has been posted here . 它已经张贴在这里

Due to the fact that WildFly and Undertow have difficulties in dealing with the StandardServletMultipartResolver , it is more effective (maybe even necessary) to use the CommonsMultipartResolver . 由于WildFly和Undertow在处理StandardServletMultipartResolver遇到困难,因此使用CommonsMultipartResolver更为有效(甚至有必要)。 However, this must be called before the rest of the POST-data is processed. 但是,必须在处理其余的POST数据之前调用此方法。
To ensure this, it is necessary to call a MultipartFilter and create a filterMultipartResolver -Bean like this: 为了确保这一点,有必要调用MultipartFilter并创建filterMultipartResolver -Bean,如下所示:

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

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

This ensures that the filter is called first, and it in turn calls the resolver. 这样可以确保首先调用过滤器,然后依次调用解析器。 The only downside is that there is no out-of-the-box way to limit the individual filesize for uploads. 唯一的缺点是没有开箱即用的方式来限制要上传的单个文件的大小。 This can be done by setting maxUploadSize(value) , which limits the overall request size. 可以通过设置maxUploadSize(value)来完成,该设置限制了整个请求的大小。

Final Edit 最终编辑

So, here's what I ended up using, which allows for effective upload and handling of oversized files. 因此,这就是我最终使用的功能,可以有效地上传和处理超大文件。 I am not sure if this will be as effective when uploading large files, since this handles the oversized files after converting the request to FileItems but before parsing said FileItems . 我不确定这在上传大文件时是否会有效,因为这会在将请求转换为FileItems但在解析所述FileItems之前处理过大的文件。

I extended the CommonsMultipartResolver to override parseRequest like this: 我将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);
}

and added methods to set maxFileSize via bean configuration. 并添加了通过bean配置设置maxFileSize的方法。 Should the request size be exceeded, all values will be dropped, so be careful with that, especially if you use _csrf-token or similar. 如果超出了请求大小,则所有值都将被丢弃,因此请特别小心,尤其是在使用_csrf-token或类似值的情况下。

In the controller, it is now easy to check for the added attribute and place error messages on the page. 在控制器中,现在可以轻松检查添加的属性并将错误消息放置在页面上。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM