架構探險筆記10-框架優化之文件上傳

肯定文件上傳使用場景

一般狀況下,咱們能夠經過一個form(表單)來上傳文件,就如下面的「建立客戶」爲例來講明(對應的文件名是customer_create.jsp),須要提供一個form,並將其enctype屬性設爲multipart/form-data,表示以form data方式提交表單數據。html

注意:enctype的默認值爲application/x-www-form-urlencoded,表示以url encoded方式提交表單數據。java

下面咱們使用jQuery與jQuery Form插件快速編寫一個基於Ajax的文件上傳表單,代碼以下:jquery

<%@ page pageEncoding="UTF-8" contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<c:set var="BASE" value="${pageContext.request.contextPath}"/>
<html>
<head>
    <title>客戶管理-建立客戶</title>
</head>
<body>

<h1>建立客戶界面</h1>
${msg}
<form id="customer_form" enctype="multipart/form-data">
    <table>
        <tr>
            <td>客戶名稱:</td>
            <td><input type="text" name="name" value="${customer.name}"></td>
        </tr>
        <tr>
            <td>聯繫人:</td>
            <td><input type="text" name="contact" value="${customer.contact}"></td>
        </tr>
        <tr>
            <td>電話號碼:</td>
            <td><input type="text" name="telephone" value="${customer.telephone}"></td>
        </tr>
        <tr>
            <td>郵箱地址:</td>
            <td><input type="text" name="email" value="${customer.email}"></td>
        </tr>
        <tr>
            <td>照片:</td>
            <td><input type="file" name="photo" value="${customer.photo}"></td>
        </tr>
    </table>
    <button type="submit">保存</button>
</form>

<script src="${BASE}/asset/lib/jquery/jquery.min.js"></script>
<script src="${BASE}/asset/lib/jquery-form/jquery.form.min.js"></script>
<script>
    $(function () {
        $('#customer_form').ajaxForm({
            type:'post',
            url:'${BASE}/customer_create',
            success:function (data) {
                if(data){
                    location.href = '${BASE}/customer';
                }
            }
        });
    });
</script>
</body>
</html>

當表單提交時,請求會轉發到CustomerController的createSubmit方法上。該方法帶有一個Param參數,咱們打算經過該參數來獲取「表單字段的名值對映射」與「所上傳的文件參數對象」,應該如何編碼呢?下面是咱們要實現的目標:git

@Controller
public class CustomerController {
    /**
     * 處理 建立客戶請求 - 帶圖片
     */
    @Action("post:/customer_create")
    public Data createSubmit(Param param){
        Map<String,Object> fieldMap = param.getFieldMap();
        FileParam fileParam = param.getFile("photo");
        boolean result = customerService.createCustomer(fieldMap,fileParam);
        return new Data(result);
    } 
}

調用Param的getFieldMap()方法來獲取表單字段的鍵值對映射(Map fieldMap),指定一個具體的文件字段名稱photo,並調用getFile方法便可獲取對應的文件參數對象(FileParam fileParam)。隨後,可調用customerService的createCustomer方法,將fieldMap與fileParam這兩個參數傳入。github

Controller層的代碼就是這樣,具體業務邏輯都在Service層了,對於CustomerService而言,只需寫幾行代碼便可實現業務邏輯,將輸入參數存入數據庫,同時將文件上傳到服務器上。web

@Service
public class CustomerService {
    /**
     * 建立客戶
     */
    @Transaction
    public boolean createCustomer(Map<String,Object> fieldMap,FileParam fileParam){
        Boolean result = DBHelper.insertEntity(Customer.class,fieldMap);
        if (result){
            UploadHelper.uploadFile("/tmp/upload/",fileParam);
        }
        return result;
    }
}

可見,除了使用DatabaseHelper操做數據庫,還能夠經過UploadHelper將文件上傳到指定的服務器目錄中。ajax

注意:實際上,徹底能夠經過代碼來讀取配置文件中定義的文件上傳路徑,此處只是爲了簡化,請注意。數據庫

咱們把計劃要完成的事情總結一下:apache

