Spring MVC -- 上傳文件

Servlet技術出現之前,文件上傳的編程仍然是一項很困難的任務,它涉及在服務器端解析原始的HTTP響應。爲了減輕編程的痛苦,開發人員藉助於商業的文件上傳組件。值得慶幸的是,2003年,Apache Software Foundation發佈了開源的Commons FileUpload組件,它很快成爲了Java Web應用程序員的利器。css

通過不少年,Servlet的設計人員才意識到文件文件上傳的重要性,並終於成爲Servlet 3.0的內置特性。Servlet 3.0的開發人員再也不須要將Commons FileUpload組件導入到他們的項目中去。html

爲此,在Spring MVC中處理文件上傳有兩種狀況:前端

  • 在Servlet 3.0版本如下,使用Apache Commons FileUpload組件;
  • 在Servlet 30.版本以上,利用Servlet 3.0及其更高版本的內置支持。

不管使用哪一個版本的Servlet,都要利用相同的API來處理已經上傳的文件。本篇博客將會介紹如何在須要支持文件上傳的Spring MVC應用中使用Commons FileUpload和Servlet 3.0文件上傳特性。html5

一 前端編程

爲了上傳文件,必須將HTML表格enctype屬性值設置爲multipart/form-data,像下面這樣:java

<form  action="action" method="post" enctype="multipart/form-data">
   select a file <input type="file" name="fieldName"/>
   <input type="submit" value="Upload"/>
</form>

表格中必須包含類型爲file的一個input元素,它會顯示成一個按鈕,單擊時,它會打開一個對話框,用來選擇文件。程序員

在HTML 5以前,若是想要上傳多個文件,必須使用多個類型爲file的input元素。可是在HTML 5中,經過在input元素中引入multiple屬性,使得多個文件的上傳變得更加簡單。在HTML 5中編寫如下任意一行代碼,即可以生成一個按鈕來選擇多個文件:web

<input type="file" name="fieldName" multiple/>
<input type="file" name="fieldName" multiple="multiple"/>
<input type="file" name="fieldName" multiple=""/>

二 MultipartFile接口

在Spring MVC中處理已經上傳的文件十分容易。上傳到Spring MVC應用程序中的文件會被包含在一個MultipartFile對象中。咱們惟一的任務就是,用類型MultipartFile的屬性編寫一個domain類。spring

org.springframework.web.multipart.MultipartFile接口源代碼以下:express

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;

/**
 * A representation of an uploaded file received in a multipart request.
 *
 * <p>The file contents are either stored in memory or temporarily on disk.
 * In either case, the user is responsible for copying file contents to a
 * session-level or persistent store as and if desired. The temporary storage
 * will be cleared at the end of request processing.
 *
 * @author Juergen Hoeller
 * @author Trevor D. Cook
 * @since 29.09.2003
 * @see org.springframework.web.multipart.MultipartHttpServletRequest
 * @see org.springframework.web.multipart.MultipartResolver
 */
public interface MultipartFile extends InputStreamSource {

    /**
     * Return the name of the parameter in the multipart form.
     * @return the name of the parameter (never {@code null} or empty)
     */
    String getName();

    /**
     * Return the original filename in the client's filesystem.
     * <p>This may contain path information depending on the browser used,
     * but it typically will not with any other than Opera.
     * @return the original filename, or the empty String if no file has been chosen
     * in the multipart form, or {@code null} if not defined or not available
     * @see org.apache.commons.fileupload.FileItem#getName()
     * @see org.springframework.web.multipart.commons.CommonsMultipartFile#setPreserveFilename
     */
    @Nullable
    String getOriginalFilename();

    /**
     * Return the content type of the file.
     * @return the content type, or {@code null} if not defined
     * (or no file has been chosen in the multipart form)
     */
    @Nullable
    String getContentType();

    /**
     * Return whether the uploaded file is empty, that is, either no file has
     * been chosen in the multipart form or the chosen file has no content.
     */
    boolean isEmpty();

    /**
     * Return the size of the file in bytes.
     * @return the size of the file, or 0 if empty
     */
    long getSize();

    /**
     * Return the contents of the file as an array of bytes.
     * @return the contents of the file as bytes, or an empty byte array if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    byte[] getBytes() throws IOException;

    /**
     * Return an InputStream to read the contents of the file from.
     * <p>The user is responsible for closing the returned stream.
     * @return the contents of the file as stream, or an empty stream if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    @Override
    InputStream getInputStream() throws IOException;

    /**
     * Return a Resource representation of this MultipartFile. This can be used
     * as input to the {@code RestTemplate} or the {@code WebClient} to expose
     * content length and the filename along with the InputStream.
     * @return this MultipartFile adapted to the Resource contract
     * @since 5.1
     */
    default Resource getResource() {
        return new MultipartFileResource(this);
    }

    /**
     * Transfer the received file to the given destination file.
     * <p>This may either move the file in the filesystem, copy the file in the
     * filesystem, or save memory-held contents to the destination file. If the
     * destination file already exists, it will be deleted first.
     * <p>If the target file has been moved in the filesystem, this operation
     * cannot be invoked again afterwards. Therefore, call this method just once
     * in order to work with any storage mechanism.
     * <p><b>NOTE:</b> Depending on the underlying provider, temporary storage
     * may be container-dependent, including the base directory for relative
     * destinations specified here (e.g. with Servlet 3.0 multipart handling).
     * For absolute destinations, the target file may get renamed/moved from its
     * temporary location or newly copied, even if a temporary copy already exists.
     * @param dest the destination file (typically absolute)
     * @throws IOException in case of reading or writing errors
     * @throws IllegalStateException if the file has already been moved
     * in the filesystem and is not available anymore for another transfer
     * @see org.apache.commons.fileupload.FileItem#write(File)
     * @see javax.servlet.http.Part#write(String)
     */
    void transferTo(File dest) throws IOException, IllegalStateException;

