表单提交与头像上传

    |     2016年12月20日   |   Servlet与Jsp   |     0 条评论   |    4317

在 Web 后台管理系统里,数据的增、删、改几乎是标配,本质都是对数据库表记录的操作用户界面。当操作的数据里包含头像图片时,事情就多了一环——文件上传。

这篇从”一次提交到底发了什么”讲起,再对比几种实现方式,最后用”学生头像上传”的完整实例把前后端串起来,并补上现代写法和你必须注意的安全坑。


一、文件上传的底层原理

1. 一次请求,本质是发送一段文本

当浏览器向服务器发一次请求,本质上就是发送一段按 HTTP 协议格式组装的文本数据。普通表单提交是 application/x-www-form-urlencoded,而一旦表单里要带文件,就必须改成 multipart/form-data——它会把每个字段(包括文件)用”分隔符(boundary)”切成一段一段。

原文对 HTTP 请求格式有配图,报文结构如下:

HTTP 请求报文格式

下面用一段 multipart/form-data 请求体帮你再看清结构:

POST /addStudentServlet.do HTTP/1.1 Host: localhost Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="userName" 张三 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="number" 2021001 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; name="headerImg"; filename="avatar.png" Content-Type: image/png <这里是头像文件的二进制数据> ------WebKitFormBoundaryABC123--

注意三点:

  • 普通文本字段(userName、number)和文件字段(headerImg)被同一个 boundary 隔开;
  • 文件字段额外带 filename 和 Content-Type;
  • 文件内容就是原始二进制流,服务器要按这个格式把它”切”出来、再写到磁盘。

2. 上传 = 按格式读 + io 流写文件

所以”自己实现上传”的思路很朴素:按 HTTP 协议格式把请求体读出来,找到文件那段二进制,用 IO 流 write 到服务器磁盘。难点在于解析格式繁琐、边界情况多——所以生产环境都用现成库。


二、三种实现方式对比

方式 思路 现状
① 自己解析 HTTP 手动读流、切 boundary、写文件 适合理解原理,生产不推荐
② SmartUpload 库 一行 su.upload() 搞定 已停止维护,仅历史项目可见
③ commons-fileupload Apache 组件,解析封装到 FileItem 经典但 已 EOL(2021 起废弃),新项目用 Servlet 3.0+

1. 自己解析实现

用到两块知识:HTTP 协议(按上面格式组装/拆解请求)+ IO 流(把读到的头像二进制写文件)。理解了它,你就明白”上传”没有任何魔法。

2. SmartUpload 上传库

早年很流行,几行代码就能上传:

SmartUpload su = new SmartUpload(); su.initialize(getServletConfig(), req, resp); // 初始化对象 su.upload(); // 上传文件 su.save(file); // 保存文件

注记:SmartUpload 早已停止维护,且对中文、大文件支持差,现代项目不要再用。

3. commons-fileupload 上传库(经典方案)

