当前位置: 首页 > news >正文

【Spring Boot 快速入门】五、文件上传

目录

  • 文件上传
    • 简介
    • 本地存储

文件上传

简介

文件上传是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程

在使用文件上传的时候,前端界面是由表单来提交的;

在这里插入图片描述

该表单的代码为:

<form action="/upload" method="post" enctype="multipart/form-data">姓名:<input type="text" name="name"><br>年龄:<input type="text" name="age"><br>头像:<input type="file" name="image"><br><input type="submit" value="提交">
</form>

这个表单中有几个属性需要了解:

  • action="/upload":指定表单数据提交的目标 URL,这里数据会被发送到当前域名下的/upload路径。
  • method="post":设置表单数据的提交方式为 POST,适合提交包含文件或大量数据的表单(相比 GET 方式更安全,且无数据长度限制)。
  • enctype="multipart/form-data":这是文件上传时必须设置的编码类型,它会将表单数据拆分为多个部分,确保文件内容能正确传输。
  • type="file"表示这是一个文件选择控件,用户点击后可选择本地文件(这里用于上传头像)。
  • name="image"是该文件字段的标识,后端会通过image获取上传的文件数据(包括文件名、内容等)。

而后端对应的 controller 类 UploadController 的代码如下:

import com.example.demo.responed.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
public class UploadController {@PostMapping("/upload")public Result upload(String name, Integer age, MultipartFile image){return Result.success();}
}

其中的 MultipartFile 是 Spring Framework 提供的接口,用于处理 HTTP 请求中上传的文件数据,是 Spring 中处理文件上传的核心接口。它封装了上传文件的相关信息和操作方法,简化了文件上传的处理流程。

本地存储

在服务端,接收到上传的文件之后,将文件存储在本地服务器磁盘中

数据库表 user 为:

create table user (id int unsigned primary key auto_increment comment 'ID',username varchar(20) not null unique comment '用户名',name varchar(10) not null comment '姓名',gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',image varchar(255) comment '头像',create_time datetime not null comment '创建时间',update_time datetime not null comment '修改时间'
) comment '员工表';

实体类 User 为:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {private Integer id;private String username;private String name;private Short gender;private String image;private LocalDateTime createTime;private LocalDateTime updateTime;
}

mapper 接口 UserMapper 中的方法为:

import com.example.demo.pojo.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;import java.util.List;@Mapper
public interface UserMapper {@Insert("insert into user(username,name,gender,image,create_time,update_time) values(#{username},#{name},#{gender},#{image},#{createTime},#{updateTime})")public void adduser(User user);@Select("select * from user")List<User> getusers();
}

controller 类 UserController 为:

import com.example.demo.response.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;@RestController
public class UserController {@Autowiredprivate UserService userService;// 新增用户(文件上传)@PostMapping("/adduser")public Result adduser(String  username, String name, Short gender, MultipartFile image) throws Exception {userService.adduser(username, name, gender, image);return Result.success();}// 查询所有用户@GetMapping("/getusers")public Result getusers() {return Result.success(userService.getusers());}
}

service 接口 UserService 中的方法为:

import com.example.demo.pojo.User;
import org.springframework.web.multipart.MultipartFile;import java.util.List;public interface UserService {void adduser (String username, String name, Short gender, MultipartFile image) throws Exception;List<User> getusers();
}

service 实现类 UserServiceImpl 为;

import com.example.demo.mapper.UserMapper;
import com.example.demo.pojo.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic void adduser(String username, String name, Short gender, MultipartFile image) throws Exception {// 获取文件的原始文件名String originalFilename = image.getOriginalFilename();// 构造唯一的文件名(不能重复) - uuid(通用唯一识别码)int index = originalFilename.lastIndexOf(".");String ext = originalFilename.substring(index);String filename = UUID.randomUUID().toString() + ext;// 确保目录存在String uploadDir = "IDEA-workspace/demo3/images/";File dir = new File(uploadDir);if (!dir.exists()) {dir.mkdirs();}// 将文件存储在服务器的磁盘目录中image.transferTo(new File(uploadDir + filename));String path = "/images/" + filename;User user = new User(null, username, name, gender, path, LocalDateTime.now(), LocalDateTime.now());userMapper.adduser(user);}@Overridepublic List<User> getusers() {return userMapper.getusers();}
}

