通過這篇文章你可以了解到:
- 使用 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="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
