文件上传

简介

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

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

在这里插入图片描述

该表单的代码为:

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

测试结果如下:

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