    /**
     * Transfer the received file to the given destination file.
     * <p>The default implementation simply copies the file input stream.
     * @since 5.1
     * @see #getInputStream()
     * @see #transferTo(File)
      */
    default void transferTo(Path dest) throws IOException, IllegalStateException {
        FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
    }

}
View Code

該接口具備如下方法:apache

    /**
     * Return the contents of the file as an array of bytes.
     * @return the contents of the file as bytes, or an empty byte array if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    byte[] getBytes() throws IOException;

它以字節數組的形式返回文件的內容。

    /**
     * Return the content type of the file.
     * @return the content type, or {@code null} if not defined
     * (or no file has been chosen in the multipart form)
     */
    @Nullable
    String getContentType();

它返回文件的內容類型。

    /**
     * Return an InputStream to read the contents of the file from.
     * <p>The user is responsible for closing the returned stream.
     * @return the contents of the file as stream, or an empty stream if empty
     * @throws IOException in case of access errors (if the temporary store fails)
     */
    @Override
    InputStream getInputStream() throws IOException;

它返回一個InputStream ,從中讀取文件的內容。

    /**
     * Return the name of the parameter in the multipart form.
     * @return the name of the parameter (never {@code null} or empty)
     */
    String getName();

它以多部分的形式返回參數的名稱。

    /**
     * Return the original filename in the client's filesystem.
     * <p>This may contain path information depending on the browser used,
     * but it typically will not with any other than Opera.
     * @return the original filename, or the empty String if no file has been chosen
     * in the multipart form, or {@code null} if not defined or not available
     * @see org.apache.commons.fileupload.FileItem#getName()
     * @see org.springframework.web.multipart.commons.CommonsMultipartFile#setPreserveFilename
     */
    @Nullable
    String getOriginalFilename();

它返回客戶端文件系統中文件的原始文件名稱。

    /**
     * Return the size of the file in bytes.
     * @return the size of the file, or 0 if empty
     */
    long getSize();

它以字節爲單位,返回文件的大小。

    /**
     * Transfer the received file to the given destination file.
     * <p>This may either move the file in the filesystem, copy the file in the
     * filesystem, or save memory-held contents to the destination file. If the
     * destination file already exists, it will be deleted first.
     * <p>If the target file has been moved in the filesystem, this operation
     * cannot be invoked again afterwards. Therefore, call this method just once
     * in order to work with any storage mechanism.
     * <p><b>NOTE:</b> Depending on the underlying provider, temporary storage
     * may be container-dependent, including the base directory for relative
     * destinations specified here (e.g. with Servlet 3.0 multipart handling).
     * For absolute destinations, the target file may get renamed/moved from its
     * temporary location or newly copied, even if a temporary copy already exists.
     * @param dest the destination file (typically absolute)
     * @throws IOException in case of reading or writing errors
     * @throws IllegalStateException if the file has already been moved
     * in the filesystem and is not available anymore for another transfer
     * @see org.apache.commons.fileupload.FileItem#write(File)
     * @see javax.servlet.http.Part#write(String)
     */
    void transferTo(File dest) throws IOException, IllegalStateException;

它將上傳的文件保存到目標目錄下。

    /**
     * Return whether the uploaded file is empty, that is, either no file has
     * been chosen in the multipart form or the chosen file has no content.
     */
    boolean isEmpty();

它表示被上傳的文件是否爲空(沒有上傳文件、或者文件內容爲空)。

三 使用Commons Fileupload組件上傳文件

只有實現了Servlet 3.0及其更高版本規範的Servlet容器,才支持文件上傳。對於版本低於Servlet 3.0的容器,則須要Apache Commons Fileupload組件,commons-fileupload.jar包的下載路徑以下:https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload

這是一個開源項目,所以是免費的,它會提供了源代碼。爲了讓Commons Fileupload可以運行,還須要一個Apache Commins組件commons-io.jar,commons-io.jar包的下載路徑以下:https://mvnrepository.com/artifact/commons-io/commons-io

下載完這兩個JAR包,咱們還須要作如下工做:

  • 將這兩個JAR文件複製到應用程序的/WEB-INF/lib路徑下;
  • 在Spring MVC配置文件中定義multipartResolver bean;
    <bean id="multipartResolver"
            class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="5000000000"/>
    </bean>

CommonsMultipartResolver類,實際上就是將org.apache.commons.fileupload.servlet.ServletFileUpload類和org.apache.commons.fileupload.disk.DiskFileItemFactory的功能進行了整合,具體代碼以下:

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.multipart.commons;

import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import org.springframework.util.Assert;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest;
import org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest;
import org.springframework.web.util.WebUtils;

/**
 * Servlet-based {@link MultipartResolver} implementation for
 * <a href="https://commons.apache.org/proper/commons-fileupload">Apache Commons FileUpload</a>
 * 1.2 or above.
 *
 * <p>Provides "maxUploadSize", "maxInMemorySize" and "defaultEncoding" settings as
 * bean properties (inherited from {@link CommonsFileUploadSupport}). See corresponding
 * ServletFileUpload / DiskFileItemFactory properties ("sizeMax", "sizeThreshold",
 * "headerEncoding") for details in terms of defaults and accepted values.
 *
 * <p>Saves temporary files to the servlet container's temporary directory.
 * Needs to be initialized <i>either</i> by an application context <i>or</i>
 * via the constructor that takes a ServletContext (for standalone usage).
 *
 * @author Trevor D. Cook
 * @author Juergen Hoeller
 * @since 29.09.2003
 * @see #CommonsMultipartResolver(ServletContext)
 * @see #setResolveLazily
 * @see org.apache.commons.fileupload.servlet.ServletFileUpload
 * @see org.apache.commons.fileupload.disk.DiskFileItemFactory
 */