下载 commons-fileupload-x.x.jar 和 `commons-io-x.x.jar(地址),核心就两个类。

① 文件项目工厂 DiskFileItemFactory

决定”小文件放内存、大文件落临时盘”:

  • DiskFileItemFactory():默认 sizeThreshold = 10KB,超过则存到临时目录(System.getProperty("java.io.tmpdir") 获取);
  • DiskFileItemFactory(int sizeThreshold, File repository):可自定义阈值和临时目录路径。

② 请求解析类 ServletFileUpload

把上传请求解析成 List<FileItem>,每个 FileItem 就是表单里的一个字段(文本或文件)。关键设置:

方法 作用
setHeaderEncoding("UTF-8") 读取请求头用的编码,防中文乱码
setSizeMax(long) 单次请求总大小上限,默认 -1 不限制
setFileSizeMax(long) 单个文件大小上限,默认 -1 不限制
ServletFileUpload sfu = new ServletFileUpload(factory); sfu.setHeaderEncoding("UTF-8"); List<FileItem> items = sfu.parseRequest(req);

注记:Apache 已于 2021 年宣布 commons-fileupload 停止维护(EOL),并建议迁移到 Servlet 3.0 内置的 Part API 或框架封装。下面的实例保留其写法以便读老代码,现代写法见第六节。


三、实战:学生头像上传实例

下面是一套**添加学生(含头像)+ 修改学生(换头像、删旧图)**的完整实现,逻辑清晰,是理解”表单 + 文件”混合提交的好样本。

1. 添加学生

前端 addstudent.jsp —— 关键是 enctype="multipart/form-data":

<div class="login-form"> <form action="./addStudentServlet.do" method="post" enctype="multipart/form-data"> <input type="text" name="userName" placeholder="请输入学生姓名"> <input type="text" name="number" placeholder="请输入学生学号"><br><br> 头像 :<input type="file" name="headerImg"/> <input type="submit" value="添加"> </form> </div>

后端 Servlet —— 把”解析请求”和”存数据库”拆成两个方法:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 存放所有请求参数(文本 + 文件相对路径) HashMap<String, String> parameterMap = new HashMap<String, String>(); // —— 上传头像,并把参数收进 hashMap —— uploadHeader(request, parameterMap); // —— 写入数据库 —— addStudent(parameterMap, request, response); } /** 解析上传请求:文本字段存值,文件字段写盘并存路径 */ public void uploadHeader(HttpServletRequest request, HashMap<String, String> parameterMap) throws UnsupportedEncodingException { ServletFileUpload servletFileUpload = new ServletFileUpload(new DiskFileItemFactory()); servletFileUpload.setHeaderEncoding("UTF-8"); List<FileItem> fileItemLists = null; try { fileItemLists = servletFileUpload.parseRequest(request); } catch (FileUploadException e) { e.printStackTrace(); } Iterator<FileItem> iterator = fileItemLists.iterator(); while (iterator.hasNext()) { FileItem fileItem = iterator.next(); if (fileItem.isFormField()) { // 普通文本字段 String fieldName = fileItem.getFieldName(); String fileValue = fileItem.getString("UTF-8"); parameterMap.put(fieldName, fileValue); } else { // 文件字段 String rootDir = getServletContext().getRealPath("/"); String fileDir = "upload/" + fileItem.getName(); File file = new File(rootDir + fileDir); FileUtil.createFile(file); try { fileItem.write(file); // 写盘 } catch (Exception e) { e.printStackTrace(); } parameterMap.put(fileItem.getFieldName(), fileDir); // 存相对路径 } } } /** 把收集到的参数写入数据库 */ private void addStudent(HashMap<String, String> parameters, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userName = parameters.get("userName"); String number = parameters.get("number"); int numbers = Integer.parseInt(number); int ages = 23; String fileUrl = parameters.get("headerImg"); System.out.println("userName:" + userName + ", number:" + number + ", fileUrl:" + fileUrl); // 这里执行数据库 INSERT }

核心思路一句话:isFormField() 区分文本和文件——文本直接取值,文件先写盘再把”相对路径”当作普通参数存起来,最后一起进数据库。

2. 修改学生(换头像)

修改比添加多一步:先上传新头像,再删掉旧头像,最后更新数据库字段。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HashMap<String, String> parameterMap = new HashMap<>(); // —— 上传新头像,参数收进 hashMap —— uploadHeader(request, parameterMap); // —— 删除原头像文件 —— deleteHeaderImg(parameterMap); // —— 更新数据库字段 —— updateStudent(parameterMap); response.sendRedirect(request.getContextPath() + "/student_list.jsp"); }

顺序很重要:先拿到新图路径,再根据数据库里的旧路径删文件,最后更新字段。若先删再传,中途失败就会”图没了字段也没更新”。


四、现代写法(推荐新项目用)

上面的实例是理解原理的教科书,但新项目请用更简洁的方案。

1. 后端:Servlet 3.0+ 的 Part API(无需任何上传库)

Servlet 3.0 起内置了文件上传,加一个 @MultipartConfig 注解即可:

@WebServlet("/upload") @MultipartConfig( fileSizeThreshold = 1024 * 1024, // 1MB 以下留内存 maxFileSize = 5 * 1024 * 1024, // 单文件最大 5MB maxRequestSize = 10 * 1024 * 1024 // 单次请求最大 10MB ) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { String userName = req.getParameter("userName"); // 文本照常取 Part filePart = req.getPart("headerImg"); // 文件用 getPart String fileName = UUID.randomUUID() + "_" + filePart.getSubmittedFileName(); // 重命名防冲突 String saveDir = getServletContext().getRealPath("/") + "upload/"; filePart.write(saveDir + fileName); // 把 userName + "upload/" + fileName 写库 } }

2. 后端:Spring Boot 的 MultipartFile

Spring 项目更直接,参数直接是对象:

@PostMapping("/students") public String addStudent(@RequestParam String userName, @RequestParam MultipartFile headerImg) throws IOException { String fileName = UUID.randomUUID() + "_" + headerImg.getOriginalFilename(); headerImg.transferTo(new File(uploadDir + fileName)); // 写库 return "redirect:/student_list"; }

3. 前端:AJAX + FormData 异步上传(带头像预览)

现代后台几乎都用异步提交,体验更好——选完图立刻预览,不必整页刷新:

<form id="stuForm"> <input type="text" name="userName" placeholder="学生姓名"> <input type="file" id="headerImg" name="headerImg" accept="image/*"> <img id="preview" src="" alt="头像预览" style="max-width:120px"> <button type="button" onclick="submitForm()">添加</button> </form> <script> // 选图即时预览 document.getElementById('headerImg').onchange = e => { const url = URL.createObjectURL(e.target.files[0]); document.getElementById('preview').src = url; }; function submitForm() { const fd = new FormData(document.getElementById('stuForm')); fetch('/students', { method: 'POST', body: fd }) .then(r => r.json()) .then(data => console.log('上传成功', data)); } </script>

五、安全与避坑(务必看)

文件上传是 Web 安全的重灾区,上线前至少做这几件事:

风险 对策
传了可执行脚本(如 .jsp/.php)被绕过后门 扩展名白名单校验,只允许 jpg/png/gif/webp 等
超大文件撑爆磁盘 设 fileSizeMax / Spring 的 max-file-size 上限
路径穿越(filename="../../web.xml") 文件名用 UUID 重命名,丢弃原始名;或用 FilenameUtils.getName() 只取最后一段
重新部署丢文件 不要存到 webapp/upload/ 下(打包会覆盖),应存项目外目录或对象存储(OSS/COS)
图片内容伪造 校验真实 Content-Type 或读取文件头(Magic Number),不只看后缀

一句话:永远不要把用户传上来的东西,原封不动地当成”可信文件”放在可被直接访问/执行的路径下。


六、结语

“表单提交 + 头像上传”看似是两件事,其实是一件事:表单提交负责文本,文件上传负责二进制,二者靠 multipart/form-data 在同一个请求里被一起打包。

  • 想懂原理:看 commons-fileupload 怎么把请求解析成 FileItem;
  • 想写新项目:用 Servlet 3.0 的 Part 或 Spring 的 MultipartFile,配 AJAX 异步上传;
  • 想上线:务必过一遍”扩展名白名单 + 大小限制 + UUID 重命名 + 对象存储”四道闸。

把这一套跑通,你就具备了后台管理系统里最典型的一类”图文混合提交”能力。

转载请注明来源:表单提交与头像上传
本文链接地址:https://ai.zhousir.top/?p=1819

上一篇:

下一篇:

回复 取消