【SpringBoot】19 文件/图片下载(MySQL + Thymeleaf)

Git仓库

https://gitee.com/Lin_DH/system

介绍

从 MySQL 中,下载保存的 blob 格式的文件。

代码实现

第一步:配置文件

application.yml

yml 复制代码
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: root
 mybatis-plus:
  type-aliases-package: com.lm.system.common
  mapper-locations: classpath:com.lm.system/mapper/*Mapper.xml
  check-config-location: true
  configuration:
    #日志实现,不配置不会输出SQL日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

第二步:编写实体类

SysFile.java

java 复制代码
package com.lm.system.common;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author DUHAOLIN
 * @date 2024/10/17
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysFile {

    @TableId(value = "id", type = IdType.INPUT)
    private Integer id;
    private String name;
    private String format;
    private byte[] data;
    private long size;
    private Date createTime;

}

第三步:编写dao层接口

SysFileMapper.java

java 复制代码
package com.lm.system.mapper;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.lm.system.common.SysFile;

import java.util.List;

/**
 * @author DUHAOLIN
 * @date 2024/10/17
 */
//@DS("system")
public interface SysFileMapper {

    int insertFile(SysFile file);

    List<SysFile> queryFiles();

    SysFile queryFileById(Integer id);

}

第四步:编写dao层实现SQL

SysFileMapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lm.system.mapper.SysFileMapper">

    <resultMap id="files" type="com.lm.system.common.SysFile">
        <id property="id" column="id" jdbcType="INTEGER" />
        <result property="name" column="name" jdbcType="VARCHAR" />
        <result property="format" column="format" jdbcType="VARCHAR" />
        <result property="data" column="data" jdbcType="BLOB" />
        <result property="size" column="size" jdbcType="DOUBLE" />
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP" />
    </resultMap>

    <insert id="insertFile" parameterType="com.lm.system.common.SysFile">
        INSERT INTO t_file (NAME, FORMAT, DATA, SIZE)
        VALUES (#{name}, #{format}, #{data}, #{size})
    </insert>

    <select id="queryFiles" resultMap="files">
        SELECT ID, NAME, FORMAT, `SIZE`, CREATE_TIME
        FROM t_file
    </select>

<!--    SELECT ID, NAME, FORMAT,-->
<!--    (CASE WHEN `SIZE` <![CDATA[<]]> 1024 THEN CONCAT(`SIZE`, ' b')-->
<!--    WHEN `SIZE` <![CDATA[>=]]> 1024 AND `SIZE` <![CDATA[<]]> 1024000 THEN CONCAT(ROUND(`SIZE` / 1024, 2), ' KB')-->
<!--    WHEN `SIZE` <![CDATA[>=]]> 1024000 AND `SIZE` <![CDATA[<]]> 1024000000 THEN CONCAT(ROUND(`SIZE` / 1024000, 2), ' MB')-->
<!--    END) `SIZE`,-->
<!--    DATE_FORMAT(CREATE_TIME, '%Y-%m-%d %h:%i:%s') CREATE_TIME-->
<!--    FROM t_file-->

    <select id="queryFileById" resultType="com.lm.system.common.SysFile">
        SELECT ID, NAME, FORMAT, DATA, `SIZE`, CREATE_TIME
        FROM t_file
        WHERE ID = #{id}
    </select>

</mapper>

第五步:编写下载页面

download.html

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head lang="en">
    <meta charset="UTF-8" />
    <title>文件下载页面</title>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
</head>
<body>
<h1>文件下载页面</h1>
<table border="1">
    <!--  表头  -->
    <thead>
        <tr>
            <th>#</th>
            <th>ID</th>
            <th>文件名</th>
            <th>格式</th>
            <th>文件大小</th>
            <th>创建时间</th>
            <th>操作</th>
        </tr>
    </thead>
    <!--  表体  -->
    <tbody>
        <tr th:each="file,stats:${files}">
            <td th:text="${stats.count}"></td>
            <td th:text="${file.id}"></td>
            <td th:text="${file.name}"></td>
            <td th:text="${file.format}"></td>
            <td th:if="${file.size} < 1024" th:text="${#numbers.formatDecimal(file.size, 0, 0)} + ' b'"></td>
            <td th:if="${file.size} >= 1024 and ${file.size} < 1024000" th:text="${#numbers.formatDecimal(file.size/1024, 0, 0)} + ' KB'"></td>
            <td th:if="${file.size} >= 1024000 and ${file.size} < 1024000000" th:text="${#numbers.formatDecimal(file.size/1024000, 0, 0)} + ' MB'"></td>
            <td th:text="${#dates.format(file.createTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
            <td>
                <button th:onclick="'downloadFile(\'' + ${file.id} + '\');'">下载</button>
            </td>
        </tr>
    </tbody>
</table>

<script th:inline="javascript">
    let files = [[${files}]];

    function downloadFile(id) {
        $.ajax({
            url: '/downloadFile/' + id,
            type: 'GET',
            responseType: 'blob',
            success: function (res, status, xhr) {
                let link = document.createElement("a");
                link.href = this.url;
                let filename = xhr.getResponseHeader("Content-Disposition").split("attachment; filename=")[1];
                link.setAttribute("download", filename);
                link.click();
                window.URL.revokeObjectURL(link.href); //释放内存
            },
            error: function (xhr, status, error) {
                console.log("error", error);
            }
        });
    }
</script>

</body>
</html>

第七步:编写文件 Controller 类

FileController.java

java 复制代码
package com.lm.system.controller;

import com.lm.system.common.SysFile;
import com.lm.system.exception.FileException;
import com.lm.system.mapper.SysFileMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;


/**
 * @author DUHAOLIN
 * @date 2024/10/15
 */
@Controller
public class FileController {

    @Resource
    private SysFileMapper fileMapper;

    private final static String FILE_FORMAT_TXT = "txt";
    private final static String FILE_FORMAT_JPEG = "jpg";
    private final static String FILE_FORMAT_PNG = "png";


    @Value("${file.upload.path}")
    private String path;

    @GetMapping("uploadPage")
    public String uploadPage() {
        return "upload";
    }

    @PostMapping("upload")
    @ResponseBody
    public String upload(@RequestParam("file") MultipartFile file) throws IOException {
        //校验文件
        try {
            String[] fileFormats = { FILE_FORMAT_TXT };
            checkFile(file, fileFormats);
        } catch (FileException e) {
            e.printStackTrace();
            return e.getMessage();
        }

        String filename = path + file.getOriginalFilename().replace(FILE_FORMAT_TXT, "_" + System.currentTimeMillis() + FILE_FORMAT_TXT);
        java.io.File newFile = new java.io.File(filename);
        Files.copy(file.getInputStream(), newFile.toPath());
        return "新文件已生成," + newFile.getAbsolutePath();
    }

    private void checkFile(MultipartFile file, String[] fileFormats) {
        //校验文件大小
        checkSize(file.getSize());

        //校验文件名
        checkFilename(file.getOriginalFilename());

        //校验文件格式
        checkFileFormat(file.getOriginalFilename(), fileFormats);
    }

    private void checkSize(long size) {
        if (size > 10485760L) //10MB
            throw new FileException("文件大于10MB");
    }

    private void checkFilename(String filename) {
        if (!StringUtils.hasText(filename))
            throw new FileException("文件名有误");
    }

    private void checkFileFormat(String filename, String[] fileFormats) {
        int i = filename.lastIndexOf(".");
        String suffix = filename.substring(i + 1); //文件后缀
        long c = Arrays.stream(fileFormats).filter(s -> s.equals(suffix)).count(); //判断是否存在该文件后缀
        if (c < 1) throw new FileException("文件格式有误,该文件类型为:" + suffix);
    }

    @GetMapping("multiFileUploadPage")
    public String multiFileUploadPage() {
        return "multiFileUpload";
    }

    @PostMapping("multiFileUpload")
    @ResponseBody
    public String multiFileUpload(@RequestParam("files") MultipartFile[] files) throws IOException {
        StringBuilder sb = new StringBuilder();

        for (MultipartFile file : files) {
            //校验文件
            boolean b = true;
            try {
                String[] fileFormats = new String[] { FILE_FORMAT_TXT };
                checkFile(file, fileFormats);
            } catch (FileException e) {
                e.printStackTrace();
                sb.append(file.getOriginalFilename()).append(e.getMessage()).append("<br>");
                b = false;
            }

            if (b) { //文件格式不对则不进行上传
                String filename = path + file.getOriginalFilename().replace(FILE_FORMAT_TXT, "_" + System.currentTimeMillis() + FILE_FORMAT_TXT);
                java.io.File newFile = new java.io.File(filename);
                Files.copy(file.getInputStream(), newFile.toPath());
                sb.append("新文件已生成,").append(newFile.getAbsolutePath()).append("<br>");
            }
        }

        return sb.toString();
    }

    @GetMapping("uploadToDBPage")
    public String uploadToDBPage() {
        return "uploadToDB";
    }

    @PostMapping("uploadToDB")
    @ResponseBody
    public String uploadToDB(@RequestParam("file") MultipartFile file) throws IOException {
        //校验文件
        try {
            String[] fileFormats = { FILE_FORMAT_TXT, FILE_FORMAT_JPEG, FILE_FORMAT_PNG };
            checkFile(file, fileFormats);
        } catch (FileException e) {
            e.printStackTrace();
            return e.getMessage();
        }

        //构建存储对象
        SysFile sysFile = SysFile.builder()
                .name(getFilename(file.getOriginalFilename()))
                .format(getFileFormat(file.getOriginalFilename()))
                .data(file.getBytes())
                .size(file.getSize())
                .build();

        int i = fileMapper.insertFile(sysFile);

        return i > 0 ? "添加成功" : "添加失败";
    }

    private String getFileFormat(String filename) {
        int i = filename.lastIndexOf(".");
        return filename.substring(i + 1); //文件后缀
    }

    /**
     * 获取不带后缀的文件名
     */
    private String getFilename(String originalFilename) {
        int i = originalFilename.lastIndexOf(".");
        return originalFilename.substring(0, i);
    }


    @GetMapping("downloadPage")
    public String downloadPage(ModelMap map) {
        List<SysFile> files = fileMapper.queryFiles();
        map.addAttribute("files", files);
        return "download";
    }

    @GetMapping("downloadFile/{id}")
    public void downloadFile(@PathVariable Integer id, HttpServletResponse response) throws IOException {
        SysFile sysFile = fileMapper.queryFileById(id);
        String filename = sysFile.getName() + "_" + System.currentTimeMillis() + "." + sysFile.getFormat();
        //指定下载文件名
        response.setHeader("Content-Disposition", "attachment; filename=" + filename);
        response.setCharacterEncoding("UTF-8");
        //告知浏览器文件大小
        response.addHeader("Content-Length", String.valueOf(sysFile.getSize()));
        //内容类型为通用类型,表示二进制数据流
        response.setContentType(getContentType(sysFile.getFormat()));

        InputStream is = new ByteArrayInputStream(sysFile.getData());
        OutputStream os = response.getOutputStream();
        byte[] buffer = new byte[1024];
        int l = 0;
        while ((l = is.read(buffer)) != -1) {
            os.write(buffer, 0, l);
        }
        is.close();
        os.close();
    }

    private String getContentType(String format) {
        switch (format) {
            case FILE_FORMAT_TXT:
                return MediaType.TEXT_PLAIN_VALUE;
            case FILE_FORMAT_JPEG:
                return MediaType.IMAGE_JPEG_VALUE;
            case FILE_FORMAT_PNG:
                return MediaType.IMAGE_PNG_VALUE;
            default:
                return MediaType.APPLICATION_OCTET_STREAM_VALUE; //通用类型
        }
    }

}

效果图

成功下载 txt 文件。

成功下载 jpg 文件。

成功下载 png 文件。

项目结构图

相关推荐
小牛itbull1 分钟前
ReactPress 安装指南:从 MySQL 安装到项目启动
前端·javascript·数据库·mysql·react.js·开源·reactpress
友大冰32 分钟前
Go 语言已立足主流,编程语言排行榜24 年 11 月
开发语言·后端·golang
lcz-200033 分钟前
IDEA一键部署SpringBoot项目到服务器
java·spring boot·intellij-idea
尘浮生33 分钟前
Java项目实战II基于微信小程序的原创音乐小程序(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·maven
蜜桃小阿雯1 小时前
JAVA开源项目 微服务在线教育系统 计算机毕业设计
java·开发语言·spring boot·微服务·java-ee·开源·maven
三杯温开水1 小时前
基于 CentOS7.6 的 Docker 下载常用的容器(MySQL&Redis&MongoDB),解决拉取容器镜像失败问题
redis·mysql·docker
chusheng18401 小时前
Python 正则表达式进阶用法:量词与范围
python·mysql·正则表达式
Desmend__1 小时前
正则表达式那些事儿
数据库·mysql·正则表达式
hummhumm2 小时前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
程序媛小果2 小时前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot