【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 文件。

项目结构图

相关推荐
poemyang4 小时前
十年大厂员工终明白:MySQL性能优化的尽头,是对B+树的极致理解
mysql·pagecache·顺序i/o·局部性原理·b tree·b+ tree
uzong4 小时前
技术故障复盘模版
后端
GetcharZp4 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程5 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研5 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi5 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
一只爱撸猫的程序猿6 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
天宇_任6 小时前
Mysql数据库迁移到GaussDB注意事项
数据库·mysql·gaussdb
甄超锋6 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
阿华的代码王国6 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端