(1)改造Param結構,能夠經過它來獲取已上傳的文件參數(FileParam)json

(2)使用UploadHelper助手類來上傳文件。

實現文件上傳功能 

咱們不妨從FileParam開始,它其實是一個用於封裝文件參數的JavaBean,代碼以下:

/**
 * @program: FileParam
 * @description: 封裝文件參數的Bean
 */
public class FileParam {
    private String fieldName;  //文件表單的字段名
    private String fileName;   //文件名
    private long fileSize;  //文件大小
    private String contentType;    //上傳文件的Content-Type,可判斷文件類型
    private InputStream inputStream;   //上傳文件的字節輸入流

    public FileParam(String fieldName, String fileName, long fileSize, String contentType, InputStream inputStream) {
        this.fieldName = fieldName;
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.contentType = contentType;
        this.inputStream = inputStream;
    }

    public String getFieldName() {
        return fieldName;
    }

    public String getFileName() {
        return fileName;
    }

    public long getFileSize() {
        return fileSize;
    }

    public String getContentType() {
        return contentType;
    }

    public InputStream getInputStream() {
        return inputStream;
    }
}

除了文件參數(FileParam),咱們還須要一個表單參數(FormParam),代碼以下: 

/**
 * @program: FormParam
 * @description: 封裝表單參數
 */
public class FormParam {
    private String fieldName;   //表單字段名
    private Object fieldValue;   //表單字段值

    public FormParam(String fieldName, Object fieldValue) {
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    public String getFieldName() {
        return fieldName;
    }

    public Object getFieldValue() {
        return fieldValue;
    }
}

在一個表單中,全部的參數可分爲兩類:表單參數與文件參數。有必要將Param類作一個重構,讓它封裝這兩類參數,並提供一系列的get方法,用於從該對象中獲取指定的參數。

/**
 * @program: Param
 * @description: 請求參數對象
 */
public class Param {
    private List<FormParam> formParamList;
    private List<FileParam> fileParamList;

    public Param(List<FormParam> formParamList) {
        this.formParamList = formParamList;
    }

    public Param(List<FormParam> formParamList, List<FileParam> fileParamList) {
        this.formParamList = formParamList;
        this.fileParamList = fileParamList;
    }

    /**
     * 獲取請求參數映射
     * @return
     */
    public Map<String,Object> getFieldMap(){
        Map<String,Object> fieldMap = new HashMap<String,Object>();
        if (CollectionUtil.isNotEmpty(formParamList)){
            for (FormParam formParam:formParamList){
                String fieldName = formParam.getFieldName();   //表單參數名
                Object fieldValue = formParam.getFieldValue();   //表單參數值
                if (fieldMap.containsKey(fieldName)){   //若是已經有此參數名
                    fieldValue = fieldMap.get(fieldName) + StringUtil.SEPARATOR + fieldValue;  // 舊的數據<-->新的數據做爲value
                }
                fieldMap.put(fieldName,fieldValue);
            }
        }
        return fieldMap;
    }

    /**
     * 獲取上傳文件映射
     */
    public Map<String,List<FileParam>> getFileMap(){
        Map<String,List<FileParam>> fileMap = new HashMap<String,List<FileParam>>();

        if (CollectionUtil.isNotEmpty(fileMap)){
            for (FileParam fileParam:fileParamList){    //遍歷文件參數
                String fieldName = fileParam.getFieldName();    //獲取表單文件字段名
                List<FileParam> fileParamList;
                if (fileMap.containsKey(fieldName)){    //若是Map已經存在
                    fileParamList = fileMap.get(fieldName);   //獲取Map中的值
                }else{
                    fileParamList = new ArrayList<FileParam>();  //不然,新建一個值
                }
                fileParamList.add(fileParam);   //
                fileMap.put(fieldName,fileParamList);   //放入到表單文件字段名,List<FileParam>的映射中
            }
        }
        return fileMap;
    }

    /**
     * 獲取全部上傳文件
     * @param fieldName 表單文件字段名
     * @return
     */
    public List<FileParam> getFileList(String fieldName){
        return getFileMap().get(fieldName);
    }

