架构探险笔记10-框架优化之文件上传

确定文件上传使用场景

通常情况下,我们可以通过一个form(表单)来上传文件,就以下面的“创建客户”为例来说明(对应的文件名是customer_create.jsp),需要提供一个form,并将其enctype属性设为multipart/form-data,表示以form data方式提交表单数据。

注意:enctype的默认值为application/x-www-form-urlencoded,表示以url encoded方式提交表单数据。

下面我们使用jQuery与jQuery Form插件快速编写一个基于Ajax的文件上传表单,代码如下:

<%@ 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参数,我们打算通过该参数来获取“表单字段的名值对映射”与“所上传的文件参数对象”,应该如何编码呢?下面是我们要实现的目标:

@Controller
public class CustomerController {
    /**
     * 处理 创建客户请求 - 带图片
     */
    @Action("post:/customer_create")
    public Data createSubmit(Param param){
        Map 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这两个参数传入。

Controller层的代码就是这样,具体业务逻辑都在Service层了,对于CustomerService而言,只需写几行代码即可实现业务逻辑,将输入参数存入数据库,同时将文件上传到服务器上。

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

可见,除了使用DatabaseHelper操作数据库,还可以通过UploadHelper将文件上传到指定的服务器目录中。

注意:实际上,完全可以通过代码来读取配置文件中定义的文件上传路径,此处只是为了简化,请注意。

我们把计划要完成的事情总结一下:

(1)改造Param结构,可以通过它来获取已上传的文件参数(FileParam)

(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 formParamList;
    private List fileParamList;

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

    public Param(List formParamList, List fileParamList) {
        this.formParamList = formParamList;
        this.fileParamList = fileParamList;
    }

    /**
     * 获取请求参数映射
     * @return
     */
    public Map getFieldMap(){
        Map fieldMap = new HashMap();
        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> getFileMap(){
        Map> fileMap = new HashMap>();

        if (CollectionUtil.isNotEmpty(fileMap)){
            for (FileParam fileParam:fileParamList){    //遍历文件参数
                String fieldName = fileParam.getFieldName();    //获取表单文件字段名
                List fileParamList;
                if (fileMap.containsKey(fieldName)){    //如果Map已经存在
                    fileParamList = fileMap.get(fieldName);   //获取Map中的值
                }else{
                    fileParamList = new ArrayList();  //否则,新建一个值
                }
                fileParamList.add(fileParam);   //
                fileMap.put(fieldName,fileParamList);   //放入到表单文件字段名,List的映射中
            }
        }
        return fileMap;
    }

    /**
     * 获取所有上传文件
     * @param fieldName 表单文件字段名
     * @return
     */
    public List getFileList(String fieldName){
        return getFileMap().get(fieldName);
    }

    /**
     * 获取唯一上传文件
     * @param fieldName 表单文件字段名
     * @return
     */
    public FileParam getFile(String fieldName){
        List 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与List;它们分别封装了表单参数与文件参数,随后提供了两个构造器,用于初始化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-fileuploadgroupId>
            <artifactId>commons-fileuploadartifactId>
            <version>1.3.1version>
        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 formParamList = new ArrayList();
        List fileParamList = new ArrayList();

        try{
            /*解析request*/
            Map> fileItemListMap = servletFileUpload.parseParameterMap(request);   //将request转换为Map
            if (CollectionUtil.isNotEmpty(fileItemListMap)){
                //遍历Map集合,一个表单名可能有多个文件
                for (Map.Entry> fileItemListEntry : fileItemListMap.entrySet()){
                    String fieldName = fileItemListEntry.getKey();    //获取表单字段名
                    List 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 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 formParamList = new ArrayList<>();
        formParamList.addAll(parseParameterNames(request));
        formParamList.addAll(parseInputStream(request));
        return new Param(formParamList);
    }

    /**
     * 获取Form表单普通参数并放入List中
     * 适用于application/x-www-form-urlencoded
     * @param request
     * @return List
     */
    private static List parseParameterNames(HttpServletRequest request){
        List formParamList = new ArrayList();
        Enumeration 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中
     * 适用于application/json,text/xml,multipart/form-data文本流或者大文件形式提交的请求或者xml等形式的报文
     * @param request
     * @return
     * @throws IOException
     */
    private static List parseInputStream(HttpServletRequest request) throws IOException {
        List formParamList = new ArrayList();

        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 paramMap = new HashMap();
            Enumeration 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 model = view.getModel();
                for (Map.Entry entry:model.entrySet()){
                    req.setAttribute(entry.getKey(),entry.getValue());
                }
                req.getRequestDispatcher(ConfigHelper.getAppJspPath()+path).forward(req,resp);
            }

        }
    }
}

此时,一个简单的文件上传特性已基本具备,可以在框架中正常使用了。

可能出现的问题

获取注册处理JSP的Servlet报错

《架构探险笔记10-框架优化之文件上传》

问题代码

《架构探险笔记10-框架优化之文件上传》

这是因为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表单的提交按钮要用

保存

缺一个都会用默认的get方式提交。

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

后台获取文件没有内容(此bug由个人失误导致,可过滤)

 调试框架源码发现一个方法判断有误,写成了局部变量fileMap了。写时候一个不小心,调试要调试半天呐

《架构探险笔记10-框架优化之文件上传》

最终文件上传完毕,结果如下。

《架构探险笔记10-框架优化之文件上传》

框架源码

项目源码(使用开发框架)

 

点赞

Leave a Reply

Your email address will not be published. Required fields are marked *