企业级Spring MVC高级主题与实用技术讲解

[复制链接]
发表于 2025-7-9 02:10:07 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
企业级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来正确控制响应。
  1. +-------------+     +-----------------+     +---------------------+     +-------------+
  2. | User/Client | --> | DispatcherServlet | --> | RequestMappingHandler | --> | Controller  |
  3. | (Frontend)  |     +-----------------+     |       Adapter       |     +------v------+
  4. |             |     |     HTTP Req    |     +-----------+---------+            | Process Logic
  5. |             |     |                 |                 |                      | (Service/Repo)
  6. |             |     | GET /api/books/1|                 | Call Method          |
  7. |             |     | POST /api/books |                 | with @RequestBody    | Return Object
  8. |             |     | { JSON Data }   |                 |                      |
  9. +-------------+     +-----------------+     +-----------+---------+     +------^------+
  10.                            |                      | Convert to/from JSON   |
  11.                            | Look up Handler      | (@RequestBody /        | HttpMessageConverter
  12.                            |                      | @ResponseBody)         |
  13.                            v                      |                      |
  14.                      +-----------------+     +---------------------+     +------+------+
  15.                      | HandlerMapping  |     | HttpMessageConverter| <--> | JSON/XML  |
  16.                      +-----------------+     +---------------------+     +------+------+
  17.                            | Find Handler         | Serialize/Deserialize        | Write to Response
  18.                            +----------------------+------------------------------+
复制代码
代码示例 (基于图书管理体系,添加 REST API)

假设在图书管理体系中有Book实体和BookService。新增一个BookRestController:
  1. package com.yourcompany.bookmanagement.controller;
  2. import com.yourcompany.bookmanagement.entity.Book;
  3. import com.yourcompany.bookmanagement.exception.BookNotFoundException; // 复用之前的异常
  4. import com.yourcompany.bookmanagement.service.BookService; // 复用之前的 Service
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.ResponseEntity;
  8. import org.springframework.web.bind.annotation.*;
  9. import java.util.List;
  10. import java.util.Optional;
  11. @RestController // @RestController = @Controller + @ResponseBody
  12. @RequestMapping("/api/v1/books") // REST API 基础路径,v1 表示版本
  13. public class BookRestController {
  14.     private final BookService bookService;
  15.     @Autowired
  16.     public BookRestController(BookService bookService) {
  17.         this.bookService = bookService;
  18.     }
  19.     // 获取所有图书列表
  20.     // GET /api/v1/books
  21.     @GetMapping
  22.     public List<Book> getAllBooks() {
  23.         // 返回 List 会自动通过 HttpMessageConverter 转换为 JSON 数组
  24.         return bookService.findAllBooks();
  25.     }
  26.     // 获取特定图书详情
  27.     // GET /api/v1/books/{id}
  28.     @GetMapping("/{id}")
  29.     public ResponseEntity<Book> getBookById(@PathVariable Long id) {
  30.         Optional<Book> book = bookService.findBookById(id);
  31.         // 使用 ResponseEntity 控制状态码
  32.         return book.map(value -> new ResponseEntity<>(value, HttpStatus.OK)) // 找到则返回 200 OK
  33.                    .orElseThrow(() -> new BookNotFoundException(id)); // 找不到则抛出异常,由全局异常处理器处理
  34.     }
  35.     // 创建新图书
  36.     // POST /api/v1/books
  37.     @PostMapping
  38.     @ResponseStatus(HttpStatus.CREATED) // 创建成功返回 201 Created
  39.     public Book createBook(@RequestBody Book book) { // @RequestBody 将请求体 (JSON) 转换为 Book 对象
  40.         // 校验等逻辑可以在 Service 层或通过 Bean Validation 实现 (需要额外配置)
  41.         return bookService.saveBook(book); // 保存并返回新创建的图书 (可能包含生成的 ID)
  42.     }
  43.     // 更新图书
  44.     // PUT /api/v1/books/{id}
  45.     @PutMapping("/{id}")
  46.     public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
  47.         // 实际更新逻辑需要先查找,然后更新字段,最后保存
  48.         Optional<Book> existingBookOptional = bookService.findBookById(id);
  49.         if (existingBookOptional.isPresent()) {
  50.             Book existingBook = existingBookOptional.get();
  51.             // 假设只更新标题和作者,实际应根据需求更新所有字段
  52.             existingBook.setTitle(book.getTitle());
  53.             existingBook.setAuthor(book.getAuthor());
  54.             existingBook.setIsbn(book.getIsbn());
  55.             existingBook.setPublicationDate(book.getPublicationDate());
  56.             Book updatedBook = bookService.saveBook(existingBook);
  57.             return new ResponseEntity<>(updatedBook, HttpStatus.OK); // 返回更新后的图书和 200 OK
  58.         } else {
  59.             throw new BookNotFoundException(id); // 找不到则抛异常
  60.         }
  61.     }
  62.     // 删除图书
  63.     // DELETE /api/v1/books/{id}
  64.     @DeleteMapping("/{id}")
  65.     @ResponseStatus(HttpStatus.NO_CONTENT) // 删除成功返回 204 No Content
  66.     public void deleteBook(@PathVariable Long id) {
  67.         // 可以先检查是否存在,再删除
  68.         Optional<Book> book = bookService.findBookById(id);
  69.         if (!book.isPresent()) {
  70.              throw new BookNotFoundException(id); // 不存在则抛异常
  71.         }
  72.         bookService.deleteBookById(id); // 调用 Service 删除
  73.     }
  74. }