    /**
     * 獲取惟一上傳文件
     * @param fieldName 表單文件字段名
     * @return
     */
    public FileParam getFile(String fieldName){
        List<FileParam> fileParamList = getFileList(fieldName);
        if (CollectionUtil.isNotEmpty(fileParamList) && fileParamList.size() ==1){
            return fileParamList.get(0);
        }
        return null;
    }

    /**
     * 驗證參數是否爲空
     * @return
     */
    public boolean isEmpty(){
        return CollectionUtil.isEmpty(formParamList) && CollectionUtil.isEmpty(fileParamList);
    }

    /**
     * 根據參數名獲取String型參數值
     * @param name
     * @return
     */
    public String getString(String name){
        return CastUtil.castString(getFieldMap().get(name));
    }

    /**
     * 根據參數名獲取Double型參數值
     * @param name
     * @return
     */
    public Double getDouble(String name){
        return CastUtil.castDouble(getFieldMap().get(name));
    }

    /**
     * 根據參數名獲取Long型參數值
     * @param name
     * @return
     */
    public long getLong(String name){
        return CastUtil.castLong(getFieldMap().get(name));
    }
    /**
     * 根據參數名獲取int型參數值
     * @param name
     * @return
     */
    public int getInt(String name){
        return CastUtil.castInt(getFieldMap().get(name));
    }
    /**
     * 根據參數名獲取boolean型參數值
     * @param name
     * @return
     */
    public boolean getBoolean(String name){
        return CastUtil.castBoolean(getFieldMap().get(name));
    }

}

可見Param包含了兩個成員變量:List<formParamList>與List<fileParamList>;它們分別封裝了表單參數與文件參數,隨後提供了兩個構造器,用於初始化Param對象,還提供了兩個get方法,分別用於獲取全部的表單參數與文件參數。返回值均爲Map類型,其中Map表示請求參數映射,Map表示上傳文件映射。對於同名的請求參數,經過一個特殊的分隔符進行了處理,該分隔符定義在StringUtil類中,代碼以下:

    /**
     * 分隔符
     */
    public static final String SEPARATOR = String .valueOf((char)29);

對於同名的上傳文件,經過一個List進行了封裝,可輕鬆實現多文件上傳的需求。可經過List getFileList(String fieldName) 方法獲取全部上傳文件,若只上傳了一個文件,則可直接使用FileParam getFile(String fieldName)方法獲取惟一上傳文件。還提供了一個boolean isEmpty()方法,用於驗證參數是否爲空。最後,提供了一組根據參數名獲取指定類型的方法,例如,String getString(String name)、double getDouble(String name)等。

可藉助Apache Commons提供的FileUpload類庫實現文件上傳特性,首先須要在pom.xml中添加以下依賴:

        <!--文件上傳-->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.1</version>
        </dependency>

接下來咱們須要編寫一個UploadHelper類來封裝Apache Commons FileUpload的相關代碼:

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smart4j.framework.bean.FileParam;
import org.smart4j.framework.bean.FormParam;
import org.smart4j.framework.bean.Param;
import org.smart4j.framework.util.CollectionUtil;
import org.smart4j.framework.util.FileUtil;
import org.smart4j.framework.util.StreamUtil;
import org.smart4j.framework.util.StringUtil;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @program: UploadHelper
 * @description: 文件上傳助手類
 * @author: Created by Autumn
 * @create: 2018-12-14 16:21
 */
public final class UploadHelper {
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadHelper.class);

    /**
     * Apache Commons FileUpload提供的Servlet文件上傳對象
     */
    private static ServletFileUpload servletFileUpload;

    /**
     * 初始化
     */
    public static void init(ServletContext servletContext){
        /*獲取tomcat的work目錄*/
        File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");

        /**
         * DiskFileItemFactory構造的兩個參數
         *  第一個參數:sizeThreadHold - 設置緩存(內存)保存多少字節數據,默認爲10240字節,即10K
         *    若是一個文件沒有大於10K,則直接使用內存直接保存成文件就能夠了。
         *    若是一個文件大於10K,就須要將文件先保存到臨時目錄中去。
         *  第二個參數 File 是指臨時目錄位置 - 能夠不用tomcat的work目錄能夠用任意一個目錄
         */
        DiskFileItemFactory fileItemFactory = new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, repository);
        servletFileUpload = new ServletFileUpload(fileItemFactory);

