企业级Spring MVC高级主题与实用技术讲解
本手册旨在为具备Spring MVC基础的初学者,系统地讲解企业级应用开发中常用的高级主题和实用技术,涵盖RESTful API、统一异常处理、拦截器、文件处理、国际化、前端集成及Spring Security基础。内容结合JavaConfig和代码示例进行说明,并尝试与之前的图书管理系统案例和基础教程内容衔接。
1. RESTful API 设计与实践
RESTful是一种架构风格,而非强制标准。它基于HTTP协议,通过统一的接口对资源进行操作,具有无状态、客户端-服务器分离等特点。在现代企业应用中,特别是在前后端分离架构下,RESTful API是常用的后端接口风格。
核心设计原则
- 资源 (Resource) : Web上的核心概念,指代某个事物(如用户、图书)。资源通过URI (统一资源标识符) 来唯一标识。
- 示例URI:
/users
,/books/123
- 示例URI:
- URI: 应简洁、直观,描述资源而非操作。使用名词复数表示集合,名词单数表示个体。避免在URI中使用动词。
- HTTP 方法 (HTTP Methods) : 使用HTTP方法来表示对资源的操作:
GET
: 获取资源。安全且幂等。POST
: 创建新资源或执行非幂等操作。PUT
: 更新或替换资源。幂等。DELETE
: 删除资源。幂等。PATCH
: 部分更新资源。
- 状态码 (Status Codes) : 使用标准的HTTP状态码表示请求的处理结果:
2xx
(Success):200 OK
,201 Created
,204 No Content
3xx
(Redirection):301 Moved Permanently
,302 Found
4xx
(Client Error):400 Bad Request
,401 Unauthorized
,403 Forbidden
,404 Not Found
,405 Method Not Allowed
5xx
(Server Error):500 Internal Server Error
- 表述 (Representation): 资源通过某种格式(如JSON、XML)来表述其状态。客户端和服务器通过这些表述进行数据交换。JSON是目前最流行的格式。
- 无状态 (Stateless): 服务器不存储客户端的上下文信息。每个请求都包含处理该请求所需的所有信息。
Spring 构建 RESTful 服务
Spring MVC通过一系列注解简化RESTful服务的构建。
@RestController
: 标记一个类是RESTful控制器。它是@Controller
和@ResponseBody
的组合。@ResponseBody
: 标记方法返回值直接写入HTTP响应体,不作为视图名。Spring MVC会根据Accept
头和HttpMessageConverter
将返回值转换为相应格式(如JSON)。@RequestBody
: 标记方法参数来自HTTP请求体。Spring MVC会根据Content-Type
头和HttpMessageConverter
将请求体内容转换为方法参数对象。@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
: 对应HTTP方法的请求映射注解,是@RequestMapping
的快捷方式。@PathVariable
: 获取URI路径中的变量。ResponseEntity
: 封装响应的完整信息,包括响应体、状态码和头部。可以在方法中返回ResponseEntity
来精确控制响应。
svg
+-------------+ +-----------------+ +---------------------+ +-------------+
| User/Client | --> | DispatcherServlet | --> | RequestMappingHandler | --> | Controller |
| (Frontend) | +-----------------+ | Adapter | +------v------+
| | | HTTP Req | +-----------+---------+ | Process Logic
| | | | | | (Service/Repo)
| | | GET /api/books/1| | Call Method |
| | | POST /api/books | | with @RequestBody | Return Object
| | | { JSON Data } | | |
+-------------+ +-----------------+ +-----------+---------+ +------^------+
| | Convert to/from JSON |
| Look up Handler | (@RequestBody / | HttpMessageConverter
| | @ResponseBody) |
v | |
+-----------------+ +---------------------+ +------+------+
| HandlerMapping | | HttpMessageConverter| <--> | JSON/XML |
+-----------------+ +---------------------+ +------+------+
| Find Handler | Serialize/Deserialize | Write to Response
+----------------------+------------------------------+
代码示例 (基于图书管理系统,添加 REST API)
假设在图书管理系统中有Book
实体和BookService
。新增一个BookRestController
:
java
package com.yourcompany.bookmanagement.controller;
import com.yourcompany.bookmanagement.entity.Book;
import com.yourcompany.bookmanagement.exception.BookNotFoundException; // 复用之前的异常
import com.yourcompany.bookmanagement.service.BookService; // 复用之前的 Service
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;
import java.util.Optional;
@RestController // @RestController = @Controller + @ResponseBody
@RequestMapping("/api/v1/books") // REST API 基础路径,v1 表示版本
public class BookRestController {
private final BookService bookService;
@Autowired
public BookRestController(BookService bookService) {
this.bookService = bookService;
}
// 获取所有图书列表
// GET /api/v1/books
@GetMapping
public List<Book> getAllBooks() {
// 返回 List 会自动通过 HttpMessageConverter 转换为 JSON 数组
return bookService.findAllBooks();
}
// 获取特定图书详情
// GET /api/v1/books/{id}
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
Optional<Book> book = bookService.findBookById(id);
// 使用 ResponseEntity 控制状态码
return book.map(value -> new ResponseEntity<>(value, HttpStatus.OK)) // 找到则返回 200 OK
.orElseThrow(() -> new BookNotFoundException(id)); // 找不到则抛出异常,由全局异常处理器处理
}
// 创建新图书
// POST /api/v1/books
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 创建成功返回 201 Created
public Book createBook(@RequestBody Book book) { // @RequestBody 将请求体 (JSON) 转换为 Book 对象
// 校验等逻辑可以在 Service 层或通过 Bean Validation 实现 (需要额外配置)
return bookService.saveBook(book); // 保存并返回新创建的图书 (可能包含生成的 ID)
}
// 更新图书
// PUT /api/v1/books/{id}
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
// 实际更新逻辑需要先查找,然后更新字段,最后保存
Optional<Book> existingBookOptional = bookService.findBookById(id);
if (existingBookOptional.isPresent()) {
Book existingBook = existingBookOptional.get();
// 假设只更新标题和作者,实际应根据需求更新所有字段
existingBook.setTitle(book.getTitle());
existingBook.setAuthor(book.getAuthor());
existingBook.setIsbn(book.getIsbn());
existingBook.setPublicationDate(book.getPublicationDate());
Book updatedBook = bookService.saveBook(existingBook);
return new ResponseEntity<>(updatedBook, HttpStatus.OK); // 返回更新后的图书和 200 OK
} else {
throw new BookNotFoundException(id); // 找不到则抛异常
}
}
// 删除图书
// DELETE /api/v1/books/{id}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 删除成功返回 204 No Content
public void deleteBook(@PathVariable Long id) {
// 可以先检查是否存在,再删除
Optional<Book> book = bookService.findBookById(id);
if (!book.isPresent()) {
throw new BookNotFoundException(id); // 不存在则抛异常
}
bookService.deleteBookById(id); // 调用 Service 删除
}
}
JSON 数据交互的最佳实践
- 一致的格式: 保持请求和响应JSON结构的命名规范、日期格式等一致。
- 合适的
Content-Type
: 请求时使用application/json
,响应时服务器返回application/json
。Spring MVC会根据produces
和consumes
参数、Accept
头自动处理。 - 字段命名: 推荐使用小驼峰命名法 (camelCase),与JavaScript习惯一致。Jackson库默认支持。
- 错误响应: 使用统一的错误响应格式,包含状态码、错误信息等(详见下一节)。
- 分页/排序 : 对于列表接口,通过查询参数传递分页 (
page
,size
) 和排序 (sort
,sortBy
) 信息。Spring Data JPA的Pageable
很适合。 - 版本控制 : 在URI (
/api/v1/books
) 或Header (X-API-Version
) 中体现版本,便于API演进。URI版本控制更直观常用。 - HATEOAS: (Hypermedia as the Engine of Application State) 一种更高级的RESTful实践,要求资源表述中包含指向相关资源的链接,使客户端可以无需硬编码URI就能导航API。Spring HATEOAS项目提供了支持。对于初学者,理解概念即可,实现相对复杂,通常在API成熟阶段考虑。
版本控制策略
- URI 版本 (URI Versioning) : 将版本号放入URI路径中,如
/api/v1/books
。最常用且直观。缺点是URI会随着版本变化。 - Header 版本 (Header Versioning) : 将版本信息放入请求头,如
Accept: application/vnd.myapi.v1+json
或自定义头X-API-Version: 1.0
。URI保持稳定,但客户端调用稍复杂。 - 参数版本 (Query Parameter Versioning) : 将版本作为查询参数,如
/api/books?version=1.0
。不推荐,不符合RESTful风格。
选择哪种取决于项目需求和团队偏好,URI版本控制对初学者最友好。
2. 统一异常处理机制
良好的异常处理机制能够提升应用的健壮性和用户体验。Spring MVC提供了灵活的方式实现统一的异常处理。
使用 @ControllerAdvice
和 @ExceptionHandler
@ControllerAdvice
: 标记一个类是全局的控制器增强器。Spring会扫描这个类,并将其中的@ExceptionHandler
,@ModelAttribute
,@InitBinder
等方法应用到所有 (或指定范围)的@Controller
和@RestController
上。这实现了全局性。@ExceptionHandler
: 标记一个方法用于处理特定类型的异常。当Controller方法抛出@ExceptionHandler
指定类型的异常时,Spring MVC会调用匹配的异常处理方法。这实现了针对性。
代码示例 (基于图书管理系统)
图书管理系统案例中的GlobalExceptionHandler
和BookNotFoundException
就是很好的示例。
java
package com.yourcompany.bookmanagement.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; // 用于 REST API 返回 JSON
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; // 用于 REST API
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView; // 用于返回视图
// @ControllerAdvice 应用于所有 Controller
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// --- 针对返回视图的异常处理 (如图书管理系统案例中的 HTML 页面请求) ---
// 处理 BookNotFoundException 异常,返回 404 视图
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
public ModelAndView handleBookNotFoundForView(BookNotFoundException ex) {
logger.warn("Book not found: " + ex.getMessage());
ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.html
mav.addObject("message", ex.getMessage()); // 将错误信息添加到 Model
return mav;
}
// 处理所有其他未捕获的 Exception,返回 500 视图
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500
public ModelAndView handleAllExceptionsForView(Exception ex) {
logger.error("Internal Server Error: ", ex);
ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.html
mav.addObject("message", "Internal Server Error. Please try again later.");
return mav;
}
// --- 针对返回 JSON 的异常处理 (如 RESTful API 请求) ---
// 可以定义一个返回 JSON 格式的异常处理,但需要区分请求类型
// 或者更简单的做法是,如果你的 Controller 是 @RestController,异常处理方法也返回 @ResponseBody
// 这里以 BookNotFoundException 为例,演示返回 JSON 错误
@ExceptionHandler(BookNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
@ResponseBody // 直接写入响应体,由 HttpMessageConverter 处理
public ErrorResponse handleBookNotFoundForRest(BookNotFoundException ex) {
logger.warn("Book not found for REST request: " + ex.getMessage());
// 返回统一的 JSON 错误格式
return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis());
}
// 处理 @RequestBody 参数校验失败异常 (MethodArgumentNotValidException)
// 通常在 REST API 中发生
@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request
@ResponseBody
public ErrorResponse handleValidationExceptions(org.springframework.web.bind.MethodArgumentNotValidException ex) {
// 提取所有校验错误信息
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(java.util.stream.Collectors.joining(", "));
logger.warn("Validation failed: " + errorMessage);
return new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation Failed: " + errorMessage, System.currentTimeMillis());
}
// 统一错误响应格式 (POJO)
public static class ErrorResponse {
private int status;
private String message;
private long timestamp;
public ErrorResponse(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// Getters for Jackson serialization
public int getStatus() { return status; }
public String getMessage() { return message; }
public long getTimestamp() { return timestamp; }
}
// 自定义异常类 (同图书管理系统案例)
// package com.yourcompany.bookmanagement.exception;
// public class BookNotFoundException extends RuntimeException {
// public BookNotFoundException(Long id) {
// super("Book not found with ID: " + id);
// }
// }
}
说明:
- 同一个
@ControllerAdvice
类中,可以定义多个@ExceptionHandler
方法处理不同类型的异常。 @ExceptionHandler
方法的参数可以是抛出的异常对象,返回值可以是ModelAndView
,String
(视图名或重定向),@ResponseBody
返回值,ResponseEntity
等。- 通过
@ResponseStatus
可以设置响应的HTTP状态码。 - 为了同时支持返回视图和返回JSON的异常处理,可以根据请求的
Accept
头或路径 (/api/**
) 进行区分处理,或者如示例所示,为同一种异常定义两个@ExceptionHandler
方法(Spring会根据返回类型等选择更匹配的那个,或者可以通过@RequestMapping(produces = ...)
等进一步限定)。在全RESTful API应用中,通常只返回JSON。
自定义异常类
根据业务需求定义自定义异常类,继承自RuntimeException
(非检查型异常)或Exception
(检查型异常)。非检查型异常通常用于表示编程错误或运行时环境问题,检查型异常用于表示可预期的、需要调用方显式处理的业务问题。在Spring的事务管理中,默认只对非检查型异常进行回滚。
java
package com.yourcompany.bookmanagement.exception;
// 业务异常示例:图书库存不足
public class InsufficientStockException extends RuntimeException {
private Long bookId;
private int requested;
private int available;
public InsufficientStockException(Long bookId, int requested, int available) {
super("Book " + bookId + " stock insufficient. Requested: " + requested + ", Available: " + available);
this.bookId = bookId;
this.requested = requested;
this.available = available;
}
// Getters for error details
public Long getBookId() { return bookId; }
public int getRequested() { return requested; }
public int getAvailable() { return available; }
}
然后在@ControllerAdvice
中添加对应的@ExceptionHandler
处理方法。
异常处理策略
- 业务异常: 对于应用程序的正常流程中可能发生的、可预期的错误(如用户不存在、库存不足),定义特定的自定义异常。在Service层或Controller层捕获或抛出,由全局异常处理器返回友好的错误信息(给用户)和具体的错误代码(给前端/客户端)。
- 系统异常: 对于意料之外的错误(如数据库连接失败、空指针异常),通常抛出RuntimeException或其子类。全局异常处理器应记录详细日志(给开发者),并返回通用的错误提示(给用户)和500状态码。避免在生产环境泄露敏感的异常堆栈信息。
- 返回格式: 对于面向用户的Web页面,返回错误页面(如404.html, 500.html)。对于RESTful API,返回统一结构的JSON错误响应。
3. Spring MVC 拦截器 (Interceptor)
拦截器允许你在请求到达Controller之前、Controller处理之后、以及整个请求处理完成后执行自定义逻辑。它工作在DispatcherServlet内部,比Servlet Filter更靠近Spring MVC的核心流程,能够访问Handler(Controller方法)和ModelAndView等信息。
概念、生命周期与 Filter 的区别
- 概念: 拦截器是Spring MVC提供的请求处理拦截机制。
- 生命周期 : 一个请求经过拦截器链的三个阶段:
preHandle()
: 在Controller方法执行之前 调用。如果返回true
,继续执行后续拦截器和Controller;如果返回false
,中断整个请求处理流程。常用于认证、权限校验、日志记录。postHandle()
: 在Controller方法执行之后 ,视图渲染之前 调用。可以访问ModelAndView,用于修改模型数据、视图名等。注意:如果Controller方法抛出异常,postHandle
不会被调用。afterCompletion()
: 在整个请求处理完成之后(包括视图渲染完成后),无论是否发生异常,都会调用。用于资源清理等。
- 与 Filter 的区别 :
- Filter是Servlet规范的一部分,工作在Servlet容器层面,在DispatcherServlet之前执行,无法访问Spring MVC的上下文(如Handler)。适用于字符编码、会话管理、静态资源处理等。
- Interceptor是Spring MVC框架的一部分,工作在DispatcherServlet内部,HandlerMapping之后。能够访问Handler、ModelAndView、Spring容器中的Bean等。适用于更细粒度的、与业务逻辑关联的拦截,如认证、权限、性能监控、日志。
创建和配置拦截器
- 创建拦截器类 : 实现
HandlerInterceptor
接口。该接口定义了preHandle
,postHandle
,afterCompletion
方法。或者,如果只需要实现部分方法,可以继承已废弃的HandlerInterceptorAdapter
,但现在推荐直接实现HandlerInterceptor
并使用接口的默认方法。 - 配置拦截器 : 在实现
WebMvcConfigurer
接口的配置类中,通过重写addInterceptors()
方法注册拦截器。
代码示例 (基于图书管理系统)
图书管理系统案例中的AuthInterceptor
是认证拦截器的示例。
java
package com.yourcompany.bookmanagement.interceptor;
import org.springframework.web.servlet.HandlerInterceptor; // 引入接口
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class AuthInterceptor implements HandlerInterceptor { // 实现 HandlerInterceptor 接口
// 在 Controller 方法执行前调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
System.out.println("AuthInterceptor: Intercepting request: " + requestURI);
HttpSession session = request.getSession();
Object user = session.getAttribute("loggedInUser"); // 检查 Session 中是否有名为 "loggedInUser" 的属性
if (user != null) {
// 用户已登录,放行
System.out.println("AuthInterceptor: User is logged in.");
return true; // 继续执行后续拦截器或 Controller
} else {
// 用户未登录,重定向到登录页面
System.out.println("AuthInterceptor: User is NOT logged in. Redirecting to login page.");
// 获取 contextPath,避免硬编码应用名称
response.sendRedirect(request.getContextPath() + "/login");
return false; // 阻止当前请求继续处理
}
}
// 在 Controller 方法执行后,视图渲染前调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 可以在这里对 Model 或 View 进行操作
// System.out.println("AuthInterceptor postHandle...");
}
// 在整个请求处理完成后调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 用于资源清理等
// System.out.println("AuthInterceptor afterCompletion...");
}
}
拦截器配置 (WebMvcConfig.java
):
java
package com.yourcompany.bookmanagement.config;
import com.yourcompany.bookmanagement.interceptor.AuthInterceptor; // 引入拦截器类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 引入 WebMvcConfigurer
@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer { // 实现 WebMvcConfigurer
// 将拦截器注册为 Spring Bean
@Bean
public AuthInterceptor authInterceptor() {
return new AuthInterceptor();
}
// 重写 addInterceptors 方法配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor()) // 添加拦截器实例
.addPathPatterns("/**") // 配置需要拦截的路径模式,"/**" 表示拦截所有请求
.excludePathPatterns("/login", "/logout", "/resources/**", "/webjars/**"); // 配置排除的路径模式,登录、注销、静态资源通常需要排除
}
// ... 其他配置,如 ViewResolver, MultipartResolver 等
}
典型应用场景
- 日志记录 : 在
preHandle
记录请求信息,在afterCompletion
记录处理时间、响应状态等。 - 权限校验 : 在
preHandle
检查用户是否已登录、是否有权访问当前资源。未通过则重定向或返回错误。 - 请求预处理 : 在
preHandle
或postHandle
设置一些通用的请求属性、上下文信息等。 - 性能监控 : 在
preHandle
记录请求开始时间,在afterCompletion
计算并记录总耗时。
4. 文件上传与下载
Spring MVC对文件上传和下载提供了内置支持,特别是结合Servlet 3.0+的MultipartRequest API。
文件上传
- 配置
MultipartResolver
: Spring MVC需要一个MultipartResolver
Bean来解析multipart/form-data
请求。StandardServletMultipartResolver
: 基于Servlet 3.0+ 标准。推荐使用,无需额外依赖。CommonsMultipartResolver
: 基于Apache Commons FileUpload库。需要添加commons-fileupload
依赖。
- Controller 中接收文件 : 在Controller方法中使用
@RequestParam MultipartFile
参数接收上传的文件。MultipartFile
接口提供了获取文件名、内容类型、文件大小、字节流等方法。 - 文件大小限制、类型校验 : 可以在
MultipartResolver
中配置总大小和单个文件大小限制。文件类型校验通常在Controller或Service中根据file.getContentType()
和file.getOriginalFilename()
进行。 - 存储上传文件 : 获取到
MultipartFile
后,可以使用transferTo(File dest)
方法将其保存到文件系统,或者获取getInputStream()
/getBytes()
写入数据库、云存储等。
代码示例 (文件上传)
WebMvcConfig.java
配置 StandardServletMultipartResolver
:
java
package com.yourcompany.bookmanagement.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver; // 引入接口
import org.springframework.web.multipart.support.StandardServletMultipartResolver; // 引入 Standard 实现
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {
// 配置 StandardServletMultipartResolver
// Servlet 3.0+ 容器会自动提供 MultipartConfigElement,StandardServletMultipartResolver 基于此工作
// 文件大小等限制可以在 Servlet 注册时(例如 MyWebAppInitializer)或通过容器配置实现
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
// ... 其他配置
}
如果在 MyWebAppInitializer.java
中需要配置上传属性 (如文件大小限制),可以重写 customizeRegistration
方法:
java
package com.yourcompany.bookmanagement.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.MultipartConfigElement; // 引入
import javax.servlet.ServletRegistration; // 引入
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ... getRootConfigClasses, getServletConfigClasses, getServletMappings methods
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// 配置文件上传属性:临时文件存放路径,最大文件大小,最大请求大小,文件阈值大小
// 这些配置会传递给 StandardServletMultipartResolver
String fileUploadTempDir = System.getProperty("java.io.tmpdir"); // 使用系统的临时目录
long maxFileSize = 5 * 1024 * 1024; // 5MB
long maxRequestSize = 10 * 1024 * 1024; // 10MB
int fileSizeThreshold = 0; // 所有文件都直接写入临时文件,而不是内存
MultipartConfigElement multipartConfigElement = new MultipartConfigElement(
fileUploadTempDir,
maxFileSize,
maxRequestSize,
fileSizeThreshold);
registration.setMultipartConfig(multipartConfigElement);
}
}
Controller 处理文件上传:
java
package com.yourcompany.bookmanagement.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.nio.file.Files; // 使用 NIO.2 进行文件操作
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
@RequestMapping("/files")
public class FileUploadController {
// 文件保存的根目录 (请根据实际环境修改!)
// 生产环境不应该硬编码在此处,应从配置读取
private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!
@GetMapping("/upload")
public String showUploadForm() {
return "uploadForm"; // 返回上传表单视图 (如 uploadForm.html)
}
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
// 简单校验:文件是否为空
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/files/uploadStatus"; // 重定向到状态页面
}
try {
// 获取文件名
String fileName = file.getOriginalFilename();
// 防止路径穿越等安全问题,实际应用中应更严格处理文件名
Path path = Paths.get(UPLOADED_FOLDER + fileName);
// 创建目标目录 (如果不存在)
Files.createDirectories(path.getParent());
// 将文件保存到目标路径
Files.copy(file.getInputStream(), path); // 使用 NIO.2 Copy Stream
redirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + fileName + "'");
} catch (IOException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message", "Failed to upload file: " + e.getMessage());
}
return "redirect:/files/uploadStatus"; // 重定向到状态页面显示结果
}
@GetMapping("/uploadStatus")
public String uploadStatus() {
return "uploadStatus"; // 返回上传状态视图 (如 uploadStatus.html)
}
}
上传表单视图 (uploadForm.html
- Thymeleaf 示例):
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>File Upload</title>
</head>
<body>
<h1>Upload File</h1>
<!-- 注意:method 必须是 POST,enctype 必须是 multipart/form-data -->
<form method="POST" action="/files/upload" enctype="multipart/form-data">
<div>
<label for="file">Select File:</label>
<input type="file" name="file" id="file" required/>
</div>
<div>
<button type="submit">Upload</button>
</div>
</form>
</body>
</html>
上传状态视图 (uploadStatus.html
- Thymeleaf 示例):
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Upload Status</title>
</head>
<body>
<h1>Upload Status</h1>
<!-- 使用 th:text 获取 RedirectAttributes 中的 Flash 属性 -->
<div th:if="${message}">
<p th:text="${message}"></p>
</div>
<p><a th:href="@{/files/upload}">Upload Another File</a></p>
</body>
</html>
文件下载
文件下载通常是通过设置HTTP响应头,让浏览器以下载方式处理响应内容。
- Controller 方法 : 可以返回
ResponseEntity<Resource>
,或直接操作HttpServletResponse
。 - 设置响应头 : 关键在于设置正确的
Content-Disposition
头,指定文件名并告知浏览器以下载方式处理;Content-Type
头指定文件类型;Content-Length
头指定文件大小。 - 写入响应体 : 将文件内容通过流写入
HttpServletResponse
的输出流。
代码示例 (文件下载)
java
package com.yourcompany.bookmanagement.controller;
import org.springframework.core.io.InputStreamResource; // 引入资源类型
import org.springframework.core.io.Resource; // 引入资源接口
import org.springframework.http.HttpHeaders; // 引入 HTTP 头部
import org.springframework.http.MediaType; // 引入媒体类型
import org.springframework.http.ResponseEntity; // 引入 ResponseEntity
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest; // 可能需要
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
@RequestMapping("/files")
public class FileDownloadController {
// 文件存放的根目录 (同上传示例)
private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!
// 文件下载方法,返回 ResponseEntity<Resource>
@GetMapping("/download/{fileName:.+}") // :.+ 匹配文件名,包括点号
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize(); // 构造文件路径
File file = filePath.toFile();
// 检查文件是否存在且可读
if (!file.exists() || !file.canRead()) {
// 抛出异常或返回 404 响应
// return new ResponseEntity<>(HttpStatus.NOT_FOUND);
throw new RuntimeException("File not found or cannot be read: " + fileName); // 示例抛出异常
}
try {
// 确定文件的 Content-Type
String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
if (contentType == null) {
contentType = "application/octet-stream"; // 默认类型
}
// 创建 InputStreamResource
InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
// 构建响应
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType)) // 设置 Content-Type
// 设置 Content-Disposition,inline 表示在线打开,attachment 表示下载
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.contentLength(file.length()) // 设置 Content-Length
.body(resource); // 设置响应体
} catch (IOException ex) {
// 记录错误日志并返回 500 响应或抛出异常
ex.printStackTrace();
// return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
throw new RuntimeException("Error reading file: " + fileName, ex); // 示例抛出异常
}
}
/*
// 另一种使用 HttpServletResponse 的方式 (不推荐,因为它绕过了 Spring 的 HttpMessageConverter 等)
@GetMapping("/download2/{fileName:.+}")
public void downloadFile2(@PathVariable String fileName, HttpServletResponse response) throws IOException {
Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();
File file = filePath.toFile();
if (!file.exists() || !file.canRead()) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
return;
}
String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
if (contentType == null) {
contentType = "application/octet-stream";
}
response.setContentType(contentType);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");
response.setContentLength((int) file.length());
try (InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
} catch (IOException e) {
// 错误处理
e.printStackTrace();
throw e; // 抛出异常让 Spring 的异常处理器处理
}
}
*/
}
下载链接示例 (HTML):
html
<a th:href="@{/files/download/your_file_name.ext}">Download File</a>
5. 国际化 (i18n) 与本地化 (L10n)
国际化 (i18n) 是使应用程序能够适应不同语言和地区的过程。本地化 (L10n) 是为特定语言和地区定制应用程序的过程,包括翻译文本、调整日期格式等。Spring MVC为国际化提供了强大的支持。
Spring MVC 对国际化的支持
LocaleResolver
: 用于解析当前用户的区域设置 (Locale)。Spring提供了多种实现:AcceptHeaderLocaleResolver
(默认): 根据请求头的Accept-Language
确定Locale。SessionLocaleResolver
: 将Locale存储在HttpSession中。CookieLocaleResolver
: 将Locale存储在Cookie中。FixedLocaleResolver
: 固定使用某个Locale。
MessageSource
: 用于根据Locale加载国际化消息。它从资源文件(如.properties
文件)中读取键值对。Spring提供了ResourceBundleMessageSource
等实现。
配置资源文件和 Spring Bean
- 创建资源文件 : 在
src/main/resources
目录下创建消息源文件,遵循basename_locale.properties
的命名约定。messages.properties
(默认语言)messages_en.properties
(英语)messages_zh_CN.properties
(简体中文)- 文件内容为键值对,如:
app.title=Book Management System
- 配置
MessageSource
Bean : 在Spring配置中定义MessageSource
Bean。 - 配置
LocaleResolver
Bean : 在Spring MVC配置 (WebMvcConfigurer
) 中定义LocaleResolver
Bean。 - 配置
LocaleChangeInterceptor
(可选) : 如果想通过请求参数切换语言,配置LocaleChangeInterceptor
。
代码示例 (国际化)
资源文件示例 (src/main/resources/messages_zh_CN.properties
, messages_en.properties
):
messages_zh_CN.properties
:
properties
app.title=图书管理系统
book.list.title=图书列表
book.detail.title=图书详情
book.form.add=新增图书
book.form.edit=编辑图书
book.title=标题
book.author=作者
book.isbn=ISBN
book.publicationDate=出版日期
button.save=保存
button.cancel=取消
action.details=详情
action.edit=编辑
action.delete=删除
message.save.success=图书信息保存成功!
message.delete.success=图书删除成功!
error.book.notFound=找不到ID为 {0} 的图书。
validation.NotBlank=字段不能为空
validation.Size=字段长度不符合要求
validation.Pattern=字段格式不正确
validation.PastOrPresent=日期不能晚于今天
login.title=用户登录
login.username=用户名
login.password=密码
login.button=登录
login.error=用户名或密码不正确。
logout.success=您已成功注销。
messages_en.properties
:
properties
app.title=Book Management System
book.list.title=Book List
book.detail.title=Book Detail
book.form.add=Add Book
book.form.edit=Edit Book
book.title=Title
book.author=Author
book.isbn=ISBN
book.publicationDate=Publication Date
button.save=Save
button.cancel=Cancel
action.details=Details
action.edit=Edit
action.delete=Delete
message.save.success=Book information saved successfully!
message.delete.success=Book deleted successfully!
error.book.notFound=Book not found with ID: {0}.
validation.NotBlank=Field must not be blank
validation.Size=Field size constraints violated
validation.Pattern=Field format is incorrect
validation.PastOrPresent=Date cannot be in the future
login.title=User Login
login.username=Username
login.password=Password
login.button=Login
login.error=Invalid username or password.
logout.success=You have been logged out successfully.
WebMvcConfig.java
配置 MessageSource
和 LocaleResolver
:
java
package com.yourcompany.bookmanagement.config;
import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource; // 引入实现类
import org.springframework.web.servlet.LocaleResolver; // 引入 LocaleResolver
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver; // 引入 CookieLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; // 引入 LocaleChangeInterceptor
import java.util.Locale; // 引入 Locale
@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {
// ... MultipartResolver, AuthInterceptor 等其他 Bean
// 配置 MessageSource Bean
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
// 设置资源文件的 basename (不带语言和后缀)
messageSource.setBasename("messages"); // 对应 src/main/resources/messages.properties, messages_en.properties 等
messageSource.setDefaultEncoding("UTF-8"); // 设置编码
messageSource.setUseCodeAsDefaultMessage(true); // 如果找不到对应的 code,使用 code 本身作为消息
return messageSource;
}
// 配置 LocaleResolver Bean
// 使用 CookieLocaleResolver 将用户选择的语言存储在 Cookie 中
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.CHINA); // 设置默认区域为中国
localeResolver.setCookieName("mylocale"); // 设置存储 Locale 的 Cookie 名称
localeResolver.setCookieMaxAge(3600); // Cookie 有效期 (秒)
return localeResolver;
}
// 配置 LocaleChangeInterceptor,用于通过参数切换语言
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
// 设置参数名,例如通过访问 /books?lang=en 或 /books?lang=zh_CN 切换语言
interceptor.setParamName("lang");
return interceptor;
}
// 将 LocaleChangeInterceptor 注册到拦截器链中
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ... 注册 AuthInterceptor
registry.addInterceptor(localeChangeInterceptor()); // 注册语言切换拦截器
}
// ... 其他 WebMvcConfigurer 方法
}
在视图和后端使用本地化消息
-
在视图中 (Thymeleaf) : 使用
#messages
内置对象和#{...}
语法获取消息。html<h1 th:text="#{book.list.title}">图书列表</h1> <p th:text="#{message.save.success}"></p> <!-- 获取带参数的消息,例如 error.book.notFound=找不到ID为 {0} 的图书。 --> <p th:text="#{error.book.notFound(${bookId})}"></p> <!-- 生成带语言切换参数的 URL --> <a th:href="@{/books(lang='en')}">English</a> | <a th:href="@{/books(lang='zh_CN')}">中文</a>
-
在视图中 (JSP) : 需要配置Spring标签库,并使用
<spring:message>
标签。jsp<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> <spring:message code="book.list.title"/> <spring:message code="error.book.notFound" arguments="${bookId}"/> <a href="<spring:url value='/books'><spring:param name='lang' value='en'/></spring:url>">English</a>
-
在后端代码 (Controller/Service) 中 : 通过注入
MessageSource
,使用getMessage()
方法获取消息。
java
package com.yourcompany.bookmanagement.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.i18n.LocaleContextHolder; // 引入 LocaleContextHolder
import org.springframework.stereotype.Service;
import java.util.Locale; // 引入 Locale
@Service
public class MyService {
private final MessageSource messageSource;
@Autowired
public MyService(MessageSource messageSource) {
this.messageSource = messageSource;
}
public String getLocalizedGreeting(String name) {
// 获取当前线程绑定的 Locale (由 LocaleResolver 决定)
Locale currentLocale = LocaleContextHolder.getLocale();
// 从 MessageSource 获取消息
String greeting = messageSource.getMessage("greeting", new Object[]{name}, currentLocale);
return greeting;
}
// 在异常处理中获取本地化消息
// GlobalExceptionHandler.java (示例)
// @ExceptionHandler(BookNotFoundException.class)
// public ResponseEntity<ErrorResponse> handleBookNotFoundForRest(BookNotFoundException ex) {
// Locale currentLocale = LocaleContextHolder.getLocale();
// String errorMessage = messageSource.getMessage("error.book.notFound", new Object[]{ex.getBookId()}, currentLocale);
// return new ResponseEntity<>(new ErrorResponse(HttpStatus.NOT_FOUND.value(), errorMessage, System.currentTimeMillis()), HttpStatus.NOT_FOUND);
// }
}
6. 与前端框架集成考量
当Spring MVC作为纯后端API服务(使用@RestController
),与前端框架(Vue.js, React, Angular等)集成时,主要需要考虑数据交互格式、接口设计和跨域问题。
集成模式
- 前后端分离: 后端Spring MVC提供RESTful API,前端框架负责整个UI渲染和用户交互。两者通过HTTP请求进行通信。这是目前主流的企业级Web应用开发模式。
- 后端渲染+前端增强: 后端Spring MVC使用Thymeleaf等模板引擎渲染基础HTML页面,前端框架用于局部增强页面交互(如通过Ajax请求更新部分内容)。图书管理系统案例属于此模式。
CORS (跨域资源共享)
跨域请求指浏览器发起的,目标URL与当前页面URL的协议、域名、端口中任意一个不同的请求。出于安全考虑,浏览器会阻止非同源的HTTP请求,除非服务器明确允许。RESTful API通常部署在与前端不同的域名或端口上,因此需要处理CORS问题。
Spring MVC提供了多种方式处理CORS:
-
@CrossOrigin
注解 : 应用在Controller类或方法上,允许来自指定来源的跨域请求。javapackage com.yourcompany.bookmanagement.controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/data") // 允许来自 http://localhost:8080 和 http://example.com 的跨域请求 @CrossOrigin(origins = {"http://localhost:8080", "http://example.com"}) public class DataController { @GetMapping("/public") public String getPublicData() { return "This is public data."; } @GetMapping("/private") @CrossOrigin("http://localhost:3000") // 方法级别的 @CrossOrigin 会覆盖类级别的设置 public String getPrivateData() { return "This is private data."; } }
-
全局 CORS 配置 : 在
WebMvcConfigurer
中集中配置,适用于更复杂的场景或希望统一管理CORS规则。
java
package com.yourcompany.bookmanagement.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; // 引入 CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {
// ... 其他 WebMvcConfigurer 方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 配置需要允许跨域的路径模式,例如所有 /api 下的请求
.allowedOrigins("http://localhost:3000", "http://your-frontend-domain.com") // 允许的来源域名
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法
.allowedHeaders("*") // 允许的请求头
.allowCredentials(true) // 是否发送 Cookie 或认证信息
.maxAge(3600); // 预检请求 (Preflight Request) 的缓存时间 (秒)
}
}
API 接口设计对前端友好的考量
- 一致的数据格式: 前端更容易处理一致的JSON结构。保持字段命名、日期格式等规范。
- 清晰的错误处理: RESTful API应返回明确的状态码,并在响应体中包含统一结构的错误信息,便于前端解析和展示错误。
- 合理的数据结构: 根据前端页面或组件需要的数据结构来设计API响应,避免过度嵌套或返回冗余字段。
- 分页与过滤: 对于列表数据,提供分页、排序、过滤等查询参数,让前端能够灵活地获取所需数据。
- 文档: 提供清晰的API文档(如Swagger/OpenAPI),方便前端开发者理解和使用接口。
7. Spring Security 入门
Spring Security是一个强大且高度可定制的认证和授权框架。它可以轻松地为Spring应用程序提供安全性。
Spring Security 核心概念
- Authentication (认证): 验证用户身份,证明"你是谁"。通常通过用户名/密码、证书等方式。
- Authorization (授权): 在身份认证后,确定用户是否有权访问某个资源或执行某个操作。
- Principal: 当前认证用户的代表,通常包含用户名、密码、权限等信息。
- GrantedAuthority: 授予Principal的权限或角色(如ROLE_USER, read_permission)。
- AuthenticationManager: 负责处理认证请求。
- AccessDecisionManager: 负责处理授权决策。
- SecurityContextHolder: 存储当前应用程序中Principal详细信息的容器。默认使用ThreadLocal策略,确保每个线程独立。
- Filter Chain (过滤器链) : Spring Security通过一系列Servlet Filter来实现各种安全功能(如认证、授权、CSRF防护)。这些Filter被组织成一个链。
DelegatingFilterProxy
是Spring Security的核心Filter,它将Servlet容器的请求委托给Spring Bean中的Security Filter Chain。
基础配置 (JavaConfig)
Spring Security通常通过JavaConfig进行配置。核心是创建一个继承自WebSecurityConfigurerAdapter
的配置类(或者使用新的SecurityFilterChain
Bean方式,取决于Spring Security版本)。
java
package com.yourcompany.bookmanagement.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; // 引入 HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; // 启用 Spring Security
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; // 引入适配器类
import org.springframework.security.core.userdetails.User; // 引入 UserDetails
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; // 引入 UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 引入密码编码器
import org.springframework.security.crypto.password.PasswordEncoder; // 引入接口
import org.springframework.security.provisioning.InMemoryUserDetailsManager; // 引入内存用户存储
// 启用 Spring Security 的 Web 安全功能
@EnableWebSecurity
@Configuration // 标记为配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter { // 继承 WebSecurityConfigurerAdapter (Spring Security 5.7+ 推荐使用 SecurityFilterChain Bean)
// 配置密码编码器 Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用 BCrypt 强哈希算法
}
// 配置用户详情服务 (AuthenticationManager 的一部分)
// 这里使用内存存储用户,实际应用中通常从数据库加载 (实现 UserDetailsService 接口)
@Bean
@Override
public UserDetailsService userDetailsService() {
// 创建一个内存用户
UserDetails user = User.builder()
.username("admin")
.password(passwordEncoder().encode("password")) // 密码必须编码
.roles("USER", "ADMIN") // 设置角色
.build();
return new InMemoryUserDetailsManager(user);
}
// 配置 HTTP 请求的安全性规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 授权配置
.authorizeRequests()
// /login, /resources/**, /webjars/** 路径无需认证即可访问
.antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
// /books/** 路径需要认证且具有 USER 或 ADMIN 角色才能访问
.antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
// /admin/** 路径需要认证且具有 ADMIN 角色才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
// 所有其他路径需要认证才能访问
.anyRequest().authenticated()
.and()
// 表单登录配置
.formLogin()
.loginPage("/login") // 指定自定义的登录页面 URL
.permitAll() // 登录页面允许所有用户访问
.and()
// 注销配置
.logout()
.logoutUrl("/logout") // 指定注销 URL (POST 请求)
.logoutSuccessUrl("/login?logout") // 注销成功后重定向的 URL
.permitAll(); // 注销 URL 允许所有用户访问
// 禁用 CSRF 防护 (仅为简化示例,生产环境应启用并处理)
// http.csrf().disable();
}
// Spring Security 5.7+ 推荐的配置方式 (使用 SecurityFilterChain Bean 替代 WebSecurityConfigurerAdapter)
/*
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 授权配置
.authorizeRequests()
.antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
.antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
// 表单登录配置
.formLogin()
.loginPage("/login")
.permitAll()
.and()
// 注销配置
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll();
return http.build();
}
*/
}
说明:
@EnableWebSecurity
注解会加载Spring Security的核心配置类。WebSecurityConfigurerAdapter
提供了一个方便的基类来定制安全性配置。configure(HttpSecurity http)
方法是配置URL授权规则、表单登录、注销等的核心。userDetailsService()
方法配置如何加载用户信息。InMemoryUserDetailsManager
用于测试,实际应用需要实现UserDetailsService
从数据库等加载。passwordEncoder()
配置密码加密器,Spring Security强制要求使用加密后的密码。BCryptPasswordEncoder
是推荐的选择。
密码加密
使用PasswordEncoder
接口对用户密码进行加密存储和比对。BCryptPasswordEncoder
使用BCrypt算法,该算法包含了随机盐值和多次哈希迭代,安全性较高。
- 存储密码时:
String encodedPassword = passwordEncoder.encode(rawPassword);
- 校验密码时:
boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);
方法级别安全注解
除了保护URL路径,Spring Security还支持通过注解保护Controller或Service方法。
-
启用方法安全 : 在任何一个
@Configuration
类上添加@EnableGlobalMethodSecurity
注解。prePostEnabled = true
: 启用@PreAuthorize
和@PostAuthorize
注解。securedEnabled = true
: 启用@Secured
注解。
javapackage com.yourcompany.bookmanagement.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; // 引入 @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 启用方法安全注解 public class MethodSecurityConfig { // 此类通常是独立的,或者合并到 SecurityConfig 中 }
-
使用注解 :
@PreAuthorize
: 在方法执行之前 进行权限检查。可以使用Spring EL表达式。@PreAuthorize("hasRole('ADMIN')")
: 只有ADMIN角色才能访问。@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
: USER或ADMIN角色都能访问。@PreAuthorize("hasPermission(#bookId, 'book', 'read')")
: 自定义权限表达式。@PreAuthorize("#username == authentication.principal.username")
: 参数username必须是当前登录用户的username。
@PostAuthorize
: 在方法执行之后 进行权限检查。可以访问返回值。@PostAuthorize("returnObject.username == authentication.principal.username")
: 返回值的username必须是当前登录用户的username。
@Secured
: 基于角色的简单权限检查,不支持Spring EL。@Secured("ROLE_ADMIN")
: 只有ROLE_ADMIN角色才能访问。@Secured({"ROLE_USER", "ROLE_ADMIN"})
: ROLE_USER或ROLE_ADMIN角色都能访问。
代码示例 (方法安全)
java
package com.yourcompany.bookmanagement.controller;
import org.springframework.security.access.annotation.Secured; // 引入 @Secured
import org.springframework.security.access.prepost.PreAuthorize; // 引入 @PreAuthorize
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/secure")
public class SecureController {
// 需要 ADMIN 角色才能访问
@GetMapping("/admin")
@Secured("ROLE_ADMIN") // 或 @PreAuthorize("hasRole('ADMIN')")
public String adminOnly() {
return "This content is for ADMINs only!";
}
// 需要 USER 或 ADMIN 角色才能访问
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public String userOrAdmin() {
return "This content is for logged-in users (USER or ADMIN)!";
}
// 示例:基于输入参数的权限检查
// 只有当请求的 username 与当前认证用户的 username 相同时才能访问
@GetMapping("/profile/{username}")
@PreAuthorize("#username == authentication.principal.username")
public String getUserProfile(@PathVariable String username) {
return "Viewing profile for user: " + username;
}
}
保护URL路径访问
通过在HttpSecurity
配置中使用antMatchers()
, regexMatchers()
, anyRequest()
等匹配器结合permitAll()
, authenticated()
, hasRole()
, hasAuthority()
等方法来定义哪些URL需要哪些权限。这是最常用的保护方式。示例已包含在基础配置代码中。
运行与部署
参考图书管理系统案例中的Maven构建WAR包和部署到Servlet容器步骤。Spring Security作为Filter Chain会自动集成到请求处理流程中。
总结
通过学习这些高级主题,你将能够构建更加健壮、安全、易于维护和集成的企业级Spring MVC应用程序。RESTful API是前后端分离的基石;统一异常处理提升用户体验和开发效率;拦截器提供灵活的请求处理增强;文件处理是常见功能;国际化使应用适应全球用户;Spring Security提供全面的安全保障。
持续学习建议:
- Spring Security 深入: 用户详情服务 (
UserDetailsService
)、自定义认证提供者、OAuth2、JWT等。 - RESTful API 进阶: API文档(Swagger/OpenAPI)、API网关、更复杂的HATEOAS实现。
- 缓存: 使用Spring Cache提升数据访问性能。
- 消息队列: 集成RabbitMQ, Kafka等处理异步任务和解耦。
- 分布式系统: 了解Spring Cloud体系。
- 最重要:转向 Spring Boot! 将这些学到的原生Spring MVC知识应用到Spring Boot环境中,你会发现开发效率的巨大提升。Spring Boot基于约定大于配置的理念,自动集成了大量常用功能(包括上述大部分高级特性),让你更专注于业务逻辑。
希望这篇文章能帮助你更好地迈向Spring MVC高级开发!