复制代码
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就是很好的示例。
  1. package com.yourcompany.bookmanagement.exception;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.http.HttpStatus;
  5. import org.springframework.http.ResponseEntity; // 用于 REST API 返回 JSON
  6. import org.springframework.web.bind.annotation.ControllerAdvice;
  7. import org.springframework.web.bind.annotation.ExceptionHandler;
  8. import org.springframework.web.bind.annotation.ResponseBody; // 用于 REST API
  9. import org.springframework.web.bind.annotation.ResponseStatus;
  10. import org.springframework.web.servlet.ModelAndView; // 用于返回视图
  11. // @ControllerAdvice 应用于所有 Controller
  12. @ControllerAdvice
  13. public class GlobalExceptionHandler {
  14.     private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  15.     // --- 针对返回视图的异常处理 (如图书管理系统案例中的 HTML 页面请求) ---
  16.     // 处理 BookNotFoundException 异常,返回 404 视图
  17.     @ExceptionHandler(BookNotFoundException.class)
  18.     @ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
  19.     public ModelAndView handleBookNotFoundForView(BookNotFoundException ex) {
  20.         logger.warn("Book not found: " + ex.getMessage());
  21.         ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.html
  22.         mav.addObject("message", ex.getMessage()); // 将错误信息添加到 Model
  23.         return mav;
  24.     }
  25.     // 处理所有其他未捕获的 Exception,返回 500 视图
  26.     @ExceptionHandler(Exception.class)
  27.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500
  28.     public ModelAndView handleAllExceptionsForView(Exception ex) {
  29.         logger.error("Internal Server Error: ", ex);
  30.         ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.html
  31.         mav.addObject("message", "Internal Server Error. Please try again later.");
  32.         return mav;
  33.     }
  34.     // --- 针对返回 JSON 的异常处理 (如 RESTful API 请求) ---
  35.     // 可以定义一个返回 JSON 格式的异常处理,但需要区分请求类型
  36.     // 或者更简单的做法是,如果你的 Controller 是 @RestController,异常处理方法也返回 @ResponseBody
  37.     // 这里以 BookNotFoundException 为例,演示返回 JSON 错误
  38.     @ExceptionHandler(BookNotFoundException.class)
  39.     @ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
  40.     @ResponseBody // 直接写入响应体,由 HttpMessageConverter 处理
  41.     public ErrorResponse handleBookNotFoundForRest(BookNotFoundException ex) {
  42.         logger.warn("Book not found for REST request: " + ex.getMessage());
  43.         // 返回统一的 JSON 错误格式
  44.         return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis());
  45.     }
  46.     // 处理 @RequestBody 参数校验失败异常 (MethodArgumentNotValidException)
  47.     // 通常在 REST API 中发生
  48.     @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
  49.     @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request
  50.     @ResponseBody
  51.     public ErrorResponse handleValidationExceptions(org.springframework.web.bind.MethodArgumentNotValidException ex) {
  52.         // 提取所有校验错误信息
  53.         String errorMessage = ex.getBindingResult().getFieldErrors().stream()
  54.                 .map(error -> error.getField() + ": " + error.getDefaultMessage())
  55.                 .collect(java.util.stream.Collectors.joining(", "));
  56.         logger.warn("Validation failed: " + errorMessage);
  57.         return new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation Failed: " + errorMessage, System.currentTimeMillis());
  58.     }
  59.     // 统一错误响应格式 (POJO)
  60.     public static class ErrorResponse {
  61.         private int status;
  62.         private String message;
  63.         private long timestamp;
  64.         public ErrorResponse(int status, String message, long timestamp) {
  65.             this.status = status;
  66.             this.message = message;
  67.             this.timestamp = timestamp;
  68.         }
  69.         // Getters for Jackson serialization
  70.         public int getStatus() { return status; }
  71.         public String getMessage() { return message; }
  72.         public long getTimestamp() { return timestamp; }
  73.     }
  74.     // 自定义异常类 (同图书管理系统案例)
  75.     // package com.yourcompany.bookmanagement.exception;
  76.     // public class BookNotFoundException extends RuntimeException {
  77.     //     public BookNotFoundException(Long id) {
  78.     //         super("Book not found with ID: " + id);
  79.     //     }
  80.     // }
  81. }