        int uploadLimit = ConfigHelper.getAppUploadLimit();  //獲取文件上傳限制默認爲10(M)
        if (uploadLimit != 0){
            servletFileUpload.setFileSizeMax(uploadLimit*1024*1024);   //設置單文件最大大小爲10M
        }
    }

    /**
     * 判斷請求是否爲multipart類型
     */
    public static boolean isMultipart(HttpServletRequest request){
        return ServletFileUpload.isMultipartContent(request);
    }

    /**
     * 建立請求對象
     * 將request轉換爲Param參數
     * @return
     */
    public static Param createParam(HttpServletRequest request) throws IOException {
        List<FormParam> formParamList = new ArrayList<FormParam>();
        List<FileParam> fileParamList = new ArrayList<FileParam>();

        try{
            /*解析request*/
            Map<String,List<FileItem>> fileItemListMap = servletFileUpload.parseParameterMap(request);   //將request轉換爲Map
            if (CollectionUtil.isNotEmpty(fileItemListMap)){
                //遍歷Map集合,一個表單名可能有多個文件
                for (Map.Entry<String,List<FileItem>> fileItemListEntry : fileItemListMap.entrySet()){
                    String fieldName = fileItemListEntry.getKey();    //獲取表單字段名
                    List<FileItem> fileItemList = fileItemListEntry.getValue();   //文件集合

                    if (CollectionUtil.isNotEmpty(fileItemListMap)){
                        for (FileItem fileItem:fileItemList){   //遍歷文件集合
                            if (fileItem.isFormField()){   //若是是表單字段
                                String fieldValue = fileItem.getString("UTF-8");
                                formParamList.add(new FormParam(fieldName,fieldValue));
                            }else{   //若是是文件
                                String fileName = FileUtil.getRealFileName(new String(fileItem.getName().getBytes(),"UTF-8"));   //獲取文件名
                                if (StringUtil.isNotEmpty(fileName)){  //若是文件名不爲空
                                    long fileSize = fileItem.getSize();  //獲取文件大小
                                    String contentType = fileItem.getContentType();   //獲取文件類型
                                    InputStream inputStream = fileItem.getInputStream();   //獲取文件輸入流
                                    fileParamList.add(new FileParam(fieldName,fileName,fileSize,contentType,inputStream));
                                }
                            }
                        }
                    }
                }
            }
        } catch (FileUploadException e) {
            LOGGER.error("create param failure",e);
            throw new RuntimeException(e);
        }
        return new Param(formParamList,fileParamList);
    }


    /**
     * 上傳文件
     * @param basePath
     * @param fileParam
     */
    public static void uploadFile(String basePath,FileParam fileParam){
        try{
            if (fileParam != null){
                String filePath = basePath + fileParam.getFileName();   //路徑+文件名
                FileUtil.createFile(filePath);  //建立文件
                InputStream inputStream = new BufferedInputStream(fileParam.getInputStream());  //獲取文件的輸入流
                OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(filePath));   //獲取輸出流
                StreamUtil.copyStream(inputStream,outputStream);   //輸入流拷貝到輸出流中
            }
        } catch (FileNotFoundException e) {
            LOGGER.error("upload file failure",e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 批量上傳文件
     * @param basePath
     * @param fileParamList
     */
    public static void uploadFile(String basePath,List<FileParam> fileParamList){
        try {
            if (CollectionUtil.isNotEmpty(fileParamList)){
                for (FileParam fileParam : fileParamList){
                    uploadFile(basePath,fileParam);
                }
            }
        }catch (Exception e){
            LOGGER.error("upload file failure",e);
            throw new RuntimeException(e);
        }

    }
}

須要提供一個init方法,在該方法中初始化ServletFileUpload對象。通常狀況下,只需設置一個上傳文件的臨時目錄與上傳文件的最大限制;上傳文件的臨時目錄可設置爲應用服務器的臨時目錄,上傳文件的最大限制可以讓用戶自行配置。因此咱們使用了ConfigHelper.getAppUploadLimit()來獲取,能夠在smart.properties文件中進行配置。

首先,在ConfigConstant中添加一個配置常量APP_UPLOAD_LIMIT;

    String APP_UPLOAD_LIMIT = "smart.framework.app.upload_limit";

這也就意味着,咱們能夠在smart.properties文件中使用smart.framwork.app.upload_limit配置項來設定上傳文件的最大限制。

而後,在ConfigHelper中添加一個int getAppUploadLimit()方法,用於獲取該配置的值,此時可設置該配置的初始值(10),也就是說,若不在smart.properties文件中提供該配置,則上傳文件的最大限制是10MB。

public class ConfigHelper {
    /**
     * 獲取應用文件上傳限制
     * @return
     */
    public static int getAppUploadLimit(){
        return PropsUtil.getInt(CONFIG_PROPS,ConfigConstant.APP_UPLOAD_LIMIT,10);
    }
}

在UploadHelper中提供一個boolean isMultipart(HttpServletRequest request)方法,用於判斷當前請求對象是否爲multipart類型。只有在上傳文件時對應的請求類型纔是multipart類型,也就是說,可經過isMultipart方法來判斷當前請求時否爲文件上傳請求。

接下來提供一個很是重要的方法,可從當前請求中建立Param對象,它就是Param createParam(HttpServletRequest request)方法:其中咱們使用了ServletFileUpload對象來解析請求參數,並經過遍歷全部請求參數來初始化List formParamList與List fileParamList變量的值。在遍歷請求參數時,須要對當前的org.apache.commons.fileupload.FileItem對象進行判斷,若爲普通表單字段(調用fileItem.isFormField()返回true),則建立FormParam對象,並添加到formParamList對象中。不然即爲文件上傳字段,經過FileUtil提供的getRealFileName來獲取上傳文件後的真實文件名,並從FileItem對象中構造FileParam對象,添加到fileParamList對象中,最後,經過formParamList與fileParamList來構造Param對象並返回。

FileUtil代碼以下

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;

/**
 * @program: FileUtil
 * @description: 文件操做工具類
 * @author: Created by Autumn
 * @create: 2018-12-19 13:03
 */
public class FileUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class);

    /**
     * 獲取真實文件名(自動去掉文件路徑)
     *
     * @param fileName
     * @return
     */
    public static String getRealFileName(String fileName) {
        return FilenameUtils.getName(fileName);
    }

    /**
     * 建立文件
     *
     * @param filePath
     * @return
     */
    public static File createFile(String filePath) {
        File file;
        file = new File(filePath);   //根據路徑建立文件
        try {
            File parentDir = file.getParentFile();   //獲取文件父目錄
            if (!parentDir.exists()) {  //判斷上層目錄是否存在
                FileUtils.forceMkdir(parentDir);   //建立父級目錄
            }
        } catch (IOException e) {
            LOGGER.error("create file failure",e);
            throw new RuntimeException(e);
            //e.printStackTrace();
        }
        return file;
    }
}

