java学习笔记 -- 文件上传

November . 02 . 2018

 文件上传是算是java web项目中基操了,主要业务就是将文件通过IO流上传到服务器的某一个特定的文件夹下.浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦,所以一般选择采用apache的开源工具common-fileupload这个文件上传组件, 为了方便显示上传之后的显示相应数据, 在此项目中还需要一个转换数据格式的包 json-org, 具体如下图:

1.PNG

项目的包结构如下图(开发环境是 Idea):

2.PNG

在正式开始之前我们先来梳理一下思路, web项目一般分为前端与后端,  前端负责与用户的交互操作, 后端负责处理业务逻辑, 在此项目中也不例外, 前端通过表单提交文件, 后端接收文件并存储在服务器上, 理清思路后就可以正式开始了.

  • 前端
upload.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        
        <style>
            .main{
                
            }
            
            .main input{
                margin: 20px;
            }
            .main input[type='submit']{
                background-color: cornflowerblue;
                color: #fff;
                border: 1px solid #ccc;
                border-radius: 1px;
                padding: 8px 18px;
            }
        </style>
    </head>
    <body>
        <div class='main'>
            <form method="post" action="uploadFile" enctype="multipart/form-data">
                <input type="file" name="fileUpload" /> <br>
                <input type="submit" value="上传" /> <br>
            </form>
       </div>
    </body>
</html>

效果图:

3.PNG

代码很简单, 就是一个赤裸裸的文件上传表单域, 前端的内容到这就结束了.

  • 后端

1. 自定义文件描述类

FileUploadInfo.java
package my;

import java.io.File;

public class FileUploadInfo {

    public String realName;     // 原始文件名
    public String suffix;       // 后缀
    public String tmpFileName;  // 服务器临时文件名
    public File tmpFile;        // 文件实体
    public long fileSize;       // 文件大小
}

该类描述了一个上传文件的基本信息.

2. 创建 Servlet

FileUploadService.java
FileUploadService.java
package my;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.json.JSONObject;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

public class FileUploadService extends HttpServlet {

    // 文件上传所在目录
    File tmpDir;

    @Override
    public void init() throws ServletException {
        // 绝对路径
        File webRoot = new File(getServletContext().getRealPath("/"));
        tmpDir = new File(webRoot, "upload");
        tmpDir.mkdirs();     // 上传进来的文件存放在 WebRoot/upload 下
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        JSONObject jresp = new JSONObject();
        try {
            Object data = doUpload(request, response);    // 执行上传
            jresp.put("error", 0);
            jresp.put("reason", "ok");
            if (data!= null) {
                jresp.put("data", data);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/plain");
        PrintWriter out = response.getWriter();
        out.write(jresp.toString(2));
        out.close();
    }
    
    // 上传
    private Object doUpload(HttpServletRequest request, HttpServletResponse response) throws Exception {
        
        // ServletFileUpload : commons包里提供的工具类
        // 判断请求编码是否为 mutipart/form-data
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);    
        
        if (!isMultipart) {
            throw new Exception("请求编码必须为: mutipart/form-data !");
        }

        request.setCharacterEncoding("UTF-8");

        ServletFileUpload upload = new ServletFileUpload();
        FileUploadInfo info = new FileUploadInfo();

        // 表单域迭代器
        FileItemIterator iter = upload.getItemIterator(request);

        while (iter.hasNext())
        {
            // 表单域
            FileItemStream item = iter.next();
            String fieldName = item.getFieldName();         // 表单标签的name属性的值

            InputStream inStream = item.openStream();    // 允许读取

            // 判断普通的表单域, 还是文件上传表单域
            if (item.isFormField()) {
                // 普通表单域 直接读取值
                // 指的是<input> <select> <textarea> 输入控件的值, 字符串类型
                String fieldValue = Streams.asString(inStream, "UTF-8");
                printLog("表单域: " + fieldName + "=" + fieldValue);
            } else {
                // 文件域, 值 <file> 控件的值, 值文件的数据
                // 生成唯一的文件名
                info.realName = item.getName();
                info.suffix = fileSuffix(info.realName);   // 获取后缀
                info.tmpFileName = createTmpFileName(info.suffix);    // 服务器临时文件名
                info.tmpFile = new File(tmpDir, info.tmpFileName);
                info.fileSize = 0;  // 文件大小

                printLog("文件上传开始: " + info.realName + ">>" + info.tmpFile);
                // 从 FieldStream 读取数据, 保存到目标文件
                info.tmpFile.getParentFile().mkdirs();

                // 输出流把文件写入本地, 可以自动创建文件, 但不能创建目录
                FileOutputStream fileStream = new FileOutputStream(info.tmpFile);
                try {
                    // 从请求里读取文件数据, 保存到本地文件
                    info.fileSize = copy(inStream, fileStream);
                } finally {
                    try {inStream.close();} catch (Exception e){}
                    try {fileStream.close();} catch (Exception e){}
                }
                printLog("文件上传完成: " + info.realName + ", 大小: " + info.fileSize);

            }
        }

        // storePath: 如/upload/1231238723478.jpg
        // url: 相对项目URL, 前面加载ContextPath
        String storePath =  "/upload/" + info.tmpFileName;
        String url = getServletContext().getContextPath() + storePath;

        // 返回一个 JSON
        JSONObject result = new JSONObject();
        result.put("storePath", storePath);
        result.put("url", url);
        return result;
    }

    private long copy(InputStream in, OutputStream out) throws IOException {
        long count = 0;
        byte[] buf = new byte[8192];
        int i = 0;
        while (true)
        {
            // 一次读取不超过buf.length字节的数据, 并存储到buf中, 返回值为实际读取的字节数, 到达文件末尾返回-1
            int n = in.read(buf);
            if (n < 0) {
                break;
            }
            if (n == 0) {
                continue;
            }

            out.write(buf, 0, n);
            System.out.println("第 " + i++ + " 次" + "读取了 " + n + "的数据");
            count += n;
        }
        return count;
    }

    // 打印输出
    private void printLog(String message)
    {
        System.out.println(message);
    }

    // 得到文件后缀名
    private String fileSuffix(String fileName)
    {
        int p = fileName.lastIndexOf('.');
        if (p > 0) {
            return fileName.substring(p+1).toLowerCase();
        }
        return "";
    }

    // 得到一个不重复的临时文件名
    private String createTmpFileName(String suffix)
    {
        String dateStr = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
        String name = dateStr + "-" + createUUID() + "." + suffix;
        return name;
    }

    // 产生唯一id
    private String createUUID() {
        String s = UUID.randomUUID().toString();
        String s2 = s.substring(0, 8) + s.substring(9, 13) + s.substring(14, 18) + s.substring(19, 23)+s.substring(24);
        return s2.toUpperCase();
    }
}

注意看注释, 文件上传的逻辑大致可以总结为:

  1. 在服务器上创建存放上传文件的目录, 此例中的临时目录是: web/upload.
  2. 从 HttpServletRequest 中取出文件, 并做判断防止上传的文件太大或是病毒文件.
  3. 使用 InputStram 打开一个输入流句柄, 用来读取文件内容.
  4. 使用 FileOutputStream 打开一个输出流句柄, 用来把上传文件写入本地, 并重命名上传文件, 防止重名.
  5. 关闭 InputStram , FileOutputStream 句柄, 防止浪费内存资源.

3. 配置 web.xml

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>FileUploadService</servlet-name>
        <servlet-class>my.FileUploadService</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FileUploadService</servlet-name>
        <url-pattern>/uploadFile</url-pattern>
    </servlet-mapping>
</web-app>

done