复制代码
分析:


  • 同一个@ControllerAdvice类中,可以界说多个@ExceptionHandler方法处理惩罚差别类型的异常。
  • @ExceptionHandler方法的参数可以是抛出的异常对象,返回值可以是ModelAndView, String (视图名或重定向), @ResponseBody 返回值, ResponseEntity等。
  • 通过@ResponseStatus可以设置响应的HTTP状态码。
  • 为了同时支持返回视图和返回JSON的异常处理惩罚,可以根据请求的Accept头或路径 (/api/**) 进行区分处理惩罚,大概如示例所示,为同一种异常界说两个@ExceptionHandler方法(Spring会根据返回类型等选择更匹配的谁人,大概可以通过@RequestMapping(produces = ...)等进一步限定)。在全RESTful API应用中,通常只返回JSON。
自界说异常类

根据业务需求界说自界说异常类,继承自RuntimeException(非查抄型异常)或Exception(查抄型异常)。非查抄型异常通常用于表示编程错误或运行时情况问题,查抄型异常用于表示可预期的、必要调用方显式处理惩罚的业务问题。在Spring的事件管理中,默认只对非查抄型异常进行回滚。
  1. package com.yourcompany.bookmanagement.exception;
  2. // 业务异常示例:图书库存不足
  3. public class InsufficientStockException extends RuntimeException {
  4.     private Long bookId;
  5.     private int requested;
  6.     private int available;
  7.     public InsufficientStockException(Long bookId, int requested, int available) {
  8.         super("Book " + bookId + " stock insufficient. Requested: " + requested + ", Available: " + available);
  9.         this.bookId = bookId;
  10.         this.requested = requested;
  11.         this.available = available;
  12.     }
  13.     // Getters for error details
  14.     public Long getBookId() { return bookId; }
  15.     public int getRequested() { return requested; }
  16.     public int getAvailable() { return available; }
  17. }
复制代码
然后在@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是认证拦截器的示例。
  1. package com.yourcompany.bookmanagement.interceptor;
  2. import org.springframework.web.servlet.HandlerInterceptor; // 引入接口
  3. import org.springframework.web.servlet.ModelAndView;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletResponse;
  6. import javax.servlet.http.HttpSession;
  7. public class AuthInterceptor implements HandlerInterceptor { // 实现 HandlerInterceptor 接口
  8.     // 在 Controller 方法执行前调用
  9.     @Override
  10.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  11.         String requestURI = request.getRequestURI();
  12.         System.out.println("AuthInterceptor: Intercepting request: " + requestURI);
  13.         HttpSession session = request.getSession();
  14.         Object user = session.getAttribute("loggedInUser"); // 检查 Session 中是否有名为 "loggedInUser" 的属性
  15.         if (user != null) {
  16.             // 用户已登录,放行
  17.             System.out.println("AuthInterceptor: User is logged in.");
  18.             return true; // 继续执行后续拦截器或 Controller
  19.         } else {
  20.             // 用户未登录,重定向到登录页面
  21.             System.out.println("AuthInterceptor: User is NOT logged in. Redirecting to login page.");
  22.             // 获取 contextPath,避免硬编码应用名称
  23.             response.sendRedirect(request.getContextPath() + "/login");
  24.             return false; // 阻止当前请求继续处理
  25.         }
  26.     }
  27.     // 在 Controller 方法执行后,视图渲染前调用
  28.     @Override
  29.     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  30.         // 可以在这里对 Model 或 View 进行操作
  31.         // System.out.println("AuthInterceptor postHandle...");
  32.     }
  33.     // 在整个请求处理完成后调用
  34.     @Override
  35.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  36.         // 用于资源清理等
  37.         // System.out.println("AuthInterceptor afterCompletion...");
  38.     }
  39. }
复制代码
拦截器配置 (WebMvcConfig.java):
  1. package com.yourcompany.bookmanagement.config;
  2. import com.yourcompany.bookmanagement.interceptor.AuthInterceptor; // 引入拦截器类
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  6. import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
  7. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 引入 WebMvcConfigurer
  8. @Configuration
  9. @EnableWebMvc
  10. // @ComponentScan...
  11. public class WebMvcConfig implements WebMvcConfigurer { // 实现 WebMvcConfigurer
  12.     // 将拦截器注册为 Spring Bean
  13.     @Bean
  14.     public AuthInterceptor authInterceptor() {
  15.         return new AuthInterceptor();
  16.     }
  17.     // 重写 addInterceptors 方法配置拦截器
  18.     @Override
  19.     public void addInterceptors(InterceptorRegistry registry) {
  20.         registry.addInterceptor(authInterceptor()) // 添加拦截器实例
  21.                 .addPathPatterns("/**") // 配置需要拦截的路径模式,"/**" 表示拦截所有请求
  22.                 .excludePathPatterns("/login", "/logout", "/resources/**", "/webjars/**"); // 配置排除的路径模式,登录、注销、静态资源通常需要排除
  23.     }
  24.     // ... 其他配置,如 ViewResolver, MultipartResolver 等
  25. }
复制代码
典型应用场景



  • 日志记录: 在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:
  1. package com.yourcompany.bookmanagement.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.multipart.MultipartResolver; // 引入接口
  5. import org.springframework.web.multipart.support.StandardServletMultipartResolver; // 引入 Standard 实现
  6. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  7. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  8. @Configuration
  9. @EnableWebMvc
  10. // @ComponentScan...
  11. public class WebMvcConfig implements WebMvcConfigurer {
  12.     // 配置 StandardServletMultipartResolver
  13.     // Servlet 3.0+ 容器会自动提供 MultipartConfigElement,StandardServletMultipartResolver 基于此工作
  14.     // 文件大小等限制可以在 Servlet 注册时(例如 MyWebAppInitializer)或通过容器配置实现
  15.     @Bean
  16.     public MultipartResolver multipartResolver() {
  17.         return new StandardServletMultipartResolver();
  18.     }
  19.     // ... 其他配置
  20. }
复制代码
假如在 MyWebAppInitializer.java 中必要配置上传属性 (如文件大小限定),可以重写 customizeRegistration 方法:
  1. package com.yourcompany.bookmanagement.config;
  2. import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
  3. import javax.servlet.MultipartConfigElement; // 引入
  4. import javax.servlet.ServletRegistration; // 引入
  5. public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  6.     // ... getRootConfigClasses, getServletConfigClasses, getServletMappings methods
  7.     @Override
  8.     protected void customizeRegistration(ServletRegistration.Dynamic registration) {
  9.         // 配置文件上传属性:临时文件存放路径,最大文件大小,最大请求大小,文件阈值大小
  10.         // 这些配置会传递给 StandardServletMultipartResolver
  11.         String fileUploadTempDir = System.getProperty("java.io.tmpdir"); // 使用系统的临时目录
  12.         long maxFileSize = 5 * 1024 * 1024; // 5MB
  13.         long maxRequestSize = 10 * 1024 * 1024; // 10MB
  14.         int fileSizeThreshold = 0; // 所有文件都直接写入临时文件,而不是内存
  15.         MultipartConfigElement multipartConfigElement = new MultipartConfigElement(
  16.                 fileUploadTempDir,
  17.                 maxFileSize,
  18.                 maxRequestSize,
  19.                 fileSizeThreshold);
  20.         registration.setMultipartConfig(multipartConfigElement);
  21.     }
  22. }
复制代码
Controller 处理惩罚文件上传:
  1. package com.yourcompany.bookmanagement.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.PostMapping;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RequestParam;
  7. import org.springframework.web.multipart.MultipartFile;
  8. import org.springframework.web.servlet.mvc.support.RedirectAttributes;
  9. import java.io.IOException;
  10. import java.nio.file.Files; // 使用 NIO.2 进行文件操作
  11. import java.nio.file.Path;
  12. import java.nio.file.Paths;
  13. @Controller
  14. @RequestMapping("/files")
  15. public class FileUploadController {
  16.     // 文件保存的根目录 (请根据实际环境修改!)
  17.     // 生产环境不应该硬编码在此处,应从配置读取
  18.     private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!
  19.     @GetMapping("/upload")
  20.     public String showUploadForm() {
  21.         return "uploadForm"; // 返回上传表单视图 (如 uploadForm.html)
  22.     }
  23.     @PostMapping("/upload")
  24.     public String handleFileUpload(@RequestParam("file") MultipartFile file,
  25.                                    RedirectAttributes redirectAttributes) {
  26.         // 简单校验:文件是否为空
  27.         if (file.isEmpty()) {
  28.             redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
  29.             return "redirect:/files/uploadStatus"; // 重定向到状态页面
  30.         }
  31.         try {
  32.             // 获取文件名
  33.             String fileName = file.getOriginalFilename();
  34.             // 防止路径穿越等安全问题,实际应用中应更严格处理文件名
  35.             Path path = Paths.get(UPLOADED_FOLDER + fileName);
  36.             // 创建目标目录 (如果不存在)
  37.             Files.createDirectories(path.getParent());
  38.             // 将文件保存到目标路径
  39.             Files.copy(file.getInputStream(), path); // 使用 NIO.2 Copy Stream
  40.             redirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + fileName + "'");
  41.         } catch (IOException e) {
  42.             e.printStackTrace();
  43.             redirectAttributes.addFlashAttribute("message", "Failed to upload file: " + e.getMessage());
  44.         }
  45.         return "redirect:/files/uploadStatus"; // 重定向到状态页面显示结果
  46.     }
  47.     @GetMapping("/uploadStatus")
  48.     public String uploadStatus() {
  49.         return "uploadStatus"; // 返回上传状态视图 (如 uploadStatus.html)
  50.     }
  51. }
复制代码
上传表单视图 (uploadForm.html - Thymeleaf 示例):
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>File Upload</title>
  6. </head>
  7. <body>
  8.     <h1>Upload File</h1>
  9.     <!-- 注意:method 必须是 POST,enctype 必须是 multipart/form-data -->
  10.     <form method="POST" action="/files/upload" enctype="multipart/form-data">
  11.         <div>
  12.             <label for="file">Select File:</label>
  13.             <input type="file" name="file" id="file" required/>
  14.         </div>
  15.         <div>
  16.             <button type="submit">Upload</button>
  17.         </div>
  18.     </form>
  19. </body>
  20. </html>
复制代码
上传状态视图 (uploadStatus.html - Thymeleaf 示例):
  1. <!DOCTYPE html>
  2. <html xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Upload Status</title>
  6. </head>
  7. <body>
  8.     <h1>Upload Status</h1>
  9.     <!-- 使用 th:text 获取 RedirectAttributes 中的 Flash 属性 -->
  10.     <div th:if="${message}">
  11.         <p th:text="${message}"></p>
  12.     </div>
  13.     <p><a th:href="@{/files/upload}">Upload Another File</a></p>
  14. </body>
  15. </html>
复制代码
文件下载

文件下载通常是通过设置HTTP响应头,让浏览器以下载方式处理惩罚响应内容。

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

  1. package com.yourcompany.bookmanagement.controller;
  2. import org.springframework.core.io.InputStreamResource; // 引入资源类型
  3. import org.springframework.core.io.Resource; // 引入资源接口
  4. import org.springframework.http.HttpHeaders; // 引入 HTTP 头部
  5. import org.springframework.http.MediaType; // 引入媒体类型
  6. import org.springframework.http.ResponseEntity; // 引入 ResponseEntity
  7. import org.springframework.stereotype.Controller;
  8. import org.springframework.web.bind.annotation.GetMapping;
  9. import org.springframework.web.bind.annotation.PathVariable;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import javax.servlet.http.HttpServletRequest; // 可能需要
  12. import java.io.File;
  13. import java.io.FileInputStream;
  14. import java.io.IOException;
  15. import java.nio.file.Files;
  16. import java.nio.file.Path;
  17. import java.nio.file.Paths;
  18. @Controller
  19. @RequestMapping("/files")
  20. public class FileDownloadController {
  21.     // 文件存放的根目录 (同上传示例)
  22.     private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!
  23.     // 文件下载方法,返回 ResponseEntity<Resource>
  24.     @GetMapping("/download/{fileName:.+}") // :.+ 匹配文件名,包括点号
  25.     public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
  26.         Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize(); // 构造文件路径
  27.         File file = filePath.toFile();
  28.         // 检查文件是否存在且可读
  29.         if (!file.exists() || !file.canRead()) {
  30.             // 抛出异常或返回 404 响应
  31.             // return new ResponseEntity<>(HttpStatus.NOT_FOUND);
  32.              throw new RuntimeException("File not found or cannot be read: " + fileName); // 示例抛出异常
  33.         }
  34.         try {
  35.             // 确定文件的 Content-Type
  36.             String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
  37.             if (contentType == null) {
  38.                 contentType = "application/octet-stream"; // 默认类型
  39.             }
  40.             // 创建 InputStreamResource
  41.             InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
  42.             // 构建响应
  43.             return ResponseEntity.ok()
  44.                     .contentType(MediaType.parseMediaType(contentType)) // 设置 Content-Type
  45.                     // 设置 Content-Disposition,inline 表示在线打开,attachment 表示下载
  46.                     .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + file.getName() + """)
  47.                     .contentLength(file.length()) // 设置 Content-Length
  48.                     .body(resource); // 设置响应体
  49.         } catch (IOException ex) {
  50.             // 记录错误日志并返回 500 响应或抛出异常
  51.             ex.printStackTrace();
  52.             // return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
  53.             throw new RuntimeException("Error reading file: " + fileName, ex); // 示例抛出异常
  54.         }
  55.     }
  56.     /*
  57.     // 另一种使用 HttpServletResponse 的方式 (不推荐,因为它绕过了 Spring 的 HttpMessageConverter 等)
  58.     @GetMapping("/download2/{fileName:.+}")
  59.     public void downloadFile2(@PathVariable String fileName, HttpServletResponse response) throws IOException {
  60.          Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();
  61.          File file = filePath.toFile();
  62.          if (!file.exists() || !file.canRead()) {
  63.             response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
  64.             return;
  65.          }
  66.          String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
  67.          if (contentType == null) {
  68.              contentType = "application/octet-stream";
  69.          }
  70.          response.setContentType(contentType);
  71.          response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename="" + file.getName() + """);
  72.          response.setContentLength((int) file.length());
  73.          try (InputStream is = new FileInputStream(file);
  74.               OutputStream os = response.getOutputStream()) {
  75.              byte[] buffer = new byte[1024];
  76.              int len;
  77.              while ((len = is.read(buffer)) != -1) {
  78.                  os.write(buffer, 0, len);
  79.              }
  80.              os.flush();
  81.          } catch (IOException e) {
  82.              // 错误处理
  83.              e.printStackTrace();
  84.              throw e; // 抛出异常让 Spring 的异常处理器处理
  85.          }
  86.     }
  87.     */
  88. }