public class CommonsMultipartResolver extends CommonsFileUploadSupport
        implements MultipartResolver, ServletContextAware {

    private boolean resolveLazily = false;


    /**
     * Constructor for use as bean. Determines the servlet container's
     * temporary directory via the ServletContext passed in as through the
     * ServletContextAware interface (typically by a WebApplicationContext).
     * @see #setServletContext
     * @see org.springframework.web.context.ServletContextAware
     * @see org.springframework.web.context.WebApplicationContext
     */
    public CommonsMultipartResolver() {
        super();
    }

    /**
     * Constructor for standalone usage. Determines the servlet container's
     * temporary directory via the given ServletContext.
     * @param servletContext the ServletContext to use
     */
    public CommonsMultipartResolver(ServletContext servletContext) {
        this();
        setServletContext(servletContext);
    }


    /**
     * Set whether to resolve the multipart request lazily at the time of
     * file or parameter access.
     * <p>Default is "false", resolving the multipart elements immediately, throwing
     * corresponding exceptions at the time of the {@link #resolveMultipart} call.
     * Switch this to "true" for lazy multipart parsing, throwing parse exceptions
     * once the application attempts to obtain multipart files or parameters.
     */
    public void setResolveLazily(boolean resolveLazily) {
        this.resolveLazily = resolveLazily;
    }

    /**
     * Initialize the underlying {@code org.apache.commons.fileupload.servlet.ServletFileUpload}
     * instance. Can be overridden to use a custom subclass, e.g. for testing purposes.
     * @param fileItemFactory the Commons FileItemFactory to use
     * @return the new ServletFileUpload instance
     */
    @Override
    protected FileUpload newFileUpload(FileItemFactory fileItemFactory) {
        return new ServletFileUpload(fileItemFactory);
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        if (!isUploadTempDirSpecified()) {
            getFileItemFactory().setRepository(WebUtils.getTempDir(servletContext));
        }
    }


    @Override
    public boolean isMultipart(HttpServletRequest request) {
        return ServletFileUpload.isMultipartContent(request);
    }

    @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 {
            MultipartParsingResult parsingResult = parseRequest(request);
            return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
                    parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
        }
    }

    /**
     * Parse the given servlet request, resolving its multipart elements.
     * @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 fileUpload = prepareFileUpload(encoding);
        try {
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            return parseFileItems(fileItems, encoding);
        }
        catch (FileUploadBase.SizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
        }
        catch (FileUploadBase.FileSizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
        }
        catch (FileUploadException ex) {
            throw new MultipartException("Failed to parse multipart servlet request", ex);
        }
    }

    /**
     * Determine the encoding for the given request.
     * Can be overridden in subclasses.
     * <p>The default implementation checks the request encoding,
     * falling back to the default encoding specified for this resolver.
     * @param request current HTTP request
     * @return the encoding for the request (never {@code null})
     * @see javax.servlet.ServletRequest#getCharacterEncoding
     * @see #setDefaultEncoding
     */
    protected String determineEncoding(HttpServletRequest request) {
        String encoding = request.getCharacterEncoding();
        if (encoding == null) {
            encoding = getDefaultEncoding();
        }
        return encoding;
    }

    @Override
    public void cleanupMultipart(MultipartHttpServletRequest request) {
        if (!(request instanceof AbstractMultipartHttpServletRequest) ||
                ((AbstractMultipartHttpServletRequest) request).isResolved()) {
            try {
                cleanupFileItems(request.getMultiFileMap());
            }
            catch (Throwable ex) {
                logger.warn("Failed to perform multipart cleanup for servlet request", ex);
            }
        }
    }

}
View Code

multipartResolver 對象則經過配置property元素來調用setter方法以設置屬性值。咱們能夠經過setter方式注入的屬性有:

  • maxUploadSize:控制上傳單個文件的大小,單位是字節;
  • maxInMemorySize:設置上傳文件時用到的臨時文件的大小,單位是字節;
  • defaultEncoding:請求參數的默認編碼方式。

這些屬性被用來對上傳文件進行設置。

此外,CommonsMultipartResolver類的還有一個很是重要的函數:

  /**
     * Parse the given servlet request, resolving its multipart elements.
     * @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 fileUpload = prepareFileUpload(encoding);
        try {
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            return parseFileItems(fileItems, encoding);
        }
        catch (FileUploadBase.SizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
        }
        catch (FileUploadBase.FileSizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
        }
        catch (FileUploadException ex) {
            throw new MultipartException("Failed to parse multipart servlet request", ex);
        }
    }

經過parseRequest()函數解析form中的全部請求字段,並保存到List<FileItem>集合中,而後將集合轉換爲MultipartParsingResult類型返回:

    /**
     * Holder for a Map of Spring MultipartFiles and a Map of
     * multipart parameters.
     */
    protected static class MultipartParsingResult {

        private final MultiValueMap<String, MultipartFile> multipartFiles;

        private final Map<String, String[]> multipartParameters;

        private final Map<String, String> multipartParameterContentTypes;

        public MultipartParsingResult(MultiValueMap<String, MultipartFile> mpFiles,
                Map<String, String[]> mpParams, Map<String, String> mpParamContentTypes) {

            this.multipartFiles = mpFiles;
            this.multipartParameters = mpParams;
            this.multipartParameterContentTypes = mpParamContentTypes;
        }

        public MultiValueMap<String, MultipartFile> getMultipartFiles() {
            return this.multipartFiles;
        }

        public Map<String, String[]> getMultipartParameters() {
            return this.multipartParameters;
        }

        public Map<String, String> getMultipartParameterContentTypes() {
            return this.multipartParameterContentTypes;
        }
    }
