学习目标
第9~10章完成了功能的改造升级工作(如果没有了解可以去我主页看看 第一至十章的内容来学习)本章将继续对用户管理模块的其他功能进行改造,帮助同学们学习并掌握Spring MVC框架的文件上传功能,了解REST风格及JSR 303 数据验证框架等技术。
11.1 系统增、查、改功能
增加、查询、删除(本章不涉及)、修改是Web项目的基本功能,也是开发人员涉及最多的工作。本节使用SpringMVC框架对用户模块进行升级改造。
11.1.1改造添加功能
从用户添加功能入手,主要工作是替换控制层的实现技术,Spring MVC框架提供了接收对象入参的功能,可以直接把前端传入的参数封装到对应对象中,非常方便。
改造Java代码以添加新功能通常涉及几个关键步骤,包括需求分析、设计、编码、测试和部署。这里,我将给出一个简化的示例,说明如何在一个现有的Java类中添加一个新功能。
假设的场景
假设我们有一个简单的Calculator类,它目前支持加法和减法操作。我们的任务是给它添加一个新的功能:乘法。
现有的Calculator类
public class Calculator {
// 加法
public int add(int a, int b) {
return a + b;
}
// 减法
public int subtract(int a, int b) {
return a - b;
}
// 这里我们将添加乘法
}
添加乘法功能
- 分析需求: 需要添加一个方法,该方法接受两个整数参数并返回它们的乘积。
- 设计: 在Calculator类中增加一个名为multiply的方法。
- 编码:
public class Calculator {
// 加法
public int add(int a, int b) {
return a + b;
}
// 减法
public int subtract(int a, int b) {
return a - b;
}
// 新增乘法
public int multiply(int a, int b) {
return a * b;
}
// 可以在这里继续添加其他功能
}
- 测试: 编写单元测试来验证新添加的multiply方法是否按预期工作。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void testMultiply() {
Calculator calc = new Calculator();
assertEquals(6, calc.multiply(2, 3), "2 * 3 should be 6");
}
// 可以添加更多测试来验证其他方法
}
- 将修改后的代码部署到生产环境(对于小型项目或学习项目,这可能意味着简单地重新编译并运行你的程序)。
注意事项
- 代码审查: 在提交更改之前,请确保进行代码审查,以确保新添加的功能符合项目标准和最佳实践。
- 文档更新: 更新相关的开发文档和用户文档,以反映新的功能。
- 版本控制: 使用版本控制系统(如Git)跟踪你的更改,并确保你的更改被正确提交到存储库。
- 测试覆盖率: 尽量为新的和现有的功能编写足够的测试用例,以确保代码的稳定性和可靠性。
通过遵循上述步骤,你可以有效地在Java项目中添加新功能。
11.1.2 改造查询功能(REST 风格)
用户添加功能改造完成之后,接着改造查询详情功能。在此之前,先来了解一个比较流行的API开发规范——REST。
改造REST风格的查询功能通常意味着你正在开发一个基于RESTful API的Web服务,并且需要更新或优化该服务中的查询功能。RESTful API通过HTTP请求(如GET、POST、PUT、DELETE)来操作资源,并使用无状态通信,即每个请求都应该包含执行该请求所需的所有信息。
以下是一个简化的例子,说明如何改造一个基于Spring Boot的RESTful API中的查询功能。我们将从一个简单的Book资源开始,然后添加或修改一个查询Book的端点。
假设的Book资源
首先,我们有一个Book实体类和一个BookRepository接口(使用Spring Data JPA):
// Book.java
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
// 省略getter和setter
}
// BookRepository.java
public interface BookRepository extends JpaRepository<Book, Long> {
// 这里可以添加一些特定查询,例如根据作者查询
List<Book> findByAuthor(String author);
}
原始的REST Controller
我们可能已经有了一个基础的REST Controller来处理对Book资源的请求:
// BookController.java
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookRepository bookRepository;
// 获取所有书籍
@GetMapping
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
// 添加改造的查询功能
// ...
}
改造查询功能
现在,我们要在BookController中添加一个新的端点,以便可以通过作者名来查询书籍。我们将使用@GetMapping注解来定义这个新的端点,并使用查询参数来传递作者名。
// 在BookController.java中添加
// 根据作者名查询书籍
@GetMapping("/by-author")
public List<Book> getBooksByAuthor(@RequestParam String author) {
return bookRepository.findByAuthor(author);
}
这个新的端点/books/by-author将接受一个名为author的查询参数,并使用BookRepository中的findByAuthor方法来查询和返回具有该作者名的所有书籍。
测试改造后的查询功能
为了确保新的查询功能按预期工作,你应该编写单元测试或集成测试。然而,在这个简单的示例中,我们可以通过发送HTTP GET请求到/books/by-author?author=某作者名来手动测试它。
注意事项
- 安全性: 在生产环境中,你可能需要确保这个查询功能是安全的,例如通过验证用户身份、限制可查询的作者等。
- 分页和排序: 对于大型数据集,你可能希望实现分页和排序功能,以提高性能和用户体验。Spring Data JPA提供了强大的分页和排序支持。
- 错误处理: 你应该在代码中处理潜在的错误情况,如作者名不存在时返回适当的HTTP状态码和消息。
- 文档: 不要忘记更新你的API文档,以反映新的查询功能。
HTTP请求类型 | 数据操作类型 |
---|---|
GET | 查询 |
POST | 添加 |
PUT | 修改 |
DELETE | 删除 |
11.1.3 改造编辑功能
改造编辑功能主要分为三步。
改造编辑功能(在Java中,特别是在Web应用程序的上下文中,如使用Spring Boot和Spring MVC)通常意味着更新或优化用于修改现有资源(如数据库中的记录)的API端点。以下是一个简化的例子,说明如何改造一个基于Spring Boot的RESTful API中的编辑功能。
假设的Book资源
首先,我们假设有一个Book实体类和一个BookRepository接口(使用Spring Data JPA),这些已经在之前的示例中定义过了。
原始的REST Controller
我们可能已经有了一个基础的REST Controller来处理对Book资源的请求,包括获取所有书籍的端点,但还没有编辑书籍的端点。
// BookController.java
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookRepository bookRepository;
// 获取所有书籍
@GetMapping
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
// ... 其他端点
// 这里将添加编辑书籍的端点
}
改造编辑功能
为了改造编辑功能,我们需要添加一个端点来接收书籍的更新信息,并更新数据库中的相应记录。这通常通过PUT或PATCH请求来实现,但PUT请求更常用于完全替换资源,而PATCH请求用于部分更新资源。在这个例子中,我们将使用PUT请求。
首先,我们需要一个方法来处理书籍的更新请求。这通常意味着我们需要接收一个书籍对象作为请求体,并使用其ID来找到并更新数据库中的记录。
// 在BookController.java中添加
// 编辑书籍
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
// 检查传入的ID是否与书籍对象的ID匹配(可选,取决于你的业务逻辑)
if (!id.equals(bookDetails.getId())) {
return ResponseEntity.badRequest().body(null); // 或者返回一个错误消息
}
// 查找书籍并更新
Optional<Book> bookOptional = bookRepository.findById(id);
if (bookOptional.isPresent()) {
Book bookToUpdate = bookOptional.get();
// 更新书籍的属性(这里假设我们更新所有属性)
bookToUpdate.setTitle(bookDetails.getTitle());
bookToUpdate.setAuthor(bookDetails.getAuthor());
// ... 更新其他属性
// 保存更新
bookRepository.save(bookToUpdate);
return ResponseEntity.ok(bookToUpdate);
} else {
return ResponseEntity.notFound().build();
}
}
注意事项
- 安全性: 确保只有授权用户才能更新书籍。你可能需要实现身份验证和授权机制。
- 验证: 对传入的书籍对象进行验证,以确保其包含有效且必要的数据。
- 错误处理: 在更新过程中可能会遇到各种错误(如数据库错误、验证错误等),确保你的代码能够妥善处理这些错误,并向客户端返回适当的HTTP状态码和消息。
- 部分更新: 如果你想要支持部分更新(即只更新书籍的某些属性),你可能需要使用PATCH请求,并在你的代码中相应地处理请求体中的部分数据。
- 文档: 更新你的API文档,以反映新的编辑功能。
Spring 表单标签及数据校验
通过前面的知识,已经完成了对用户模块基本操作的改造,其中的主要工作都集中在后台,接下来对前端页面进行一些优化和扩展。
11.2.1 Spring 表单标签
在以往开发JSP项目时,通常会使用EL表达式和JSTL标签来完成页面视图的开发工作,其实,Spring也提供了自己的一套表单标签库。通过使用Spring表单标签,可以很容易地模型数据中的表单对象绑定到HTML表单元素中。
标签名称 | 说明 |
---|---|
<fm:form/> | 渲染表单元素 |
<fm:input/> | 文本框组件标签 |
<fm:password/> | 密码框组件标签 |
<fm:hidden/> | 隐藏组件标签 |
<fm:textarea/> | 多行文本框组件标签 |
<fm:radiobutton/> | 单选框组件标签 |
<fm:checkbox/> | 复选框组件标签 |
<fm:select/> | 下拉列表组件标签 |
<fm:error/> | 显示表单数据校验对应的错误信息 |
以上标签都用于绑定表单对象的属性值,基本上这些标签都拥有以下属性。
- path:属性路径,表示表单对象属性,如account、realName等。
- cssClass:表单组件对应的css样式类名。
- cssErrorClass:当提交表单后报错(服务端错误),采用css样式类。
- cssStyle:表单组件对应的css样式。
- htmlEscape:绑定的表单属性值是否要对HTML特殊字符进行转换,默认为true。
在Spring框架中,处理表单通常涉及几个关键组件:Spring MVC控制器、表单模型对象(通常是一个Java类,其属性映射到表单的输入字段)、Spring的表单标签库(用于在JSP、Thymeleaf等视图中创建表单)。这里,我将展示一个简化的例子,说明如何在Spring MVC应用中处理表单,并展示如何使用Java代码来定义模型对象以及控制器方法。
步骤 1: 定义模型类
首先,定义一个Java类,其属性将映射到表单的输入字段。
public class User {
private String name;
private String email;
// 构造器、getter和setter省略
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
步骤 2: 创建Spring MVC控制器
然后,创建一个Spring MVC控制器来处理表单的提交。
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class UserController {
@GetMapping("/userForm")
public String showForm(Model model) {
// 在模型中添加一个空的User对象,用于表单绑定
model.addAttribute("user", new User());
return "userForm"; // 返回视图名,假设是JSP页面
}
@PostMapping("/submitUser")
public String submitForm(User user) {
// 处理表单提交,例如保存到数据库
// 这里只是简单地返回成功页面
return "userSubmitted"; // 返回视图名,表示提交成功
}
}
步骤 3: 创建表单视图
假设你使用JSP作为视图技术,你需要创建两个JSP文件:userForm.jsp 和 userSubmitted.jsp。
userForm.jsp 示例(使用Spring表单标签):
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<html>
<body>
<form:form action="submitUser" modelAttribute="user" method="post">
<label for="name">Name:</label>
<form:input path="name" id="name" />
<label for="email">Email:</label>
<form:input path="email" id="email" type="email" />
<input type="submit" value="Submit" />
</form:form>
</body>
</html>
注意:为了使用Spring表单标签,你需要在JSP页面顶部添加<%@ taglib … %>声明。
步骤 4: 配置Spring MVC
确保你的Spring MVC应用已经正确配置,包括视图解析器、消息转换器等。这通常通过Java配置或XML配置来完成。
11.2.2 数据校验
使用Spring MVC框架时,有两种常用的方式校验输入的数据。
- 利用Spring 框架自带的验证框架。
- 利用JSR 303实现。
约束 | 说明 |
---|---|
@Null | 被注释的元素必须为null |
@NotNull | 被注释的元素必须不为null |
@AssertTrue | 被注释的元素必须为true |
@AssertFalse | 被注释的元素必须为false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DeclimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max,min) | 被注释的元素的大小必须在指定的范围内 |
@Digits(integer,fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
11.3 使用Spring MVC 实现文件上传
在企业开发中,文件上传是必不可少的功能,本节介绍Spring MVC框架的文件上传功能。
11.3.1 单文件上传
在Java中实现单文件上传,通常会用到Servlet API结合一些库,如Apache Commons FileUpload,来简化文件上传的处理。以下是一个简单的示例,展示了如何使用Servlet和Apache Commons FileUpload库来实现单文件上传。
首先,确保你的项目中已经包含了Apache Commons FileUpload和Apache Commons IO的依赖。如果你使用Maven,可以在pom.xml中添加如下依赖:
<!-- Apache Commons FileUpload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- Apache Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
接下来,创建一个Servlet来处理文件上传:
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
private static final String UPLOAD_DIRECTORY = "/uploads";
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查请求是否包含文件上传
if (!ServletFileUpload.isMultipartContent(request)) {
throw new ServletException("Content type is not multipart/form-data");
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析请求的内容提取文件数据
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
// 迭代表单数据
for (FileItem item : formItems) {
// 处理不在表单中的字段
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String filePath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY + File.separator + fileName;
File storeFile = new File(filePath);
// 在控制台输出文件的上传路径
System.out.println(filePath);
// 保存文件到硬盘
item.write(storeFile);
request.setAttribute("message", "文件上传成功!");
}
}
}
} catch (Exception ex) {
request.setAttribute("message", "文件上传失败!");
}
// 跳转到 message.jsp
getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
}
}
注意
- @WebServlet(“/upload”) 注解用于将Servlet映射到/upload URL。
- UPLOAD_DIRECTORY 是你希望保存上传文件的目录,这里假设是/uploads,它应该位于你的Web应用的根目录下。
- ServletFileUpload.isMultipartContent(request) 用于检查请求是否包含文件上传。
- DiskFileItemFactory 和 ServletFileUpload 用于处理文件上传。
- 遍历 formItems 列表,检查每个 FileItem 是否是文件(非表单字段)。
- 使用 item.write(storeFile) 将文件写入到指定的路径。
最后,你需要在你的Web应用中创建一个HTML表单来上传文件,并确保表单的enctype属性设置为multipart/form-data,如下所示:
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
确保表单的action属性指向你的Servlet URL(在这个例子中是/upload)。
11.3.2 多文件上传
在Java中实现多文件上传,与单文件上传类似,但你需要处理多个FileItem对象,这些对象代表了上传的文件。以下是一个使用Apache Commons FileUpload库和Servlet API实现的多文件上传的示例代码。
首先,确保你的项目中已经包含了Apache Commons FileUpload和Apache Commons IO的依赖,就像我们在单文件上传示例中所做的那样。
然后,创建一个Servlet来处理多文件上传:
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
@WebServlet("/uploadMultiple")
public class UploadMultipleServlet extends HttpServlet {
private static final String UPLOAD_DIRECTORY = "/uploads";
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查请求是否包含文件上传
if (!ServletFileUpload.isMultipartContent(request)) {
throw new ServletException("Content type is not multipart/form-data");
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析请求的内容提取文件数据
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && !formItems.isEmpty()) {
// 迭代表单数据
for (FileItem item : formItems) {
// 处理不在表单中的字段
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String filePath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY + File.separator + fileName;
File storeFile = new File(filePath);
// 在控制台输出文件的上传路径
System.out.println(filePath);
// 保存文件到硬盘
item.write(storeFile);
}
}
// 文件上传成功,可以重定向到另一个页面或发送消息
request.setAttribute("message", "文件上传成功!");
}
} catch (Exception ex) {
request.setAttribute("message", "文件上传失败: " + ex.getMessage());
}
// 跳转到 message.jsp 或其他页面来显示消息
getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
}
}
在这个例子中,UploadMultipleServlet Servlet监听/uploadMultiple URL。当提交包含多个文件的表单时,它会检查请求是否为多部分请求(即包含文件上传),然后解析请求并遍历所有的FileItem对象。对于每个FileItem对象,如果它不是表单字段(即它是文件),则提取文件名、创建文件路径,并将文件写入到指定的位置。
请注意,你应该在你的Web应用的根目录下创建/uploads目录(或你选择的任何目录),以便Servlet能够保存上传的文件。
HTML表单示例,用于上传多个文件:
<form action="uploadMultiple" method="post" enctype="multipart/form-data">
<input type="file" name="file" multiple="multiple" />
<input type="submit" value="Upload" />
</form>
注意:multiple="multiple"属性,它允许用户选择多个文件。这样,当表单提交时,所有选中的文件都将作为多部分请求的一部分发送到服务器。