复制代码
下载链接示例 (HTML):
  1. <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:
  1. app.title=图书管理系统
  2. book.list.title=图书列表
  3. book.detail.title=图书详情
  4. book.form.add=新增图书
  5. book.form.edit=编辑图书
  6. book.title=标题
  7. book.author=作者
  8. book.isbn=ISBN
  9. book.publicationDate=出版日期
  10. button.save=保存
  11. button.cancel=取消
  12. action.details=详情
  13. action.edit=编辑
  14. action.delete=删除
  15. message.save.success=图书信息保存成功!
  16. message.delete.success=图书删除成功!
  17. error.book.notFound=找不到ID为 {0} 的图书。
  18. validation.NotBlank=字段不能为空
  19. validation.Size=字段长度不符合要求
  20. validation.Pattern=字段格式不正确
  21. validation.PastOrPresent=日期不能晚于今天
  22. login.title=用户登录
  23. login.username=用户名
  24. login.password=密码
  25. login.button=登录
  26. login.error=用户名或密码不正确。
  27. logout.success=您已成功注销。
复制代码
messages_en.properties:
  1. app.title=Book Management System
  2. book.list.title=Book List
  3. book.detail.title=Book Detail
  4. book.form.add=Add Book
  5. book.form.edit=Edit Book
  6. book.title=Title
  7. book.author=Author
  8. book.isbn=ISBN
  9. book.publicationDate=Publication Date
  10. button.save=Save
  11. button.cancel=Cancel
  12. action.details=Details
  13. action.edit=Edit
  14. action.delete=Delete
  15. message.save.success=Book information saved successfully!
  16. message.delete.success=Book deleted successfully!
  17. error.book.notFound=Book not found with ID: {0}.
  18. validation.NotBlank=Field must not be blank
  19. validation.Size=Field size constraints violated
  20. validation.Pattern=Field format is incorrect
  21. validation.PastOrPresent=Date cannot be in the future
  22. login.title=User Login
  23. login.username=Username
  24. login.password=Password
  25. login.button=Login
  26. login.error=Invalid username or password.
  27. logout.success=You have been logged out successfully.
