Spring MVC 上传、下载、显示图片

时间:2021-05-15 14:37:23

通过这篇文章你可以了解到:

  1. 使用 SpringMVC 框架,上传图片,并将上传的图片保存到文件系统,并将图片路径持久化到数据库
  2. 在 JSP 页面上实现显示图片、下载图片

1. 准备工作

首先我们需要准备好开发环境,本文测试环境是 SSM(Spring 4.3.9 + SpringMVC 4.3.9 + MyBatis 3.4.4) ,数据库为 MySQL 5.5,数据库连接池 C3P0 0.9.5.2,构建包 Maven 3.5.0,Tomcat 8.5。

限于篇幅原因,关于 SSM 框架的整合方法,在这篇文章中就不做详细的讲解啦,有关图片上传和下载的相关配置,我会特别标注出来说明的。

我们假定有这样一个很常见的需求场景:用户注册。

首先我们来做一下简单的业务分析,在注册页面,用户填写自己的相关信息,然后选择上传头像图片,注册成功后显示个人信息,并将图片显示在页面上。

一看就是一个很简单的需求吧,那我们就来做相应的数据准备工作吧。

1.1 数据库表准备

数据库非常简单,就一张表:t_user

字段 类型 长度 主键 描述
user_id int 11 PK,自增 用户表主键
user_name varchar 50 用户名
user_tel varchar 20 手机号
user_password varchar 20 密码
user_pic varchar 255 用户头像地址

1.2 实体类 User 和 Mapper(DAO)

对应数据库表 t_user 创建实体类:User

这里我使用 mybatis-generate 代码生成器根据 t_user 表结构自动生成实体类 和 Mybatis 的 mapper 文件。

User 实体类的代码如下(省略了 getter/setter):

package com.uzipi.entity;

public class User {
private Integer userId;
private String userName;
private String userTel;
private String userPassword;
private String userPic;
}

生成的 dao 层 java 代码如下:

package com.uzipi.dao;

import com.uzipi.entity.User;
import org.mybatis.spring.annotation.MapperScan; @MapperScan // 允许 Spring 扫描该 Mapper
public interface UserMapper {
// 删除指定 key 的记录
int deleteByPrimaryKey(Integer userId); // 插入一条记录(完整记录)
int insert(User record); // 插入一条记录(对象中有值时写入字段,没有值的置空)
int insertSelective(User record); // 查询指定 key 的记录
User selectByPrimaryKey(Integer userId); // 将对象中的内容更新入库(对象中有值时更新字段,没有值的属性不修改)
int updateByPrimaryKeySelective(User record); // 将对象中的内容更新入库(全属性)
int updateByPrimaryKey(User record);
}

生成的 mapper.xml 文件内容比较多,在文章里就不展示了,后面附件中提供了下载文件供参考。

1.3 pom.xml 依赖包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.uzipi</groupId>
<artifactId>house</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>house Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<!-- junit 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- log4j 日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- MySQL 数据库连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.24</version>
</dependency>
<!-- c3p0 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!-- spring 支持的 json -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.7</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
</dependency>
<!-- MyBatis 与 Spring 整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!-- Servlet API需求包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP相关 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 标准标签库 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<finalName>house</finalName>
</build>
</project>

1.4 SSM 框架的整合配置

框架的整合配置 xml 文件请查看附件。

这里我特别说明一下涉及到图片(文件)上传相关的 spring-mvc 配置:

<!-- 配置文件上传 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 配置文件上传的最大体积 10M -->
<property name="maxUploadSize" value="10240000"></property>
</bean>

2. 控制器 UserController

package com.uzipi.controller;

import com.uzipi.entity.User;
import com.uzipi.service.UserService;
import com.uzipi.util.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse;
import java.io.*; @Controller
@RequestMapping("/user")
public class UserController { @Autowired
private UserService userService; // Spring 注入 UserService /**
* 跳转到注册页面
* @param model
* @return
*/
@RequestMapping(value="/register", method = RequestMethod.GET)
public String register(Model model){
/*
为什么这里要 new 一个 User 对象?
因为我们在 JSP 页面中使用了 spring form 标签
spring form 标签的 modelAttribute 默认需要一个对象用于接收数据
这里我们是新增,所以用无参构造创建一个空对象(不是null)
*/
User user = new User();
model.addAttribute("user", user); // user 加入到 request 域
return "user/register"; // 跳转到 user/register.jsp 页面
} /**
* 处理用户注册的表单请求
* @param user
* @param file
* @return
*/
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String doRegister(User user,
@RequestParam("imgFile") MultipartFile file,
Model model){
if (userService.saveRegister(user, file)){
model.addAttribute("user", user);
return "user/show"; // 注册成功,跳转到显示页面
}
return "redirect:/user/register"; // 注册失败,重定向到注册页面
} /**
* 处理图片显示请求
* @param fileName
*/
@RequestMapping("/showPic/{fileName}.{suffix}")
public void showPicture(@PathVariable("fileName") String fileName,
@PathVariable("suffix") String suffix,
HttpServletResponse response){
File imgFile = new File(Constants.IMG_PATH + fileName + "." + suffix);
responseFile(response, imgFile);
} /**
* 处理图片下载请求
* @param fileName
* @param response
*/
@RequestMapping("/downloadPic/{fileName}.{suffix}")
public void downloadPicture(@PathVariable("fileName") String fileName,
@PathVariable("suffix") String suffix,
HttpServletResponse response){
// 设置下载的响应头信息
response.setHeader("Content-Disposition",
"attachment;fileName=" + "headPic.jpg");
File imgFile = new File(Constants.IMG_PATH + fileName + "." + suffix);
responseFile(response, imgFile);
} /**
* 响应输出图片文件
* @param response
* @param imgFile
*/
private void responseFile(HttpServletResponse response, File imgFile) {
try(InputStream is = new FileInputStream(imgFile);
OutputStream os = response.getOutputStream();){
byte [] buffer = new byte[1024]; // 图片文件流缓存池
while(is.read(buffer) != -1){
os.write(buffer);
}
os.flush();
} catch (IOException ioe){
ioe.printStackTrace();
}
} }