View Code

MultipartParsingResult類有個重要的屬性:

private final MultiValueMap<String, MultipartFile> multipartFiles;

該Map的鍵值爲String類型,保存的是表單類型爲file的input元素的name屬性值,值爲MultipartFile接口類型,該類型保存了該input元素對應的上傳文件。

四 Servlet 3.0如下版本文件上傳示例

範例upload1展現瞭如何利用Apache Commons FileUpload處理已經上傳的文件。這個範例在Servlet 3.0容器中也是有效的。upload1有一個domain包,包含Procudt類,它包含了一個MultipartFile對象列表。該示例介紹瞭如何進行產品圖片的上傳。

一、目錄結構

下面展現upload1應用的目錄結構:

注意:在lib中咱們須要導入Apache Commons FileUpload組件。

二、Product類

Product類具備類型爲List<MultipartFile>的imagea屬性,這個屬性用來保存上傳的產品圖片文件(能夠是多個圖片文件):

package domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.web.multipart.MultipartFile;

public class Product implements Serializable {
    private static final long serialVersionUID = 1;

    @NotNull
    @Size(min=1, max=10)
    private String name;

    private String description;
    private BigDecimal price;
    private List<MultipartFile> images;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    public List<MultipartFile> getImages() {
        return images;
    }
    public void setImages(List<MultipartFile> images) {
        this.images = images;
    }
}

三、控制器

package controller;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import domain.Product;

@Controller
public class ProductController {

    private static final Log logger = LogFactory
            .getLog(ProductController.class);

    //請求URL:/input-product 
    @RequestMapping(value = "/input-product")
    public String inputProduct(Model model) {
        model.addAttribute("product", new Product());
        return "ProductForm";
    }