复制代码
WebMvcConfig.java 配置 MessageSource 和 LocaleResolver:
  1. package com.yourcompany.bookmanagement.config;
  2. import org.springframework.context.MessageSource; // 引入 MessageSource
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.context.support.ResourceBundleMessageSource; // 引入实现类
  6. import org.springframework.web.servlet.LocaleResolver; // 引入 LocaleResolver
  7. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  8. import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
  9. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  10. import org.springframework.web.servlet.i18n.CookieLocaleResolver; // 引入 CookieLocaleResolver
  11. import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; // 引入 LocaleChangeInterceptor
  12. import java.util.Locale; // 引入 Locale
  13. @Configuration
  14. @EnableWebMvc
  15. // @ComponentScan...
  16. public class WebMvcConfig implements WebMvcConfigurer {
  17.     // ... MultipartResolver, AuthInterceptor 等其他 Bean
  18.     // 配置 MessageSource Bean
  19.     @Bean
  20.     public MessageSource messageSource() {
  21.         ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
  22.         // 设置资源文件的 basename (不带语言和后缀)
  23.         messageSource.setBasename("messages"); // 对应 src/main/resources/messages.properties, messages_en.properties 等
  24.         messageSource.setDefaultEncoding("UTF-8"); // 设置编码
  25.         messageSource.setUseCodeAsDefaultMessage(true); // 如果找不到对应的 code,使用 code 本身作为消息
  26.         return messageSource;
  27.     }
  28.     // 配置 LocaleResolver Bean
  29.     // 使用 CookieLocaleResolver 将用户选择的语言存储在 Cookie 中
  30.     @Bean
  31.     public LocaleResolver localeResolver() {
  32.         CookieLocaleResolver localeResolver = new CookieLocaleResolver();
  33.         localeResolver.setDefaultLocale(Locale.CHINA); // 设置默认区域为中国
  34.         localeResolver.setCookieName("mylocale"); // 设置存储 Locale 的 Cookie 名称
  35.         localeResolver.setCookieMaxAge(3600); // Cookie 有效期 (秒)
  36.         return localeResolver;
  37.     }
  38.     // 配置 LocaleChangeInterceptor,用于通过参数切换语言
  39.     @Bean
  40.     public LocaleChangeInterceptor localeChangeInterceptor() {
  41.         LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
  42.         // 设置参数名,例如通过访问 /books?lang=en 或 /books?lang=zh_CN 切换语言
  43.         interceptor.setParamName("lang");
  44.         return interceptor;
  45.     }
  46.     // 将 LocaleChangeInterceptor 注册到拦截器链中
  47.     @Override
  48.     public void addInterceptors(InterceptorRegistry registry) {
  49.         // ... 注册 AuthInterceptor
  50.         registry.addInterceptor(localeChangeInterceptor()); // 注册语言切换拦截器
  51.     }
  52.     // ... 其他 WebMvcConfigurer 方法
  53. }