最後提供兩個用於上傳文件的方法,一個用於上傳單個文件,另外一個用於批量上傳。此時用到了StreamUtil工具類的copyStream方法,代碼以下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;

/**
 * @program: StreamUtil
 * @description: 流操做經常使用工具類
 * @author: Created by Autumn
 * @create: 2018-10-24 15:41
 */
public class StreamUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(StreamUtil.class);

    /**
     * 從輸入流中獲取字符串
     * @param is
     * @return
     */
    public static String getString(InputStream is){
        StringBuilder sb = new StringBuilder();
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            String line;
            while((line=reader.readLine())!=null){
                sb.append(line);
            }
        } catch (IOException e) {
            LOGGER.error("get string failure",e);
            throw new RuntimeException(e);
        }
        return sb.toString();
    }

    /**
     * 將輸入流複製到輸出流
     * @param inputStream 輸入流
     * @param outputStream 輸出流
     */
    public static void copyStream(InputStream inputStream, OutputStream outputStream){
        try {
            int length;
            byte[] buffer = new byte[4*1024];
            while((length = inputStream.read(buffer,0,buffer.length)) != -1){
                outputStream.write(buffer,0,length);
            }
            outputStream.flush();
        } catch (IOException e) {
            LOGGER.error("copy stream failure",e);
            throw new RuntimeException(e);
        } finally {
            try {
                inputStream.close();
                outputStream.close();
            } catch (IOException e) {
                LOGGER.error("close stream failure",e);
            }
        }
    }

}

