【Java Web 快速入门】五、文件上传
文件上传
简介
文件上传是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
在使用文件上传的时候,前端界面是由表单来提交的;

该表单的代码为:
<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 {
@Autowired
private 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 {
@Autowired
private UserMapper userMapper;
@Override
public 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);
}
@Override
public 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 {
@Override
public 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>
测试结果如下:

更多推荐


所有评论(0)