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

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

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

[TOC]

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.util

public class Constants {
    // 定义自己的图片文件保存路径
    public static final String IMG_PATH = "D:\\your_file_path\\";
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,650评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,802评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,056评论 25 707
  • 银龙 2014.09.05 何方喜奏衷肠曲,空闻满地传哀声。 频入云宵随鹤雁,音飞天外困龙鹰。 花前难得伤心事,月...
    醉仙王子阅读 214评论 0 0