通过这篇文章你可以了解到:
- 使用 SpringMVC 框架,上传图片,并将上传的图片保存到文件系统,并将图片路径持久化到数据库
- 在 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="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