getOriginalFilename()transferTo()MultipartFile 中封装的两个方法。

通过 uuid 来生成唯一识别码,防止旧文件被文件名重复的新文件覆盖:

在这里插入图片描述

存储目录的路径推荐使用绝对路径,相较于相对路径要安全。

目前存在一个问题:上传路径保存的是本地磁盘路径,不适合前端访问,前端拿到的路径 /Users/xxx/... 是服务器的本地路径,浏览器无法访问这个路径。

Spring Boot 提供了静态资源映射机制来解决这个问题,可以把某个磁盘目录(如 /Users/.../images)映射为浏览器可以访问的 URL 地址(如 http://localhost:8080/images/xxx.jpg)。

需要创建一个配置类 WebConfig:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 访问路径:浏览器访问 /images/xxx.jpg// 映射目录:磁盘路径 IDEA-workspace/demo3/images/registry.addResourceHandler("/images/**").addResourceLocations("file:IDEA-workspace/demo3/images/");}
}

至此,已经实现了文件上传——本地存储的功能。

在上传文件时可能会遇到上传受限的问题,这是因为文件上传默认大小不能超过 1MB 的限制,可以通过在 application.properties 中添加配置来解决问题:

# 配置单个文件上传大小的限制
spring.servlet.multipart.max-file-size=10MB# 配置总上传大小的限制
spring.servlet.multipart.max-request-size=100MB

