在上周,通过编写demo独立项目的方式,我实现了项目管理的增删改查,数据库交互等功能,理解了 Spring Boot 的核心概念,包括 依赖注入、自动配置、RESTful API 设计 等,并通过创建和管理项目的数据库交互,学习了如何使用 Spring Data JPA,如何处理 HTTP 请求、响应数据等。
本周,我将学习文件上传,异常处理,服务层,上下文策略的 Java 实现和阈值计算,OCR 集成等知识,并且通过具体的demo独立验证。
1.项目管理中增删改查的整体实现
应用分层:Controller -> Service -> Repository -> Database
引入ProjectService:
package com.example.demo.service;
import com.example.demo.model.Project;
import com.example.demo.repository.ProjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class ProjectService {
private final ProjectRepository projectRepository;
// --- 依赖注入 ---
@Autowired
public ProjectService(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
// --- 业务方法 ---
// 查询所有项目
@Transactional(readOnly = true) // 只读
public List<Project> getAllProjects() {
return projectRepository.findAll();
}
// 根据 ID 查询项目
@Transactional(readOnly = true)
public Optional<Project> getProjectById(Long id) {
return projectRepository.findById(id);
}
// 创建新项目
@Transactional // 读写事务 (默认)
public Project createProject(Project project) {
// 可以在这里添加创建前的业务逻辑校验,例如检查名称是否重复等
project.setId(null); // 确保创建时 ID 由数据库生成
return projectRepository.save(project);
}
// 更新项目
@Transactional
public Optional<Project> updateProject(Long id, Project projectDetails) {
Optional<Project> optionalExistingProject = projectRepository.findById(id);
if (optionalExistingProject.isPresent()) {
Project existingProject = optionalExistingProject.get();
// 更新需要修改的字段
existingProject.setName(projectDetails.getName());
existingProject.setSummary(projectDetails.getSummary());
// ... 更新其他字段 ...
// 保存更新后的项目
Project updatedProject = projectRepository.save(existingProject);
return Optional.of(updatedProject);
} else {
// 项目不存在
return Optional.empty();
}
}
// 删除项目
@Transactional
public boolean deleteProject(Long id) {
if (projectRepository.existsById(id)) {
projectRepository.deleteById(id);
return true; // 表示删除成功
} else {
return false; // 表示项目不存在,无法删除
}
}
// 根据名称搜索项目
@Transactional(readOnly = true)
public List<Project> searchProjectsByName(String name) {
if (name == null || name.trim().isEmpty()) {
// 如果搜索词为空,可以返回所有项目或空列表,这里返回所有
return projectRepository.findAll();
}
return projectRepository.findByNameContainingIgnoreCase(name);
}
// 置顶项目(A6)、取消置顶(A6)、配置项目设置(A7)等逻辑将来会在这里实现
}
修改ProjectController:
package com.example.demo.controller;
import com.example.demo.model.Project;
import com.example.demo.service.ProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/projects")
public class ProjectController {
// 将 Repository 依赖改为 Service 依赖
private final ProjectService projectService;
// 通过构造函数注入 Service
@Autowired
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
// 1. 获取所有项目
@GetMapping
public ResponseEntity<List<Project>> getAllProjects() {
// 调用 Service 层方法
List<Project> projects = projectService.getAllProjects();
return ResponseEntity.ok(projects);
}
// 2. 根据 ID 获取单个项目
@GetMapping("/{id}")
public ResponseEntity<Project> getProjectById(@PathVariable Long id) {
return projectService.getProjectById(id)
.map(project -> ResponseEntity.ok(project)) // 如果 Optional 有值 (找到项目)
.orElse(ResponseEntity.notFound().build()); // 如果 Optional 为空 (未找到项目)
}
// 3. 创建新项目
@PostMapping
public ResponseEntity<Project> createProject(@RequestBody Project newProject) {
Project createdProject = projectService.createProject(newProject);
return ResponseEntity.status(HttpStatus.CREATED).body(createdProject);
}
// 4. 更新项目
@PutMapping("/{id}")
public ResponseEntity<Project> updateProject(@PathVariable Long id, @RequestBody Project updatedProject) {
return projectService.updateProject(id, updatedProject)
.map(project -> ResponseEntity.ok(project)) // 更新成功
.orElse(ResponseEntity.notFound().build()); // 项目不存在,无法更新
}
// 5. 删除项目
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProject(@PathVariable Long id) {
// 调用 Service 层方法
boolean deleted = projectService.deleteProject(id);
if (deleted) {
return ResponseEntity.noContent().build(); // 204 No Content,表示成功处理请求但无内容返回
} else {
return ResponseEntity.notFound().build(); // 项目不存在,无法删除
}
}
// 6. 根据名称模糊搜索
@GetMapping("/search")
public ResponseEntity<List<Project>> searchProjects(@RequestParam(required = false) String name) {
// 调用 Service 层方法
List<Project> projects = projectService.searchProjectsByName(name);
return ResponseEntity.ok(projects);
}
}
具体测试





至此,项目管理功能中的A2-A5(增删改查)已经全部实现
2.任务分析
关于项目管理功能中的A1和A6(置顶功能相关),我初步的想法是:
- 在
Project实体类中添加一个boolean类型的字段,比如isPinned(默认false) - 在
ProjectService中添加pinProject(Long id)和unpinProject(Long id)方法,内部逻辑是获取项目、设置isPinned状态、然后save()更新 - 在
ProjectController中添加对应的 API 端点,例如PUT /api/projects/{id}/pin和PUT /api/projects/{id}/unpin(或者一个PUT /api/projects/{id}端点,通过请求体参数控制置顶状态) - 修改
getAllProjects()或添加新的 Service 方法(如getPinnedProjects()),以便前端可以获取置顶项目列表
关于项目管理功能中的A7(配置项目设置),我的初步想法是:
- 创建新的
ProjectSettings实体,并建立关联 (@OneToOne) - 更新 Repository
- 在
ProjectService中添加获取和更新项目设置的方法 (getProjectSettings(Long projectId),updateProjectSettings(Long projectId, SettingsDto settings)) - 在
ProjectController中添加对应的 API 端点 (GET /api/projects/{id}/settings,PUT /api/projects/{id}/settings)
这一部分也需要和我的其他队友沟通后进行实现,所以后续会在项目中实现,目前我打算先学习和实现上下文管理中的功能
分析上下文管理中的具体功能,其中B1-B3可以理解为基础数据,通过我目前的学习,可以应对,准备后续在项目中实现,B4-B6主要聚焦于文件处理,所以我打算先学习文件处理等相关知识
3.文件处理
1. 接收文件
通过学习,我了解到一些核心概念:
-
HTTP 请求类型
multipart/form-data:-
当 HTML 表单包含
<input type="file">标签时,为了能够将文件和其他文本数据一起发送到服务器,浏览器会将整个请求的Content-Type设置为multipart/form-data。 -
这与之前用
@RequestBody处理的application/json不同。multipart/form-data将请求体分割成多个部分,每个部分可以包含一个表单字段或一个文件。
-
-
Spring Boot 的
MultipartFile接口:-
Spring MVC内置了对
multipart/form-data请求的处理能力。 -
当一个带有
@RequestParam注解的方法参数类型是MultipartFile时,Spring 会自动将请求中对应名称的文件部分绑定到这个参数上。 -
MultipartFile是一个非常重要的接口,它提供了访问上传文件内容和元数据的方法。常用方法包括:-
String getOriginalFilename(): 获取用户上传时文件的原始名称(例如mydocument.pdf) -
long getSize(): 获取文件的大小(以字节为单位)。 -
String getContentType(): 获取文件的 MIME 类型(例如image/jpeg,app/pdf) -
byte[] getBytes(): 将文件内容读取到一个字节数组中。但对于大文件,会消耗大量内存。 -
InputStream getInputStream(): 获取一个输入流 (java.io.InputStream),可以用来读取文件内容。这是处理文件内容(例如保存到磁盘)的推荐方式。 -
boolean isEmpty(): 判断用户是否实际选择了文件进行上传。 -
void transferTo(File dest): 一个便捷方法,可以将上传的文件直接保存到服务器本地文件系统中的目标文件 (java.io.File)。
-
-
一个接收文件,并获取文件的基本信息的小demo:
// === 文件上传处理 ===
/**
* 处理指定项目的文件上传请求 (B5)
*
* @param projectId 要关联的项目 ID
* @param file 上传的文件 (来自表单中 name="file" 的 input)
* @return 包含上传结果信息的响应
*/
@PostMapping("/{projectId}/files") // 定义 API 路径,例如 POST /api/projects/1/files
public ResponseEntity<String> handleFileUpload(@PathVariable Long projectId,
@RequestParam("file") MultipartFile file) {
// 1. 检查文件是否为空
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择一个文件上传。"); // 返回 400 Bad Request
}
// 2. 获取文件信息 (暂时只打印到控制台)
String originalFilename = file.getOriginalFilename();
long sizeInBytes = file.getSize();
String contentType = file.getContentType();
System.out.println("接收到项目 ID [" + projectId + "] 的文件:");
System.out.println(" 原始文件名: " + originalFilename);
System.out.println(" 文件大小: " + sizeInBytes + " bytes");
System.out.println(" 文件类型: " + contentType);
// --- 这里是下一步要添加文件保存逻辑的地方 ---
// 现在先只是模拟成功接收
String successMessage = String.format("文件 '%s' (%.2f KB) 已成功接收,关联到项目 %d。",
originalFilename, (double) sizeInBytes / 1024, projectId);
return ResponseEntity.ok(successMessage); // 返回 200 OK 和成功信息
}