复制代码
在视图和后端使用本地化消息



  • 在视图中 (Thymeleaf): 使用#messages内置对象和#{...}语法获取消息。
    1. <h1 th:text="#{book.list.title}">图书列表</h1>
    2. <p th:text="#{message.save.success}"></p>
    3. <!-- 获取带参数的消息,例如 error.book.notFound=找不到ID为 {0} 的图书。 -->
    4. <p th:text="#{error.book.notFound(${bookId})}"></p>
    5. <!-- 生成带语言切换参数的 URL -->
    6. <a th:href="@{/books(lang='en')}">English</a> | <a th:href="@{/books(lang='zh_CN')}">中文</a>
    复制代码
  • 在视图中 (JSP): 必要配置Spring标签库,并使用<spring:message>标签。
    1. <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
    2. <spring:message code="book.list.title"/>
    3. <spring:message code="error.book.notFound" arguments="${bookId}"/>
    4. <a href="<spring:url value='/books'><spring:param name='lang' value='en'/></spring:url>">English</a>
    复制代码
  • 在后端代码 (Controller/Service) 中: 通过注入MessageSource,使用getMessage()方法获取消息。
  1. package com.yourcompany.bookmanagement.service.impl;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.MessageSource; // 引入 MessageSource
  4. import org.springframework.context.i18n.LocaleContextHolder; // 引入 LocaleContextHolder
  5. import org.springframework.stereotype.Service;
  6. import java.util.Locale; // 引入 Locale
  7. @Service
  8. public class MyService {
  9.     private final MessageSource messageSource;
  10.     @Autowired
  11.     public MyService(MessageSource messageSource) {
  12.         this.messageSource = messageSource;
  13.     }
  14.     public String getLocalizedGreeting(String name) {
  15.         // 获取当前线程绑定的 Locale (由 LocaleResolver 决定)
  16.         Locale currentLocale = LocaleContextHolder.getLocale();
  17.         // 从 MessageSource 获取消息
  18.         String greeting = messageSource.getMessage("greeting", new Object[]{name}, currentLocale);
  19.         return greeting;
  20.     }
  21.     // 在异常处理中获取本地化消息
  22.     // GlobalExceptionHandler.java (示例)
  23.     // @ExceptionHandler(BookNotFoundException.class)
  24.     // public ResponseEntity<ErrorResponse> handleBookNotFoundForRest(BookNotFoundException ex) {
  25.     //     Locale currentLocale = LocaleContextHolder.getLocale();
  26.     //     String errorMessage = messageSource.getMessage("error.book.notFound", new Object[]{ex.getBookId()}, currentLocale);
  27.     //     return new ResponseEntity<>(new ErrorResponse(HttpStatus.NOT_FOUND.value(), errorMessage, System.currentTimeMillis()), HttpStatus.NOT_FOUND);
  28.     // }
  29. }