以下是一个前端页面,可以用来测试完整的功能:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户注册测试页面</title><style>* {margin: 0;padding: 0;box-sizing: border-box;}body {font-family: 'Arial', sans-serif;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);min-height: 100vh;padding: 20px;}.main-container {display: flex;gap: 30px;max-width: 1200px;margin: 0 auto;align-items: flex-start;}.container {background: white;padding: 40px;border-radius: 15px;box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);width: 100%;max-width: 500px;flex: 1;}.user-list-container {background: white;padding: 40px;border-radius: 15px;box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);width: 100%;max-width: 600px;flex: 1;}.title {text-align: center;color: #333;margin-bottom: 30px;font-size: 28px;font-weight: 600;}.form-group {margin-bottom: 20px;}.form-group label {display: block;margin-bottom: 8px;color: #555;font-weight: 500;}.form-group input,.form-group select {width: 100%;padding: 12px 15px;border: 2px solid #e1e1e1;border-radius: 8px;font-size: 16px;transition: border-color 0.3s ease;}.form-group input:focus,.form-group select:focus {outline: none;border-color: #667eea;}.file-upload {position: relative;display: inline-block;width: 100%;}.file-upload input[type=file] {position: absolute;opacity: 0;width: 100%;height: 100%;cursor: pointer;}.file-upload-label {display: block;padding: 12px 15px;border: 2px dashed #e1e1e1;border-radius: 8px;text-align: center;cursor: pointer;transition: all 0.3s ease;background: #f9f9f9;}.file-upload-label:hover {border-color: #667eea;background: #f0f4ff;}.file-upload-label.has-file {border-color: #28a745;background: #f0fff4;color: #28a745;}.submit-btn {width: 100%;padding: 15px;background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);color: white;border: none;border-radius: 8px;font-size: 18px;font-weight: 600;cursor: pointer;transition: transform 0.2s ease;}.submit-btn:hover {transform: translateY(-2px);}.submit-btn:disabled {opacity: 0.6;cursor: not-allowed;transform: none;}.message {margin-top: 20px;padding: 12px;border-radius: 8px;text-align: center;font-weight: 500;}.message.success {background: #d4edda;color: #155724;border: 1px solid #c3e6cb;}.message.error {background: #f8d7da;color: #721c24;border: 1px solid #f5c6cb;}.preview-image {max-width: 200px;max-height: 200px;margin-top: 10px;border-radius: 8px;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);}.refresh-btn {width: 100%;padding: 12px;background: linear-gradient(135deg, #28a745 0%, #20c997 100%);color: white;border: none;border-radius: 8px;font-size: 16px;font-weight: 600;cursor: pointer;transition: transform 0.2s ease;margin-bottom: 20px;}.refresh-btn:hover {transform: translateY(-2px);}.user-list {max-height: 500px;overflow-y: auto;}.user-item {display: flex;align-items: center;padding: 15px;border: 1px solid #e1e1e1;border-radius: 8px;margin-bottom: 10px;transition: all 0.3s ease;}.user-item:hover {box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);transform: translateY(-2px);}.user-avatar {width: 60px;height: 60px;border-radius: 50%;object-fit: cover;margin-right: 15px;border: 2px solid #e1e1e1;}.user-info {flex: 1;}.user-name {font-size: 18px;font-weight: 600;color: #333;margin-bottom: 5px;}.user-username {font-size: 14px;color: #666;margin-bottom: 3px;}.user-gender {font-size: 14px;color: #888;}.no-users {text-align: center;color: #666;font-style: italic;padding: 40px 20px;}.loading {text-align: center;color: #667eea;font-weight: 500;padding: 20px;}</style>
</head>
<body><div class="main-container"><!-- 用户注册模块 --><div class="container"><h1 class="title">用户注册测试</h1><form id="userForm" enctype="multipart/form-data"><div class="form-group"><label for="username">用户名:</label><input type="text" id="username" name="username" required placeholder="请输入用户名"></div><div class="form-group"><label for="name">姓名:</label><input type="text" id="name" name="name" required placeholder="请输入真实姓名"></div><div class="form-group"><label for="gender">性别:</label><select id="gender" name="gender" required><option value="">请选择性别</option><option value="1"></option><option value="2"></option></select></div><div class="form-group"><label for="image">头像:</label><div class="file-upload"><input type="file" id="image" name="image" accept="image/*" required><label for="image" class="file-upload-label" id="fileLabel">点击选择图片文件</label></div><img id="imagePreview" class="preview-image" style="display: none;"></div><button type="submit" class="submit-btn" id="submitBtn">提交注册</button></form><div id="message" class="message" style="display: none;"></div></div><!-- 用户列表模块 --><div class="user-list-container"><h1 class="title">用户列表</h1><button class="refresh-btn" id="refreshBtn">刷新用户列表</button><div id="userListContent" class="user-list"><div class="loading">加载中...</div></div></div></div><script>const form = document.getElementById('userForm');const fileInput = document.getElementById('image');const fileLabel = document.getElementById('fileLabel');const imagePreview = document.getElementById('imagePreview');const submitBtn = document.getElementById('submitBtn');const messageDiv = document.getElementById('message');const refreshBtn = document.getElementById('refreshBtn');const userListContent = document.getElementById('userListContent');// 文件选择处理fileInput.addEventListener('change', function(e) {const file = e.target.files[0];if (file) {fileLabel.textContent = file.name;fileLabel.classList.add('has-file');// 图片预览const reader = new FileReader();reader.onload = function(e) {imagePreview.src = e.target.result;imagePreview.style.display = 'block';};reader.readAsDataURL(file);} else {fileLabel.textContent = '点击选择图片文件';fileLabel.classList.remove('has-file');imagePreview.style.display = 'none';}});// 表单提交处理form.addEventListener('submit', async function(e) {e.preventDefault();const formData = new FormData();formData.append('username', document.getElementById('username').value);formData.append('name', document.getElementById('name').value);formData.append('gender', document.getElementById('gender').value);formData.append('image', fileInput.files[0]);submitBtn.disabled = true;submitBtn.textContent = '提交中...';messageDiv.style.display = 'none';try {const response = await fetch('/adduser', {method: 'POST',body: formData});const result = await response.json();if (result.code === 200) {showMessage('注册成功!', 'success');form.reset();fileLabel.textContent = '点击选择图片文件';fileLabel.classList.remove('has-file');imagePreview.style.display = 'none';// 注册成功后刷新用户列表loadUserList();} else {showMessage('注册失败: ' + (result.message || '未知错误'), 'error');}} catch (error) {console.error('Error:', error);showMessage('网络错误,请稍后重试', 'error');} finally {submitBtn.disabled = false;submitBtn.textContent = '提交注册';}});function showMessage(text, type) {messageDiv.textContent = text;messageDiv.className = 'message ' + type;messageDiv.style.display = 'block';// 3秒后自动隐藏消息setTimeout(() => {messageDiv.style.display = 'none';}, 3000);}// 加载用户列表async function loadUserList() {try {userListContent.innerHTML = '<div class="loading">加载中...</div>';const response = await fetch('/getusers');const result = await response.json();if (result.code === 200 && result.data) {displayUserList(result.data);} else {userListContent.innerHTML = '<div class="no-users">加载用户列表失败</div>';}} catch (error) {console.error('Error loading user list:', error);userListContent.innerHTML = '<div class="no-users">网络错误,无法加载用户列表</div>';}}// 显示用户列表function displayUserList(users) {if (!users || users.length === 0) {userListContent.innerHTML = '<div class="no-users">暂无用户数据</div>';return;}const userListHTML = users.map(user => {const genderText = user.gender === 1 ? '男' : user.gender === 2 ? '女' : '未知';const avatarSrc = user.image || '/images/default-avatar.png';return `<div class="user-item"><img src="${avatarSrc}" alt="头像" class="user-avatar" onerror="this.src='/images/default-avatar.png'"><div class="user-info"><div class="user-name">${user.name || '未知'}</div><div class="user-username">用户名: ${user.username || '未知'}</div><div class="user-gender">性别: ${genderText}</div></div></div>`;}).join('');userListContent.innerHTML = userListHTML;}// 刷新按钮点击事件refreshBtn.addEventListener('click', loadUserList);// 页面加载完成后自动加载用户列表document.addEventListener('DOMContentLoaded', loadUserList);</script>
</body>
</html>

测试结果如下:

在这里插入图片描述

http://www.dtcms.com/a/310929.html

相关文章:

  • 图漾相机-ROS1_SDK_ubuntu 4.X.X版本编译
  • Shell【脚本 02】离线安装配置Zookeeper及Kafka并添加service服务和开机启动(脚本分析)
  • [硬件电路-122]:模拟电路 - 信号处理电路 - 模拟电路与数字电路、各自的面临的难题对比?
  • [硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
  • 编程与数学 03-002 计算机网络 20_计算机网络课程实验与实践
  • filezilla出现connected refused的时候排查问题
  • Flink2.0学习笔记:Stream API 窗口
  • 鸿蒙智选携手IAM进驻长隆熊猫村,为国宝打造智慧健康呼吸新空间
  • 智能合约漏洞导致的损失,法律责任应如何分配
  • Hyperliquid:揭秘高性能区块链共识引擎HyperBFT
  • 入门MicroPython+ESP32:《点亮LED灯》
  • 1.7vue声明周期
  • Token系列 - 再谈稳定币
  • 保证金率(Margin Ratio)
  • 【最新区块链论文录用资讯】CCF A--WWW 2025 23篇
  • WebForms 简介
  • Redis 核心概念、命令详解与应用实践:从基础到分布式集成
  • 通过filezilla在局域网下实现高速传输数据
  • Selenium自动化:轻松实现网页操控
  • sqli-labs:Less-21关卡详细解析
  • C/C++常用字符串函数
  • 仿muduo库实现高并发服务器
  • 利用DeepSeek将Rust程序的缓冲输出改写为C语言实现提高输出效率
  • 自动化革命:软件开发的引擎与未来蓝图
  • carla-0.10.0 矿山地图和autoware联调
  • Rust进阶-part2-泛型
  • VAST视频广告技术实现:从零开始搭建视频广告投放系统
  • 大模型笔记1——李宏毅《2025机器学习》第一讲
  • 中科院自动化所机器人视觉中的多模态融合与视觉语言模型综述
  • 【Java】在一个前台界面中动态展示多个数据表的字段及数据