完整教程:企业级Spring MVC高级主题与实用技术讲解

企业级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中使用动词。
  • 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来精确控制响应。
+-------------+ +-----------------+ +---------------------+ +-------------+
| 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

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会根据producesconsumes参数、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会调用匹配的异常处理方法。这实现了针对性

代码示例 (基于图书管理系统)

图书管理系统案例中的GlobalExceptionHandlerBookNotFoundException就是很好的示例。

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的事务管理中,默认只对非检查型异常进行回滚。

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提供的请求处理拦截机制。
  • 生命周期: 一个请求经过拦截器链的三个阶段:
    1. preHandle(): 在Controller方法执行之前调用。如果返回true,继续执行后续拦截器和Controller;如果返回false,中断整个请求处理流程。常用于认证、权限校验、日志记录。
    2. postHandle(): 在Controller方法执行之后,视图渲染之前调用。可以访问ModelAndView,用于修改模型数据、视图名等。注意:如果Controller方法抛出异常,postHandle不会被调用。
    3. afterCompletion(): 在整个请求处理完成之后(包括视图渲染完成后),无论是否发生异常,都会调用。用于资源清理等。
  • 与 Filter 的区别:
    • Filter是Servlet规范的一部分,工作在Servlet容器层面,在DispatcherServlet之前执行,无法访问Spring MVC的上下文(如Handler)。适用于字符编码、会话管理、静态资源处理等。
    • Interceptor是Spring MVC框架的一部分,工作在DispatcherServlet内部,HandlerMapping之后。能够访问Handler、ModelAndView、Spring容器中的Bean等。适用于更细粒度的、与业务逻辑关联的拦截,如认证、权限、性能监控、日志。

创建和配置拦截器

  1. 创建拦截器类: 实现HandlerInterceptor接口。该接口定义了preHandle, postHandle, afterCompletion方法。或者,如果只需要实现部分方法,可以继承已废弃的HandlerInterceptorAdapter,但现在推荐直接实现HandlerInterceptor并使用接口的默认方法。
  2. 配置拦截器: 在实现WebMvcConfigurer接口的配置类中,通过重写addInterceptors()方法注册拦截器。

代码示例 (基于图书管理系统)

图书管理系统案例中的AuthInterceptor是认证拦截器的示例。

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

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检查用户是否已登录、是否有权访问当前资源。未通过则重定向或返回错误。
  • 请求预处理: 在preHandlepostHandle设置一些通用的请求属性、上下文信息等。
  • 性能监控: 在preHandle记录请求开始时间,在afterCompletion计算并记录总耗时。

4. 文件上传与下载

Spring MVC对文件上传和下载提供了内置支持,特别是结合Servlet 3.0+的MultipartRequest API。

文件上传

  1. 配置 MultipartResolver: Spring MVC需要一个MultipartResolver Bean来解析multipart/form-data请求。
    • StandardServletMultipartResolver: 基于Servlet 3.0+ 标准。推荐使用,无需额外依赖。
    • CommonsMultipartResolver: 基于Apache Commons FileUpload库。需要添加commons-fileupload依赖。
  2. Controller 中接收文件: 在Controller方法中使用@RequestParam MultipartFile参数接收上传的文件。MultipartFile接口提供了获取文件名、内容类型、文件大小、字节流等方法。
  3. 文件大小限制、类型校验: 可以在MultipartResolver中配置总大小和单个文件大小限制。文件类型校验通常在Controller或Service中根据file.getContentType()file.getOriginalFilename()进行。
  4. 存储上传文件: 获取到MultipartFile后,可以使用transferTo(File dest)方法将其保存到文件系统,或者获取getInputStream()/getBytes()写入数据库、云存储等。

代码示例 (文件上传)

WebMvcConfig.java 配置 StandardServletMultipartResolver:

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 方法:

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 处理文件上传:

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 示例):

<!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 示例):

<!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响应头,让浏览器以下载方式处理响应内容。

  1. Controller 方法: 可以返回ResponseEntity<Resource>,或直接操作HttpServletResponse
  2. 设置响应头: 关键在于设置正确的Content-Disposition头,指定文件名并告知浏览器以下载方式处理;Content-Type头指定文件类型;Content-Length头指定文件大小。
  3. 写入响应体: 将文件内容通过流写入HttpServletResponse的输出流。

代码示例 (文件下载)

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

<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

  1. 创建资源文件: 在src/main/resources目录下创建消息源文件,遵循basename_locale.properties的命名约定。
    • messages.properties (默认语言)
    • messages_en.properties (英语)
    • messages_zh_CN.properties (简体中文)
    • 文件内容为键值对,如:app.title=Book Management System
  2. 配置 MessageSource Bean: 在Spring配置中定义MessageSource Bean。
  3. 配置 LocaleResolver Bean: 在Spring MVC配置 (WebMvcConfigurer) 中定义LocaleResolver Bean。
  4. 配置 LocaleChangeInterceptor (可选): 如果想通过请求参数切换语言,配置LocaleChangeInterceptor

代码示例 (国际化)

资源文件示例 (src/main/resources/messages_zh_CN.properties, messages_en.properties):

messages_zh_CN.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:

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 配置 MessageSourceLocaleResolver:

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内置对象和#{...}语法获取消息。
    <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>标签。
    ">English
  • 在后端代码 (Controller/Service) 中: 通过注入MessageSource,使用getMessage()方法获取消息。
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类或方法上,允许来自指定来源的跨域请求。
    package 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规则。
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版本)。

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方法。

  1. 启用方法安全: 在任何一个@Configuration类上添加@EnableGlobalMethodSecurity注解。
    • prePostEnabled = true: 启用@PreAuthorize@PostAuthorize注解。
    • securedEnabled = true: 启用@Secured注解。
    package 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 中
    }
  2. 使用注解:
    • @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角色都能访问。

代码示例 (方法安全)

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高级开发!

posted on 2025-06-21 11:46  ljbguanli  阅读(24)  评论(0)    收藏  举报