在 Controller 中,有几个地方是需要我们注意的,不然会遇到坑:

  • 当有多个文件上传时,如果用 MultipartFile 接口来接收,最好是用注解 @RequestParam("inputName") 指明该文件对应表单中的 input 标签的 name 属性。如果 name 都是同名的,可以使用 ``MultipartFile []` 文件数组来接收。
  • 注意看处理显示图片和下载图片的请求映射中,我用 {fileName}.{suffix} 这段代码将图片名和图片的后缀区分开,因为 GET 方式的 URL 请求地址中的 "." 点号会被当作通配符处理掉,有多种方式可以解决。我这种方式是一种,你也可以用 "." 转义字符来避免其通配符的作用。
  • 处理图片显示和图片下载的请求区别在于:是否设置了下载响应头 response.setHeader("Content-Disposition","attachment;fileName=" + "headPic.jpg"); 当设置了该响应头时,使用 response 输出流将会被当作附件提供给客户端下载,反之就是将流中的内容输出到页面上。
  • 处理图片流时,要注意 buffer 的大小,过小会导致下载速度变慢,过大会占用较多的带宽,需要考虑平衡。

3. 业务层 UserService

package com.uzipi.service;

import com.uzipi.dao.UserMapper;
import com.uzipi.entity.User;
import com.uzipi.util.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import java.io.File;
import java.io.IOException;
import java.util.UUID; @Service
public class UserService { @Autowired
private UserMapper userMapper; // Spring 注入 UserMapper 对象 /**
* 用户注册,记录用户信息并处理上传的图片
* @param user
* @param file
* @return
*/
public boolean saveRegister(User user, MultipartFile file){
if (file != null){
// 原始文件名
String originalFileName = file.getOriginalFilename();
// 获取图片后缀
String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
// 生成图片存储的名称,UUID 避免相同图片名冲突,并加上图片后缀
String fileName = UUID.randomUUID().toString() + suffix;
// 图片存储路径
String filePath = Constants.IMG_PATH + fileName;
File saveFile = new File(filePath);
try {
// 将上传的文件保存到服务器文件系统
file.transferTo(saveFile);
// 记录服务器文件系统图片名称
user.setUserPic(fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
// 持久化 user
return userMapper.insertSelective(user) > 0;
} /**
* 查找指定 key 的 user 对象
* @param userId
* @return
*/
public User findByUserId(int userId){
return userMapper.selectByPrimaryKey(userId);
}
}

Service 层中要注意的几个问题:

  • 我们在向数据库存入图片的路径记录时,最好是将文件名和后缀名也一并记录。这里有两种方案供参考:(1)将文件名和后缀名存入一个字段(例子中用到的方案);(2)文件名存入一个字段,后缀名存入一个字段,方便后期筛选不同的文件格式,可以对图片文件进行读取和分类查询分析等操作。
  • 上传的原始文件名存在命名冲突的问题,为了避免文件名冲突被覆盖,我们可以使用 UUID 来生成唯一的文件名,如果有时候业务需要保存原始文件名的话,可以考虑在数据库表中再增加一个字段用于持久化原始的文件名。
  • 文件刚上传上来时,是存储在临时目录中,我们可以在 spring-mvc.xml 中配置临时目录的位置。但存储在临时目录中的图片并不长久,重启服务器之后会被清理掉。我们可以利用 MultipartFile 接口提供的 transferTo(File dest) 方法将临时文件转移到我们设置的文件系统目录中。

4. JSP 页面

页面没有加样式,仅实现了功能,所以不是很好看啦。

4.1 用户注册页面 register.jsp

注册页面中使用了 spring form 标签。关于 spring form 标签,这里简单提一下,在没有 减轻 JSP 代码工作量 的需求前提下,还是推荐使用原生的 form 表单标签,因为 spring form 最终还是会被渲染成原生的 form 标签的样子,中间多了一道转换,必然会降低些许页面的渲染速度。

register.jsp 代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>用户注册</title>
<base href="<%=request.getContextPath()%>/"/>
<style>
li {list-style: none;}
</style>
</head>
<body>
<form:form action="user/register" method="post" enctype="multipart/form-data" modelAttribute="user">
<li>
<form:input path="userName" placeholder="用户名"/>
</li>
<li>
<form:password path="userPassword" placeholder="密码"/>
</li>
<li>
<form:input path="userTel" placeholder="手机号"/>
</li>
<li>
<input type="file" name="imgFile" />
</li>
<li>
<input type="submit" value="注册" />
</li>
</form:form>
</body>
</html>

register.jsp 需要注意的地方:

  • 涉及到文件上传,form 标签就需要加上 enctype="multipart/form-data" ,这大家应该都知道吧。
  • 使用了 spring form 标签,需要 modelAttribute="user" 这段属性。因此我们要在跳转到该页面之前,往 request 域中添加一个 user 对象(名字可以自定义),如果不写上这个属性,SpringMVC会默认给一个 "command"。
  • 假如 modelAttribute 对象中有引用类型的成员属性,恰好我们要填写的表单元素中有一个值正好是该引用对象的属性值,我们可以直接使用 xxx.xxx 的形式指明该属性值,提交表单时,springMVC 会自动帮助我们封装该属性对象。

4.2 用户信息显示页面 show.jsp

用户显示页面比较简单,主要是为了区分出 “显示图片” 和 “下载图片” 两种请求。

show.jsp 代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>用户个人信息</title>
<base href="<%=request.getContextPath()%>/"/>
<style>
li {list-style: none;}
</style>
</head>
<body>
<h4>个人信息</h4>
<li>
<!-- 头像显示 -->
<img src="user/showPic/${user.userPic}" style="width:100px; height: 100px;"/>
</li>
<li>
用户名:${user.userName}
</li>
<li>
手机号:${user.userTel}
</li>
<li>
<a href="user/downloadPic/${user.userPic}">下载头像图片</a>
</li>
</body>
</html>

页面比较简单,就一个地方可以说明下,可能有的同学还不太明白:

我在 <head> 标签中加入了 <base href="<%=request.getContextPath()%>/"/> 这段代码,目的是为了将当前页面的相对位置定位到 webapp 的根目录下,这样可以避免请求跳转之后,出现同一个 JSP 页面的相对路径不一样的情况。

到这里,关于 SpringMVC 上传和下载图片的步骤就算结束啦。

如果各位同学在测试的过程中遇到什么问题,可以留言、邮箱(yotow@foxmail.com)或者QQ我(281901158)。

源代码当然是少不了的啦,java 代码和 SQL 文件打包在一起了。

百度网盘: https://pan.baidu.com/s/1c3SSvj6  密码:goma