架构探险笔记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<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这两个参数传入。
Controller层的代码就是这样,具体业务逻辑都在Service层了,对于CustomerService而言,只需写几行代码即可实现业务逻辑,将输入参数存入数据库,同时将文件上传到服务器上。
@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将文件上传到指定的服务器目录中。
注意:实际上,完全可以通过代码来读取配置文件中定义的文件上传路径,此处只是为了简化,请注意。
我们把计划要完成的事情总结一下:
(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<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了。写时候一个不小心,调试要调试半天呐
最终文件上传完毕,结果如下。
![微信公众号二维码](https://images.cnblogs.com/cnblogs_com/aeolian/1679458/o_wechat_gzh_qrcode.jpg)