如今UploadHelper已編寫完畢,接下來須要找一個地方來調用init方法。整個web框架的入口也就是DispatcherServlet的init方法了,全部咱們須要在該方法中調用UploadHelper的init方法。

除了在DispatcherServlet的init方法中添加一行代碼,還須要對service代碼進行一些重構。首先須要跳過/favicon.ico請求,只處理普通的請求。而後須要判斷請求對象是否爲上傳文件,針對兩種不一樣的狀況來建立Param對象,其中經過UploadHelper來建立的方式已在前面描述了。相應的,咱們也對之前的代碼進行封裝,提供一個名爲RequestHelper類,並經過它的createParam方法來初始化Param對象。

import org.smart4j.framework.bean.FormParam;
import org.smart4j.framework.bean.Param;
import org.smart4j.framework.util.ArrayUtil;
import org.smart4j.framework.util.CodecUtil;
import org.smart4j.framework.util.StreamUtil;
import org.smart4j.framework.util.StringUtil;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * @program: RequestHelper
 * @description: 請求助手類
 * @author: Created by Autumn
 * @create: 2018-12-25 13:22
 */
public class RequestHelper {

    public static Param createParam(HttpServletRequest request) throws IOException {
        List<FormParam> formParamList = new ArrayList<>();
        formParamList.addAll(parseParameterNames(request));
        formParamList.addAll(parseInputStream(request));
        return new Param(formParamList);
    }

    /**
     * 獲取Form表單普通參數並放入List<FormParam>中
     * 適用於application/x-www-form-urlencoded
     * @param request
     * @return List<FormParam>
     */
    private static List<FormParam> parseParameterNames(HttpServletRequest request){
        List<FormParam> formParamList = new ArrayList<FormParam>();
        Enumeration<String> paramNames = request.getParameterNames();   //獲取request中的全部參數名稱枚舉
        while (paramNames.hasMoreElements()){   //遍歷參數名枚舉
            String fieldName = paramNames.nextElement();   //獲取參數名稱
            //!!!!!!!!獲取參數值(例如CheckBox的值有多個) request.getParameter(String name)是得到相應名的數據,若是有重複的名,則返回第一個的值.
            String[] fieldValues = request.getParameterValues(fieldName);
            if (ArrayUtil.isNotEmpty(fieldValues)){   //判斷是否爲空
                Object fieldValue;   //參數最終值
                if (fieldValues.length == 1){  //若是隻有一個值
                    fieldValue = fieldValues[0];   //直接賦值
                } else {  //若是有多個值(CheckBox多選)
                    StringBuilder sb = new StringBuilder("");
                    for (int i = 0; i< fieldValues.length; i++){  //遍歷
                        sb.append(fieldValues[i]);
                        if (i != fieldValues.length-1){  //若是不是最後一個
                            sb.append(StringUtil.SEPARATOR);  //加上通用分割符
                        }
                    }
                    fieldValue = sb.toString();

                }
                formParamList.add(new FormParam(fieldName,fieldValue));   //將參數鍵值對加入List參數列表中去
            }
        }
        return formParamList;
    }