2.文件存储
考虑到需要将接收到的 MultipartFile 真正地保存到某个地方,并且要处理一些实际问题,比如文件名冲突,后续提取文件中文字或精炼功能,需要将创建一个专门的 Service 来处理这个任务。
1.配置存储路径
在**application.properties**中配置默认存储路径
file.upload-dir=./uploads
#在项目运行目录下创建一个 uploads 文件夹
2.创建文件存储服务,编写FileStorageService
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct; // 导入 PostConstruct
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.UUID;
import org.springframework.util.StringUtils;
@Service
public class FileStorageService {
private final Path fileStorageLocation; // 使用 final
// 使用 @Autowired 通过构造函数注入配置值
@Autowired
public FileStorageService(@Value("${file.upload-dir}") String uploadDir) {
// 将字符串路径转换为 Path 对象
this.fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
// 初始化时尝试创建目录
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new RuntimeException("无法创建用于存储上传文件的目录!", ex);
}
}
/**
* 存储上传的文件到服务器
*
* @param file 上传的 MultipartFile
* @return 存储后的唯一文件名 (用于后续数据库记录和查找)
* @throws RuntimeException 如果存储过程中发生错误
*/
public String storeFile(MultipartFile file) {
// 1. 清理和规范化文件名
// 获取原始文件名
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
try {
// 2. 检查文件名是否有效
if (originalFilename.contains("..")) {
throw new RuntimeException("错误:文件名包含无效路径序列 " + originalFilename);
}
// 3. 生成唯一文件名 (避免冲突)
// 获取文件扩展名,例如 ".pdf"
String fileExtension = "";
int lastDotIndex = originalFilename.lastIndexOf('.');
if (lastDotIndex > 0) {
fileExtension = originalFilename.substring(lastDotIndex);
}
// 生成 UUID 作为主文件名,保留原始扩展名
String storedFilename = UUID.randomUUID().toString() + fileExtension;
// 4. 确定目标存储路径
// 假设 fileStorageLocation 是 D:/uploads 或 /var/www/uploads
// targetLocation 将是 D:/uploads/uuid-blabla.pdf
Path targetLocation = this.fileStorageLocation.resolve(storedFilename);
// 5. 将文件内容复制到目标路径
// 使用 try-with-resources 确保 InputStream 被正确关闭
try (InputStream inputStream = file.getInputStream()) {
// Files.copy(源输入流, 目标路径, 复制选项...)
// StandardCopyOption.REPLACE_EXISTING: 如果同名文件已存在,则覆盖它 (虽然我们用了 UUID,理论上不会重复)
Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING);
}
// 6. 返回存储后的文件名
return storedFilename;
} catch (IOException ex) {
// 捕获 IO 异常,包装成运行时异常抛出,以便上层处理
throw new RuntimeException("无法存储文件 " + originalFilename + "。请重试!", ex);
}
}
}
3.更新 Controller 调用 Service
// === 文件上传处理 ===
@PostMapping("/{projectId}/files")
public ResponseEntity<String> handleFileUpload(@PathVariable Long projectId,
@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请选择一个文件上传。");
}
// --- 调用 FileStorageService 来存储文件 ---
try {
// TODO: 在这里可以添加业务逻辑检查,例如项目是否存在 projectService.getProjectById(projectId).isPresent()
// 调用服务存储文件,获取存储后的唯一文件名
String storedFilename = fileStorageService.storeFile(file);
// --- 下一步: 将文件信息 (projectId, originalFilename, storedFilename, size, type) 保存到数据库 ---
// 现在我们先返回成功信息,包含存储后的文件名
String successMessage = String.format("文件 '%s' 已成功上传并存储为 '%s',关联到项目 %d。",
file.getOriginalFilename(), storedFilename, projectId);
return ResponseEntity.ok(successMessage);
} catch (RuntimeException e) {
// 如果 storeFile 抛出异常 (例如 IO 错误或无效文件名)
// 返回 500 Internal Server Error 或根据异常类型返回更具体的错误码
e.printStackTrace(); // 打印堆栈信息方便调试
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("文件上传失败: " + e.getMessage());
}
}
具体测试结果

3. 将文件信息记录到数据库
(等会放代码和具体分析)

B5-上传文件的核心逻辑已经实现
总结
本周,我对项目管理的增删改查功能进行了重构,通过引入ProjectService,实现了控制器(Controller)、服务(Service)、仓库(Repository)的清晰分层,这不仅优化了代码结构,也加深了对依赖注入、事务管理和面向服务设计的理解。
其次,我开始着手处理项目中的文件上传需求(B5),深入学习了Spring Boot如何利用MultipartFile接口处理multipart/form-data类型的请求。我成功搭建了文件接收的基础框架,并进一步创建了FileStorageService,实现了将上传文件安全、可靠地存储到服务器指定位置的核心逻辑,包括了路径配置、唯一文件名生成和文件流处理等关键步骤。通过Curl测试,验证了项目管理API和服务层逻辑的正确性,以及文件上传和存储功能的基本实现。
接下来我的计划是完善文件上传功能,继续学习异常处理、上下文策略等知识,为构建更健壮、功能更完善的应用做好准备。
349

被折叠的 条评论
为什么被折叠?