复制代码
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类或方法上,答应来自指定来源的跨域请求。
    1. package com.yourcompany.bookmanagement.controller;
    2. import org.springframework.web.bind.annotation.CrossOrigin;
    3. import org.springframework.web.bind.annotation.GetMapping;
    4. import org.springframework.web.bind.annotation.RequestMapping;
    5. import org.springframework.web.bind.annotation.RestController;
    6. @RestController
    7. @RequestMapping("/api/data")
    8. // 允许来自 http://localhost:8080 和 http://example.com 的跨域请求
    9. @CrossOrigin(origins = {"http://localhost:8080", "http://example.com"})
    10. public class DataController {
    11.     @GetMapping("/public")
    12.     public String getPublicData() {
    13.         return "This is public data.";
    14.     }
    15.     @GetMapping("/private")
    16.     @CrossOrigin("http://localhost:3000") // 方法级别的 @CrossOrigin 会覆盖类级别的设置
    17.     public String getPrivateData() {
    18.         return "This is private data.";
    19.     }
    20. }
    复制代码
  • 全局 CORS 配置: 在WebMvcConfigurer中集中配置,实用于更复杂的场景或希望同一管理CORS规则。
  1. package com.yourcompany.bookmanagement.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.servlet.config.annotation.CorsRegistry; // 引入 CorsRegistry
  4. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. @Configuration
  7. @EnableWebMvc
  8. // @ComponentScan...
  9. public class WebMvcConfig implements WebMvcConfigurer {
  10.     // ... 其他 WebMvcConfigurer 方法
  11.     @Override
  12.     public void addCorsMappings(CorsRegistry registry) {
  13.         registry.addMapping("/api/**") // 配置需要允许跨域的路径模式,例如所有 /api 下的请求
  14.                 .allowedOrigins("http://localhost:3000", "http://your-frontend-domain.com") // 允许的来源域名
  15.                 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法
  16.                 .allowedHeaders("*") // 允许的请求头
  17.                 .allowCredentials(true) // 是否发送 Cookie 或认证信息
  18.                 .maxAge(3600); // 预检请求 (Preflight Request) 的缓存时间 (秒)
  19.     }
  20. }