    /**
     * 獲取參數流並放入List<FormParam>中
     * 適用於application/json,text/xml,multipart/form-data文本流或者大文件形式提交的請求或者xml等形式的報文
     * @param request
     * @return
     * @throws IOException
     */
    private static List<FormParam> parseInputStream(HttpServletRequest request) throws IOException {
        List<FormParam> formParamList = new ArrayList<FormParam>();

        String body = CodecUtil.decodeURL(StreamUtil.getString(request.getInputStream()));
        if (StringUtil.isNotEmpty(body)){
            String[] kvs = StringUtil.splitString(body,"&");
            if (ArrayUtil.isNotEmpty(kvs)){
                for (String kv:kvs) {
                    String[] array = StringUtil.splitString(kv, "=");
                    if (ArrayUtil.isNotEmpty(array) && array.length == 2){
                        String fieldName = array[0];
                        String fieldValue = array[1];
                        formParamList.add(new FormParam(fieldName,fieldValue));
                    }
                }
            }
        }
        return formParamList;
    }
}

可見以上代碼邏輯並未變化,只是將之前放在DispatcherServlet中的相關代碼搬到了RequestHelper中了。最後獲取的View一樣也分兩種狀況進行了處理,只是此時並未提供其餘類來封裝這些代碼,而是直接在當前類中添加了兩個私有方法handleViewResult與handleDataResult。

重構後的Dispatcher代碼

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smart4j.framework.bean.Data;
import org.smart4j.framework.bean.Handler;
import org.smart4j.framework.bean.Param;
import org.smart4j.framework.bean.View;
import org.smart4j.framework.helper.*;
import org.smart4j.framework.util.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @program: DispatcherServlet
 * @description: 請求轉發器
 * @author: Created by Autumn
 * @create: 2018-10-24 11:34
 */