    //請求URL:/save-product
    @RequestMapping(value = "/save-product")
    public String saveProduct(HttpServletRequest servletRequest,
            @ModelAttribute Product product, BindingResult bindingResult,
            Model model) {

        //獲取上傳的圖片文件(能夠多個文件)
        List<MultipartFile> files = product.getImages();

        //用於保存全部文件名
        List<String> fileNames = new ArrayList<String>();

        //檢驗是否有文件?
        if (null != files && files.size() > 0) {
            //遍歷
            for (MultipartFile multipartFile : files) {
                //獲取文件名
                String fileName = multipartFile.getOriginalFilename();
                fileNames.add(fileName);
                
                //獲取應用/image虛擬路徑在文件系統上對應的真實路徑 + 文件名  並建立File對象
                File imageFile = new File(servletRequest.getServletContext()
                        .getRealPath("/image"), fileName);
                try {
                    //將上傳的文件保存到目標目錄下
                    multipartFile.transferTo(imageFile);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // save product here
        model.addAttribute("product", product);
        return "ProductDetails";
    }

}

ProductController類有inputProduct()和saveProduct()兩個請求處理方法。inputProduct()方法向瀏覽器發出一個產品表單,saveProduct()方法將已經上傳的圖片文件保存到應用程序的image目錄下,文件名不改變。

注意:必須先建立好image文件夾。

四、配置文件

下面給出springmvc-config.xml文件的全部內容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd     
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="controller" />
    <mvc:annotation-driven />

    <mvc:resources mapping="/css/**" location="/css/" />
    <mvc:resources mapping="/*.html" location="/" />
    <mvc:resources mapping="/image/**" location="/image/" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
    <bean id="multipartResolver"
            class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="5000000000"/>
    </bean>

</beans>

利用multipartResolver bean的maxUploadSize屬性,能夠設置可以接受的最大文件容量。若是沒有設置這個屬性,則沒有最大文件容量限制。沒有設置文件容量限制,並不意味着能夠上傳任意大小的文件。上傳過大的文件時須要花費很長的時間,這樣會致使服務器超時,爲了處理超大文件的問題,能夠利用HTML 5 File API將文件切片,而後再分別上傳這些文件。

若是想對上傳的文件類型進行過濾,那麼咱們能夠須要先獲取上傳文件的名稱,而後檢測其擴展名。此外,咱們也能夠在前端使用js代碼檢測上傳文件的擴展名。

部署描述符(web.xml文件):

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" 
        xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
            http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> 
            
     <!-- 配置編碼方式過濾器,注意一點:要配置在全部過濾器的前面 -->
     <filter>
       <filter-name>CharacterEncodingFilter</filter-name>
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
       <init-param>
          <param-name>encoding</param-name>
          <param-value>utf-8</param-value>
       </init-param>
     </filter>
     <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
     </filter-mapping>
  
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/config/springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>    
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

注意,web.xml文件中咱們配置了編碼方式過濾器,將全部http請求的參數編碼爲UTF-8方式,與jsp中頁面編碼一致。

五、視圖

用於上傳圖片文件的ProductForm.jsp頁面以下所示:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>

<div id="global">
<form:form modelAttribute="product" action="save-product" method="post" enctype="multipart/form-data">
    <fieldset>
        <legend>Add a product</legend>
        <p>
            <label for="name">Product Name: </label>
            <form:input id="name" path="name" cssErrorClass="error"/>
            <form:errors path="name" cssClass="error"/>
        </p>
        <p>
            <label for="description">Description: </label>
            <form:input id="description" path="description"/>
        </p>
        <p>
            <label for="price">Price: </label>
            <form:input id="price" path="price" cssErrorClass="error"/>
        </p>
        <p>
            <label for="image">Product Image: </label>
            <input type="file" name="images[0]"/>
        </p>
        <p id="buttons">
            <input id="reset" type="reset" tabindex="4">
            <input id="submit" type="submit" tabindex="5" 
                value="Add Product">
        </p>
    </fieldset>
</form:form>
</div>
</body>
</html>

注意表單中類型爲file的input元素,它將顯示爲一個按鈕,用於選擇要上傳的文件。而且input元素的name屬性指定爲"images[0]",即綁定到表單支持對象product的images屬性(List<MultipartFile>類型>)的第一個元素上。

若是想支持多個文件同時上傳,只需將  <input type="file" name="images[0]"/>替換成以下:

  <input type="file" name="images" multiple/>

提交Product表單,將會調用saveProduct()方法,若是這個方法可以順利執行,用戶將會跳轉到ProductDetails.jsp頁面:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Save Product</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
    <h4>The product has been saved.</h4>
    <p>
        <h5>Details:</h5>
        Product Name: ${product.name}<br/>
        Description: ${product.description}<br/>
        Price: $${product.price}
        <p>Following files are uploaded successfully.</p>
        <ol>
        <c:forEach items="${product.images}" var="image">
            <li>${image.originalFilename}
            <img width="100" src="<c:url value="/image/"/>
            ${image.originalFilename}"/>
            </li>
        </c:forEach>
        </ol>
    </p>
</div>
</body>
</html>

該頁面將會顯示已經保存的Product的詳細信息及其圖片。

六、測試

將應用程序部署到tomcat服務器,並在網頁輸入如下URL:

http://localhost:8008/upload1/input-product

將會看到一個如圖所示的Add Product表單,試着輸入一些產品信息,並選擇一個要上傳的文件:

單擊"Add Product"按鈕,就能夠看到以下所示的網頁:

 

同時咱們能夠在tomcat服務器,upload1應用下的image目錄下看到,已經上傳的文件:

若是將ProductForm.jsp中的:<input type="file" name="images[0]"/>更改成以下代碼:

<input type="file" name="images" multiple/>

那麼就能夠實現多個文件同時上傳:

五 Servlet 3.0及其更高版本上傳文件

有了Servlet 3.0就不須要Common FileUpload和Common IO JAR包了。在Servlet 3.0及其以上版本的容器進行服務器端文件上傳的編程,是圍繞着註解類型MultipartConfig和javax.servlet.http.Part接口進行的。處理已上傳文件的Servlets必須以@MultipartConfig進行註解。

下列是可能在MultipartConfig註解類型中出現的屬性,它們都是可選的:

  • maxFileSize:單個上傳文件的最大容量,默認值是-1,表示沒有限制,大於指定容量的文件將會遭到拒絕;
  • maxRequestSize:表示Multipart HTTP請求運行的最大容量,默認值爲-1,表示沒有限制;
  • location:表示在Part調用write()方法時,要將已上傳的文件保存到磁盤中的位置;
  • fileSizeThreshod:設置上傳文件時用到的臨時文件的大小;

Spring MVC的DispatcherServlet處理大部分或者全部請求。可是遺憾的是,若是不修改源代碼,將沒法對Servlet進行註解。但值得慶幸的是,Servlet 3.0中有一種比較容易的方法,能使一個Servlet變成一個MultipartConfig Servlet,即給部署描述符(web.xml)中的Servlet聲明賦值。如下代碼與用@MultipartConfig給DispatcherServlet進行註解的效果同樣:

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/config/springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
 <multipart-config> <max-file-size>20848820</max-file-size> <max-request-size>418018841</max-request-size> <file-size-threshold>1048576</file-size-threshold> </multipart-config>         
    </servlet>

此外,還須要在Spring MVC配置文件中使用一個StandardServletMultipartResolver,以下:

    <bean id="multipartResolver"
        class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
    </bean>

upload2應用程序展現瞭如何在Servlet 3.0以及更改版本的容器中處理文件上傳問題,這是從upload1中改寫過來的,upload2和upload1類似部分再也不作詳細介紹。主要的區別在於,如今的web.xml文件中包含了一個multipart-config元素。upload2應用的目錄結構以下:

一、配置文件

下面是upload2應用的部署描述符(web.xml文件):

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" 
        xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
            http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> 
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/config/springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
 <multipart-config> <max-file-size>20848820</max-file-size> <max-request-size>418018841</max-request-size> <file-size-threshold>1048576</file-size-threshold> </multipart-config>         
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Spring MVC配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd     
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="controller" />
    <mvc:annotation-driven />

    <mvc:resources mapping="/css/**" location="/css/" />
    <mvc:resources mapping="/*.html" location="/" />
    <mvc:resources mapping="/image/**" location="/image/" />
    <mvc:resources mapping="/file/**" location="/file/" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>   
    
     <bean id="multipartResolver"
        class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
    </bean>

</beans>

二、Produtct類

package domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.springframework.web.multipart.MultipartFile;

public class Product implements Serializable {
    private static final long serialVersionUID = 78L;

    @NotNull
    @Size(min=1, max=10)
    private String name;

    private String description;
    private BigDecimal price;
    private List<MultipartFile> images;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    public List<MultipartFile> getImages() {
        return images;
    }
    public void setImages(List<MultipartFile> images) {
        this.images = images;
    }
}

三、ProductController類

package controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import domain.Product;

@Controller

public class ProductController {
    
    private static final Log logger = LogFactory.getLog(ProductController.class);
    
     //請求URL:/input-product
    @RequestMapping(value="/input-product")
    public String inputProduct(Model model) {
        model.addAttribute("product", new Product());
        return "ProductForm";
    }

    //請求URL:/save-product
    @RequestMapping(value = "/save-product")
    public String saveProduct(HttpServletRequest servletRequest,
            @ModelAttribute Product product, BindingResult bindingResult,
            Model model) {

        //獲取上傳的圖片文件(能夠多個文件)
        List<MultipartFile> files = product.getImages();

        //用於保存全部文件名
        List<String> fileNames = new ArrayList<String>();

        //檢驗是否有文件?
        if (null != files && files.size() > 0) {
            //遍歷
            for (MultipartFile multipartFile : files) {
                //獲取文件名
                String fileName = multipartFile.getOriginalFilename();
                fileNames.add(fileName);

                //獲取應用/image虛擬路徑在文件系統上對應的真實路徑 + 文件名  並建立File對象
                File imageFile = new File(servletRequest.getServletContext()
                        .getRealPath("/image"), fileName);
                try {
                    //將上傳的文件保存到目標目錄下
                    multipartFile.transferTo(imageFile);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // save product here
        model.addAttribute("product", product);
        return "ProductDetails";
    }
    
}

四、視圖

ProductForm.jsp:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>

<div id="global">
<form:form modelAttribute="product" action="save-product" method="post" enctype="multipart/form-data">
    <fieldset>
        <legend>Add a product</legend>
        <p>
            <label for="name">Product Name: </label>
            <form:input id="name" path="name" cssErrorClass="error"/>
            <form:errors path="name" cssClass="error"/>
        </p>
        <p>
            <label for="description">Description: </label>
            <form:input id="description" path="description"/>
        </p>
        <p>
            <label for="price">Price: </label>
            <form:input id="price" path="price" cssErrorClass="error"/>
        </p>
        <p>
            <label for="image">Product Image: </label>
            <!-- <input type="file" name="images[0]"/> -->
            <input type="file" name="images" multiple/>
        </p>
        <p id="buttons">
            <input id="reset" type="reset" tabindex="4">
            <input id="submit" type="submit" tabindex="5" 
                value="Add Product">
        </p>
    </fieldset>
</form:form>
</div>
</body>
</html>

ProductDetails:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<title>Save Product</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
    <h4>The product has been saved.</h4>
    <p>
        <h5>Details:</h5>
        Product Name: ${product.name}<br/>
        Description: ${product.description}<br/>
        Price: $${product.price}
        <p>Following files are uploaded successfully.</p>
        <ol>
        <c:forEach items="${product.images}" var="image">
            <li>${image.originalFilename}
            <img width="100" src="<c:url value="/image/"/>${image.originalFilename}"/>
            </li>
        </c:forEach>
        </ol>
    </p>
</div>
</body>
</html>
View Code

main.css:

#global {
    text-align: left;
    border: 1px solid #dedede;
    background: #efefef;
    width: 560px;
    padding: 20px;
    margin: 30px auto;
}

form {
  font:100% verdana;
  min-width: 500px;
  max-width: 600px;
  width: 560px;
}

form fieldset {
  border-color: #bdbebf;
  border-width: 3px;
  margin: 0;
}

legend {
    font-size: 1.3em;
}

form label { 
    width: 250px;
    display: block;
    float: left;
    text-align: right;
    padding: 2px;
}

#buttons {
    text-align: right;
}
#errors, li {
    color: red;
}
.error {
    color: red;
    font-size: 9pt;    
}
View Code

五、測試

將應用程序部署到tomcat服務器,並在網頁輸入如下URL:

http://localhost:8008/upload2/input-product

將會看到一個如圖所示的Add Product表單,試着輸入一些產品信息,並選擇一個要上傳的文件:

單擊"Add Product"按鈕,就能夠看到以下所示的網頁:

六 upload2應用HTML 5進行文件上傳

雖然Servlet 3.0中的文件上傳特性使文件上傳變得十分容器,只需在服務器端編程便可,可是這對提高用戶體驗毫無幫助。單獨一個HTML表單並不能顯示進度條,或者顯示已經成功上傳的文件數量。開發人員採用了各類不一樣的技術來改善用戶界面,例如,單獨用一個瀏覽器線程對服務器發出請求,以便報告上傳進度,或者利用像Java applets、Adobe Flash、Microsoft Silverlight這樣的第三方技術。

這些第三方技術能夠工做,但都在必定程度上存在限制。今天Java applets和Microsoft  Silverlight幾乎過期了,Chrome不在容許Java applets和Microsoft  Silverlight,Microsoft取代Internet Explorer的新瀏覽器Edge根本不須要插件。

咱們仍然可使用Flash、由於Chrome仍然能夠運行它,Edge已經集成了它,然而,如今愈來愈多的人選擇使用HTML 5。

HTML 5在其DOM中添加了一個File API,它容許訪問本地文件。與Java applets、Adobe Flash、Microsoft Silverlight相比,HTML 5彷佛是針對客戶端文件上傳侷限性的最佳解決方案。

爲了驗證HTML 5的性能,upload2中的html5頁面採用了JavaScript和HTML 5 File API來提供報告上傳進度的進度條。upload2應用程序中也建立了一個UploadedFile 類,用於在服務器中保存已上傳的文件。

一、UploadedFile類

upload2的UploadedFile類只包含一個屬性multipartFile,用來保存已經上傳的文件:

package domain;
import java.io.Serializable;

import org.springframework.web.multipart.MultipartFile;

public class UploadedFile implements Serializable {
    private static final long serialVersionUID = 1L;

    //用來保存已經上傳的文件
    private MultipartFile multipartFile;
    
    public MultipartFile getMultipartFile() {
        return multipartFile;
    }
    public void setMultipartFile(MultipartFile multipartFile) {
        this.multipartFile = multipartFile;
    }
}

二、Html5FileUploadController類

upload2中的Html5FileUploadController類可以將已經上傳的文件保存到應用程序的file目錄下:

package controller;

import java.io.File;
import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import domain.UploadedFile;

@Controller
public class Html5FileUploadController {

    private static final Log logger = LogFactory
            .getLog(Html5FileUploadController.class);

    //請求URL:/html5
    @RequestMapping(value = "/html5")
    public String inputProduct() {
        return "Html5";
    }

    //請求URL:/upload-file
    @RequestMapping(value = "/upload-file")
    public void saveFile(HttpServletRequest servletRequest,
            @ModelAttribute UploadedFile uploadedFile,
            BindingResult bindingResult, Model model) {
        
        //獲取已經上傳的文件
        MultipartFile multipartFile = uploadedFile.getMultipartFile();
        //獲取上傳的文件名
        String fileName = multipartFile.getOriginalFilename();
        try {
            //獲取應用/file虛擬路徑在文件系統上對應的真實路徑 + 文件名  並建立File對象
            File file = new File(servletRequest.getServletContext()
                    .getRealPath("/file"), fileName);
          //將上傳的文件保存到目標目錄下
            multipartFile.transferTo(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、html5.jsp頁面

<!DOCTYPE html>
<html>
<head>
<script>
    var totalFileLength, totalUploaded, fileCount, filesUploaded;

    // show uoload file information use element of id="debug"
    function debug(s) {
        var debug = document.getElementById('debug');
        if (debug) {
            debug.innerHTML = debug.innerHTML + '<br/>' + s;
        }
    }

    //load event of XMLHttpRequest object
    function onUploadComplete(e) {
        totalUploaded += document.getElementById('files').
                files[filesUploaded].size;
        filesUploaded++;
        debug('complete ' + filesUploaded + " of " + fileCount);
        debug('totalUploaded: ' + totalUploaded);        
        if (filesUploaded < fileCount) {
            uploadNext();
        } else {
            var bar = document.getElementById('bar');
            bar.style.width = '100%';
            bar.innerHTML = '100% complete';
            alert('Finished uploading file(s)');
        }
    }
    
    //trigger when selecting file change    
    function onFileSelect(e) {
        var files = e.target.files; // FileList object
        var output = [];
        //get upload file count
        fileCount = files.length;
        totalFileLength = 0;
        for (var i=0; i<fileCount; i++) {
            var file = files[i];
            output.push(file.name, ' (',
                  file.size, ' bytes, ',
                  file.lastModifiedDate.toLocaleDateString(), ') '
            );
            output.push('<br/>');
            debug('add  ' + file.name + ' ('+ file.size  + 'bytes' + ') ' );
            totalFileLength += file.size;
        }
        //show selecting file information
        document.getElementById('selectedFiles').innerHTML = 
            output.join('');
        debug('totalFileLength: ' + totalFileLength + 'bytes');
    }

    //progress event of XMLHttpRequest object
    function onUploadProgress(e) {
        if (e.lengthComputable) {
            var percentComplete = parseInt(
                    (e.loaded + totalUploaded) * 100 
                    / totalFileLength);
            var bar = document.getElementById('bar');
            bar.style.width = percentComplete + '%';
            bar.innerHTML = percentComplete + ' % complete';
        } else {
            debug('unable to compute');
        }
    }

    //error event of XMLHttpRequest object
    function onUploadFailed(e) {
        alert("Error uploading file");
    }
    
    //upload next file
    function uploadNext() {
        var xhr = new XMLHttpRequest();
        var fd = new FormData();
        var file = document.getElementById('files').
                files[filesUploaded];
        fd.append("multipartFile", file);
        xhr.upload.addEventListener(
                "progress", onUploadProgress, false);
        xhr.addEventListener("load", onUploadComplete, false);
        xhr.addEventListener("error", onUploadFailed, false);
        xhr.open("POST", "upload-file");
        debug('uploading ' + file.name);
        xhr.send(fd);
    }

    //trigger when  click Upload button 
    function startUpload() {
        totalUploaded = filesUploaded = 0;
        uploadNext();
    }
    
    //trigger when  window load
    window.onload = function() {
        document.getElementById('files').addEventListener(
                'change', onFileSelect, false);
        document.getElementById('uploadButton').
                addEventListener('click', startUpload, false);
    }
</script>
</head>
<body>
<h1>Multiple file uploads with progress bar</h1>
<div id='progressBar' style='height:20px;border:2px solid green'>
    <div id='bar' 
            style='height:100%;background:#33dd33;width:0%'>
    </div>
</div>
<form>
    <input type="file" id="files" multiple/>
    <br/>
    <output id="selectedFiles"></output>
    <input id="uploadButton" type="button" value="Upload"/>
</form>
<div id='debug' 
    style='height:300px;border:2px solid green;overflow:auto'>
</div>
</body>
</html>

html5.jsp頁面主要包含如下三部分:

  • 一個id爲progressBar的div元素:用於展現上傳進度;
  • 一個表單:表單中有一個類型爲file的input元素和一個按鈕,用於上傳文件;
  • 一個id爲debug的div元素,用來顯示調試信息,主要包括上傳文件信息;

這個表單有兩點須要注意:

<form>
    <input type="file" id="files" multiple/>
    <br/>
    <output id="selectedFiles"></output>
    <input id="uploadButton" type="button" value="Upload"/>
</form>
  • id爲files的input元素,它有一個multiple屬性,用於支持多文件選擇;
  • 這個按鈕不是一個提交按鈕,所以單擊它不會提交表單,事實上,腳本是利用XMLHttpRequest對象來上傳的;

下面來看JavaScript代碼。執行腳本時,它作的第一件事就是爲這4個變量分配空間:

 var totalFileLength, totalUploaded, fileCount, filesUploaded;

(1) totalFileLength變量保存要上傳的文件總長度;

(2) totalUploaded是指目前已經上傳的字節數;

(3) fileCount:要上傳的文件數量;

(4) filesUploaded:表示已經上傳的文件數量;

隨後,當html5.jsp頁面徹底加載後,便觸發window.onload事件:

    window.onload = function() {
        document.getElementById('files').addEventListener(
                'change', onFileSelect, false);
        document.getElementById('uploadButton').
                addEventListener('click', startUpload, false);
    }

這段代碼將id爲files的input元素的change事件映射到onFileSelect()函數,將按鈕的click事件映射到startUpload()函數。

每當用戶從本地目錄中修改了不一樣的文件時,都會觸發change事件。與該事件相關的事件處理器onFileSelect()函數只是在一個id爲selectedFiles的output元素中輸出已選中的文件的名稱和數量:

    //trigger when selecting file change    
    function onFileSelect(e) {
        var files = e.target.files; // FileList object
        var output = [];
        //get upload file count
        fileCount = files.length;
        totalFileLength = 0;
        for (var i=0; i<fileCount; i++) {
            var file = files[i];
            output.push(file.name, ' (',
                  file.size, ' bytes, ',
                  file.lastModifiedDate.toLocaleDateString(), ') '
            );
            output.push('<br/>');
            debug('add  ' + file.name + ' ('+ file.size  + 'bytes' + ') ' );
            totalFileLength += file.size;
        }
        //show selecting file information
        document.getElementById('selectedFiles').innerHTML = 
            output.join('');
        debug('totalFileLength: ' + totalFileLength + 'bytes');
    }

當用戶點擊Upload按鈕時,就會調用startUpload()函數:

    //trigger when  click Upload button 
    function startUpload() {
        totalUploaded = filesUploaded = 0;
        uploadNext();
    }

並隨着調用uploadNext()函數,uploadNext()函數上傳已選文件列表中的下一個文件。它首先建立一個XMLHttpRequest對象和一個FormData對象(表單對象),並將接下來經過document.getElementById('files')獲取一個FileList對象,並將要上傳的文件添加到屬性multipartFile上:

        var xhr = new XMLHttpRequest();
        var fd = new FormData();
        var file = document.getElementById('files').
                files[filesUploaded];
        fd.append("multipartFile", file);

隨後,uploadNext()函數將XMLHttpRequest對象的progress事件綁定添加到onUploadProgress(),並將load事件和error時間分別添加到onUploadComplete()和onUploadFalied:

        xhr.upload.addEventListener(
                "progress", onUploadProgress, false);
        xhr.addEventListener("load", onUploadComplete, false);
        xhr.addEventListener("error", onUploadFailed, false);

接下來,打開一個服務器鏈接,請求/upload-file頁面,併發出FormData:

        xhr.open("POST", "upload-file");
        debug('uploading ' + file.name);
        xhr.send(fd);

fd是一個表單對象,該對象的各個屬性會被綁定到/upload-file頁面對應的請求處理方法saveFile()的模型參數uploadedFile的各個屬性上:

public void saveFile(HttpServletRequest servletRequest,
            @ModelAttribute UploadedFile uploadedFile,
            BindingResult bindingResult, Model model)

在上傳期間,會重複的調用onUploadProgress()函數,讓它有機會更新進度條。更新包括計算已經上傳的總字節比率,計算已選擇文件的字節數,拓寬progressBar div元素裏面的div元素:

    //progress event of XMLHttpRequest object
    function onUploadProgress(e) {
        if (e.lengthComputable) {
            var percentComplete = parseInt(
                    (e.loaded + totalUploaded) * 100 
                    / totalFileLength);
            var bar = document.getElementById('bar');
            bar.style.width = percentComplete + '%';
            bar.innerHTML = percentComplete + ' % complete';
        } else {
            debug('unable to compute');
        }
    }

上傳完成時,調用onUploadComplete()函數。這個事件處理器會添加totalUploaded,即已經完成上傳的文件容量,並添加filesUploaded值。隨後,它會查看已經選中的全部文件是否都已經上傳,若是是,則會顯示一條消息,告訴用戶文件上傳已經成功完成,若是不是,則再次調用uploadNext():

    //load event of XMLHttpRequest object
    function onUploadComplete(e) {
        totalUploaded += document.getElementById('files').
                files[filesUploaded].size;
        filesUploaded++;
        debug('complete ' + filesUploaded + " of " + fileCount);
        debug('totalUploaded: ' + totalUploaded);        
        if (filesUploaded < fileCount) {
            uploadNext();
        } else {
            var bar = document.getElementById('bar');
            bar.style.width = '100%';
            bar.innerHTML = '100% complete';
            alert('Finished uploading file(s)');
        }
    }

若是上傳失敗,則會調用onUploadFailed()函數,而且顯示一條消息:

    //error event of XMLHttpRequest object
    function onUploadFailed(e) {
        alert("Error uploading file");
    }

四、測試

 在瀏覽器中中輸入以下URL:

http://localhost:8008/upload2/html5

選中幾個文件,並單擊Upload按鈕,將會看到一個進度條、以及文件上傳的信息,以下:

注意:若是有中文,則有必要將整個應用的字符編碼設置爲UTF-8。

參考文章

[1]完全解決springMVC中文亂碼

[2]Spring MVC學習指南

[3]FormData對象

[4]小記 HTML5 file對象

[5]HTML筆記(HTML5 File API)

相關文章
相關標籤/搜索