复制代码
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版本)。
  1. package com.yourcompany.bookmanagement.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.annotation.web.builders.HttpSecurity; // 引入 HttpSecurity
  5. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; // 启用 Spring Security
  6. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; // 引入适配器类
  7. import org.springframework.security.core.userdetails.User; // 引入 UserDetails
  8. import org.springframework.security.core.userdetails.UserDetails;
  9. import org.springframework.security.core.userdetails.UserDetailsService; // 引入 UserDetailsService
  10. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 引入密码编码器
  11. import org.springframework.security.crypto.password.PasswordEncoder; // 引入接口
  12. import org.springframework.security.provisioning.InMemoryUserDetailsManager; // 引入内存用户存储
  13. // 启用 Spring Security 的 Web 安全功能
  14. @EnableWebSecurity
  15. @Configuration // 标记为配置类
  16. public class SecurityConfig extends WebSecurityConfigurerAdapter { // 继承 WebSecurityConfigurerAdapter (Spring Security 5.7+ 推荐使用 SecurityFilterChain Bean)
  17.     // 配置密码编码器 Bean
  18.     @Bean
  19.     public PasswordEncoder passwordEncoder() {
  20.         return new BCryptPasswordEncoder(); // 使用 BCrypt 强哈希算法
  21.     }
  22.     // 配置用户详情服务 (AuthenticationManager 的一部分)
  23.     // 这里使用内存存储用户,实际应用中通常从数据库加载 (实现 UserDetailsService 接口)
  24.     @Bean
  25.     @Override
  26.     public UserDetailsService userDetailsService() {
  27.         // 创建一个内存用户
  28.         UserDetails user = User.builder()
  29.                 .username("admin")
  30.                 .password(passwordEncoder().encode("password")) // 密码必须编码
  31.                 .roles("USER", "ADMIN") // 设置角色
  32.                 .build();
  33.         return new InMemoryUserDetailsManager(user);
  34.     }
  35.     // 配置 HTTP 请求的安全性规则
  36.     @Override
  37.     protected void configure(HttpSecurity http) throws Exception {
  38.         http
  39.             // 授权配置
  40.             .authorizeRequests()
  41.                 // /login, /resources/**, /webjars/** 路径无需认证即可访问
  42.                 .antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
  43.                 // /books/** 路径需要认证且具有 USER 或 ADMIN 角色才能访问
  44.                 .antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
  45.                 // /admin/** 路径需要认证且具有 ADMIN 角色才能访问
  46.                 .antMatchers("/admin/**").hasRole("ADMIN")
  47.                 // 所有其他路径需要认证才能访问
  48.                 .anyRequest().authenticated()
  49.             .and()
  50.             // 表单登录配置
  51.             .formLogin()
  52.                 .loginPage("/login") // 指定自定义的登录页面 URL
  53.                 .permitAll() // 登录页面允许所有用户访问
  54.             .and()
  55.             // 注销配置
  56.             .logout()
  57.                 .logoutUrl("/logout") // 指定注销 URL (POST 请求)
  58.                 .logoutSuccessUrl("/login?logout") // 注销成功后重定向的 URL
  59.                 .permitAll(); // 注销 URL 允许所有用户访问
  60.         // 禁用 CSRF 防护 (仅为简化示例,生产环境应启用并处理)
  61.         // http.csrf().disable();
  62.     }
  63.     // Spring Security 5.7+ 推荐的配置方式 (使用 SecurityFilterChain Bean 替代 WebSecurityConfigurerAdapter)
  64.     /*
  65.     @Bean
  66.     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  67.          http
  68.             // 授权配置
  69.             .authorizeRequests()
  70.                 .antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
  71.                 .antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
  72.                 .antMatchers("/admin/**").hasRole("ADMIN")
  73.                 .anyRequest().authenticated()
  74.             .and()
  75.             // 表单登录配置
  76.             .formLogin()
  77.                 .loginPage("/login")
  78.                 .permitAll()
  79.             .and()
  80.             // 注销配置
  81.             .logout()
  82.                 .logoutUrl("/logout")
  83.                 .logoutSuccessUrl("/login?logout")
  84.                 .permitAll();
  85.         return http.build();
  86.     }
  87.      */
  88. }
复制代码
分析:


  • @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注解。
    1. package com.yourcompany.bookmanagement.config;
    2. import org.springframework.context.annotation.Configuration;
    3. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; // 引入
    4. @Configuration
    5. @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 启用方法安全注解
    6. public class MethodSecurityConfig {
    7.     // 此类通常是独立的,或者合并到 SecurityConfig 中
    8. }
    复制代码

  • 使用注解:

    • @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角色都能访问。


代码示例 (方法安全)

  1. package com.yourcompany.bookmanagement.controller;
  2. import org.springframework.security.access.annotation.Secured; // 引入 @Secured
  3. import org.springframework.security.access.prepost.PreAuthorize; // 引入 @PreAuthorize
  4. import org.springframework.web.bind.annotation.*;
  5. @RestController
  6. @RequestMapping("/secure")
  7. public class SecureController {
  8.     // 需要 ADMIN 角色才能访问
  9.     @GetMapping("/admin")
  10.     @Secured("ROLE_ADMIN") // 或 @PreAuthorize("hasRole('ADMIN')")
  11.     public String adminOnly() {
  12.         return "This content is for ADMINs only!";
  13.     }
  14.     // 需要 USER 或 ADMIN 角色才能访问
  15.     @GetMapping("/user")
  16.     @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
  17.     public String userOrAdmin() {
  18.         return "This content is for logged-in users (USER or ADMIN)!";
  19.     }
  20.     // 示例:基于输入参数的权限检查
  21.     // 只有当请求的 username 与当前认证用户的 username 相同时才能访问
  22.     @GetMapping("/profile/{username}")
  23.     @PreAuthorize("#username == authentication.principal.username")
  24.     public String getUserProfile(@PathVariable String username) {
  25.         return "Viewing profile for user: " + username;
  26.     }
  27. }
复制代码
掩护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高级开辟!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

© 2001-2025 Discuz! Team. Powered by Discuz! X3.5

GMT+8, 2025-7-25 08:06 , Processed in 0.085878 second(s), 30 queries 手机版|qidao123.com技术社区-IT企服评测▪应用市场 ( 浙ICP备20004199 )|网站地图

快速回复 返回顶部 返回列表