@WebServlet(urlPatterns = "/*",loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {
    private static final Logger LOGGER = LoggerFactory.getLogger(DispatcherServlet.class);

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        //初始化相關Helper類
        HelperLoader.init();
        //獲取ServletContext對象(用於註冊Servlet)
        ServletContext servletContext = servletConfig.getServletContext();
        //註冊處理JSP的Servlet
        ServletRegistration jspServlet = servletContext.getServletRegistration("jsp");
        jspServlet.addMapping(ConfigHelper.getAppJspPath()+"*");
        //註冊處理靜態資源的默認Servlet
        ServletRegistration defaultServlet = servletContext.getServletRegistration("default");
        defaultServlet.addMapping(ConfigHelper.getAppAssetPath()+"*");
        //初始化上傳文件大小,以及超過最大大小存放的目錄
        UploadHelper.init(servletContext);
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //獲取請求方法與請求路徑
        String requestMethod = req.getMethod().toLowerCase();
        String requestPath = req.getPathInfo();

        if (requestPath.equals("\favicon.ico")){
            return ;
        }
        //獲取Action處理器
        Handler handler= ControllerHelper.getHandler(requestMethod,requestPath);
        if(handler!=null){
            //獲取Controller類機器Bean實例
            Class<?> controllerClass = handler.getControllerClass();
            Object controllerBean = BeanHelper.getBean(controllerClass);

            Param param;
            if (UploadHelper.isMultipart(req)){   //若是是multipart/form-data stream
                param = UploadHelper.createParam(req);   //multipart方式
            }else{   //若是是非multipart方式提交(即application/x-www-form-urlencoded,application/json,text/xml)
                param = RequestHelper.createParam(req);   //非multipart表單方式
            }

            /*將一下代碼放入RequestHelper中去
            //建立請求參數對象
            Map<String,Object> paramMap = new HashMap<String, Object>();
            Enumeration<String> paramNames = req.getParameterNames();
            while(paramNames.hasMoreElements()){
                String paramName = paramNames.nextElement();
                String paramValue = req.getParameter(paramName);
                paramMap.put(paramName,paramValue);
            }
            //獲取請求body中的參數
            String body = CodecUtil.decodeURL(StreamUtil.getString(req.getInputStream()));
            if (StringUtil.isNotEmpty(body)){
                String[] params = StringUtil.splitString(body,"&");
                if (ArrayUtil.isNotEmpty(params)){
                    for (String param:params){
                        String[] array = StringUtil.splitString(param,"=");
                        if (ArrayUtil.isNotEmpty(array)&&array.length==2){
                            String paramName = array[0];
                            String paramValue = array[1];
                            paramMap.put(paramName,paramValue);
                        }
                    }
                }
            }
            Param param = new Param(paramMap);
            */

            Object result = null;
            //調用Action方法
            Method actionMethod = handler.getActionMethod();
            /*優化沒有參數的話不須要寫參數*/
            if (param.isEmpty()){  //若是沒有參數
                result = ReflectionUtil.invokeMethod(controllerBean,actionMethod);  //就不傳參數
            }else{  //有參數
                result = ReflectionUtil.invokeMethod(controllerBean,actionMethod,param);  //傳參數
            }

            //處理Action方法返回值
            if (result instanceof View){
                //返回JSP頁面
                handleViewResult((View) result, req, resp);
            }else if (result instanceof Data){
                //返回Json數據
                handleDataResult((Data) result, resp);
            }
        }else{
            LOGGER.error("Request-Handler Mapping get null by Request("+requestMethod+","+requestPath+")");
            throw new RuntimeException("Request-Handler Mapping get null by Request("+requestMethod+","+requestPath+")");
        }

    }

    /**
     * 處理Json格式的數據
     * @param result Data對象
     * @param resp
     * @throws IOException
     */
    private void handleDataResult(Data result, HttpServletResponse resp) throws IOException {
        Data data = result;
        Object model = data.getModel();
        if (model!=null){
            resp.setContentType("application/json");
            resp.setCharacterEncoding("UTF-8");
            PrintWriter writer = resp.getWriter();
            String json = JsonUtil.toJson(model);
            writer.write(json);
            writer.flush();
            writer.close();
        }
    }

    /**
     * 處理視圖結果
     * @param result View對象(jsp路徑+數據)
     * @param req
     * @param resp
     * @throws IOException
     * @throws ServletException
     */
    private void handleViewResult(View result, HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        View view = result;
        String path = view.getPath();
        if (StringUtil.isNotEmpty(path)){
            if (path.startsWith("/")){   //若是View的Path以/開頭則以項目根目錄爲根路徑
                resp.sendRedirect(req.getContextPath()+path);
            } else {    //若是View的Path沒有以/開頭,則以配置的APPJSP(/WEB-INF/view/)爲根目錄
                Map<String,Object> model = view.getModel();
                for (Map.Entry<String,Object> entry:model.entrySet()){
                    req.setAttribute(entry.getKey(),entry.getValue());
                }
                req.getRequestDispatcher(ConfigHelper.getAppJspPath()+path).forward(req,resp);
            }

        }
    }
}

此時,一個簡單的文件上傳特性已基本具有,能夠在框架中正常使用了。

可能出現的問題

獲取註冊處理JSP的Servlet報錯

問題代碼

這是由於tomcat用的是maven插件,並非真實的tomcat。因此致使獲取jsp的servlet失敗。

jQuery未引入致使用原生form提交

原生form提交的幾個要素

action:url 地址,服務器接收表單數據的地址

method:提交服務器的http方法,通常爲post和get

enctype: 表單數據提交時使用的編碼類型,默認使用 "pplication/x-www-form-urlencoded" 。若是是使用POST請求,則請求頭中的content-type指定值就是該值。若是表單中有上傳文件,編碼類型須要使用 "multipart/form-data" ,類型,才能完成傳遞文件數據。

寫了method、enctype和action後,最後form表單的提交按鈕要用

<input type="submit">保存</input>

缺一個都會用默認的get方式提交。

<form id="customer_form" action="${BASE}/customer_create" method="post" enctype="multipart/form-data">
    <input type="submit">保存</input>
</form>

後臺獲取文件沒有內容(此bug由我的失誤致使,可過濾)

 調試框架源碼發現一個方法判斷有誤,寫成了局部變量fileMap了。寫時候一個不當心,調試要調試半天吶

最終文件上傳完畢,結果以下。

框架源碼

項目源碼(使用開發框架)

相關文章
相關標籤/搜索