企业级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来精确控制响应。
svg 复制代码
+-------------+     +-----------------+     +---------------------+     +-------------+
| User/Client | --> | DispatcherServlet | --> | RequestMappingHandler | --> | Controller  |
| (Frontend)  |     +-----------------+     |       Adapter       |     +------v------+
|             |     |     HTTP Req    |     +-----------+---------+            | Process Logic
|             |     |                 |                 |                      | (Service/Repo)
|             |     | GET /api/books/1|                 | Call Method          |
|             |     | POST /api/books |                 | with @RequestBody    | Return Object
|             |     | { JSON Data }   |                 |                      |
+-------------+     +-----------------+     +-----------+---------+     +------^------+
                           |                      | Convert to/from JSON   |
                           | Look up Handler      | (@RequestBody /        | HttpMessageConverter
                           |                      | @ResponseBody)         |
                           v                      |                      |
                     +-----------------+     +---------------------+     +------+------+
                     | HandlerMapping  |     | HttpMessageConverter| <--> | JSON/XML  |
                     +-----------------+     +---------------------+     +------+------+
                           | Find Handler         | Serialize/Deserialize        | Write to Response
                           +----------------------+------------------------------+

代码示例 (基于图书管理系统,添加 REST API)

假设在图书管理系统中有Book实体和BookService。新增一个BookRestController

java 复制代码
package com.yourcompany.bookmanagement.controller;

import com.yourcompany.bookmanagement.entity.Book;
import com.yourcompany.bookmanagement.exception.BookNotFoundException; // 复用之前的异常
import com.yourcompany.bookmanagement.service.BookService; // 复用之前的 Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController // @RestController = @Controller + @ResponseBody
@RequestMapping("/api/v1/books") // REST API 基础路径,v1 表示版本
public class BookRestController {

    private final BookService bookService;

    @Autowired
    public BookRestController(BookService bookService) {
        this.bookService = bookService;
    }

    // 获取所有图书列表
    // GET /api/v1/books
    @GetMapping
    public List<Book> getAllBooks() {
        // 返回 List 会自动通过 HttpMessageConverter 转换为 JSON 数组
        return bookService.findAllBooks();
    }

    // 获取特定图书详情
    // GET /api/v1/books/{id}
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Optional<Book> book = bookService.findBookById(id);
        // 使用 ResponseEntity 控制状态码
        return book.map(value -> new ResponseEntity<>(value, HttpStatus.OK)) // 找到则返回 200 OK
                   .orElseThrow(() -> new BookNotFoundException(id)); // 找不到则抛出异常,由全局异常处理器处理
    }

    // 创建新图书
    // POST /api/v1/books
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED) // 创建成功返回 201 Created
    public Book createBook(@RequestBody Book book) { // @RequestBody 将请求体 (JSON) 转换为 Book 对象
        // 校验等逻辑可以在 Service 层或通过 Bean Validation 实现 (需要额外配置)
        return bookService.saveBook(book); // 保存并返回新创建的图书 (可能包含生成的 ID)
    }

    // 更新图书
    // PUT /api/v1/books/{id}
    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
        // 实际更新逻辑需要先查找,然后更新字段,最后保存
        Optional<Book> existingBookOptional = bookService.findBookById(id);
        if (existingBookOptional.isPresent()) {
            Book existingBook = existingBookOptional.get();
            // 假设只更新标题和作者,实际应根据需求更新所有字段
            existingBook.setTitle(book.getTitle());
            existingBook.setAuthor(book.getAuthor());
            existingBook.setIsbn(book.getIsbn());
            existingBook.setPublicationDate(book.getPublicationDate());
            Book updatedBook = bookService.saveBook(existingBook);
            return new ResponseEntity<>(updatedBook, HttpStatus.OK); // 返回更新后的图书和 200 OK
        } else {
            throw new BookNotFoundException(id); // 找不到则抛异常
        }
    }

    // 删除图书
    // DELETE /api/v1/books/{id}
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT) // 删除成功返回 204 No Content
    public void deleteBook(@PathVariable Long id) {
        // 可以先检查是否存在,再删除
        Optional<Book> book = bookService.findBookById(id);
        if (!book.isPresent()) {
             throw new BookNotFoundException(id); // 不存在则抛异常
        }
        bookService.deleteBookById(id); // 调用 Service 删除
    }
}

JSON 数据交互的最佳实践

  • 一致的格式: 保持请求和响应JSON结构的命名规范、日期格式等一致。
  • 合适的 Content-Type : 请求时使用 application/json,响应时服务器返回 application/json。Spring MVC会根据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就是很好的示例。

java 复制代码
package com.yourcompany.bookmanagement.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; // 用于 REST API 返回 JSON
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; // 用于 REST API
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView; // 用于返回视图

// @ControllerAdvice 应用于所有 Controller
@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // --- 针对返回视图的异常处理 (如图书管理系统案例中的 HTML 页面请求) ---
    // 处理 BookNotFoundException 异常,返回 404 视图
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
    public ModelAndView handleBookNotFoundForView(BookNotFoundException ex) {
        logger.warn("Book not found: " + ex.getMessage());
        ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.html
        mav.addObject("message", ex.getMessage()); // 将错误信息添加到 Model
        return mav;
    }

    // 处理所有其他未捕获的 Exception,返回 500 视图
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500
    public ModelAndView handleAllExceptionsForView(Exception ex) {
        logger.error("Internal Server Error: ", ex);
        ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.html
        mav.addObject("message", "Internal Server Error. Please try again later.");
        return mav;
    }


    // --- 针对返回 JSON 的异常处理 (如 RESTful API 请求) ---
    // 可以定义一个返回 JSON 格式的异常处理,但需要区分请求类型
    // 或者更简单的做法是,如果你的 Controller 是 @RestController,异常处理方法也返回 @ResponseBody
    // 这里以 BookNotFoundException 为例,演示返回 JSON 错误
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404
    @ResponseBody // 直接写入响应体,由 HttpMessageConverter 处理
    public ErrorResponse handleBookNotFoundForRest(BookNotFoundException ex) {
        logger.warn("Book not found for REST request: " + ex.getMessage());
        // 返回统一的 JSON 错误格式
        return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis());
    }

    // 处理 @RequestBody 参数校验失败异常 (MethodArgumentNotValidException)
    // 通常在 REST API 中发生
    @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request
    @ResponseBody
    public ErrorResponse handleValidationExceptions(org.springframework.web.bind.MethodArgumentNotValidException ex) {
        // 提取所有校验错误信息
        String errorMessage = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(java.util.stream.Collectors.joining(", "));

        logger.warn("Validation failed: " + errorMessage);
        return new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation Failed: " + errorMessage, System.currentTimeMillis());
    }

    // 统一错误响应格式 (POJO)
    public static class ErrorResponse {
        private int status;
        private String message;
        private long timestamp;

        public ErrorResponse(int status, String message, long timestamp) {
            this.status = status;
            this.message = message;
            this.timestamp = timestamp;
        }

        // Getters for Jackson serialization
        public int getStatus() { return status; }
        public String getMessage() { return message; }
        public long getTimestamp() { return timestamp; }
    }

    // 自定义异常类 (同图书管理系统案例)
    // package com.yourcompany.bookmanagement.exception;
    // public class BookNotFoundException extends RuntimeException {
    //     public BookNotFoundException(Long id) {
    //         super("Book not found with ID: " + id);
    //     }
    // }
}

说明:

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

自定义异常类

根据业务需求定义自定义异常类,继承自RuntimeException(非检查型异常)或Exception(检查型异常)。非检查型异常通常用于表示编程错误或运行时环境问题,检查型异常用于表示可预期的、需要调用方显式处理的业务问题。在Spring的事务管理中,默认只对非检查型异常进行回滚。

java 复制代码
package com.yourcompany.bookmanagement.exception;

// 业务异常示例:图书库存不足
public class InsufficientStockException extends RuntimeException {
    private Long bookId;
    private int requested;
    private int available;

    public InsufficientStockException(Long bookId, int requested, int available) {
        super("Book " + bookId + " stock insufficient. Requested: " + requested + ", Available: " + available);
        this.bookId = bookId;
        this.requested = requested;
        this.available = available;
    }

    // Getters for error details
    public Long getBookId() { return bookId; }
    public int getRequested() { return requested; }
    public int getAvailable() { return available; }
}

然后在@ControllerAdvice中添加对应的@ExceptionHandler处理方法。

异常处理策略

  • 业务异常: 对于应用程序的正常流程中可能发生的、可预期的错误(如用户不存在、库存不足),定义特定的自定义异常。在Service层或Controller层捕获或抛出,由全局异常处理器返回友好的错误信息(给用户)和具体的错误代码(给前端/客户端)。
  • 系统异常: 对于意料之外的错误(如数据库连接失败、空指针异常),通常抛出RuntimeException或其子类。全局异常处理器应记录详细日志(给开发者),并返回通用的错误提示(给用户)和500状态码。避免在生产环境泄露敏感的异常堆栈信息。
  • 返回格式: 对于面向用户的Web页面,返回错误页面(如404.html, 500.html)。对于RESTful API,返回统一结构的JSON错误响应。

3. Spring MVC 拦截器 (Interceptor)

拦截器允许你在请求到达Controller之前、Controller处理之后、以及整个请求处理完成后执行自定义逻辑。它工作在DispatcherServlet内部,比Servlet Filter更靠近Spring MVC的核心流程,能够访问Handler(Controller方法)和ModelAndView等信息。

概念、生命周期与 Filter 的区别

  • 概念: 拦截器是Spring MVC提供的请求处理拦截机制。
  • 生命周期 : 一个请求经过拦截器链的三个阶段:
    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是认证拦截器的示例。

java 复制代码
package com.yourcompany.bookmanagement.interceptor;

import org.springframework.web.servlet.HandlerInterceptor; // 引入接口
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class AuthInterceptor implements HandlerInterceptor { // 实现 HandlerInterceptor 接口

    // 在 Controller 方法执行前调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        System.out.println("AuthInterceptor: Intercepting request: " + requestURI);

        HttpSession session = request.getSession();
        Object user = session.getAttribute("loggedInUser"); // 检查 Session 中是否有名为 "loggedInUser" 的属性

        if (user != null) {
            // 用户已登录,放行
            System.out.println("AuthInterceptor: User is logged in.");
            return true; // 继续执行后续拦截器或 Controller
        } else {
            // 用户未登录,重定向到登录页面
            System.out.println("AuthInterceptor: User is NOT logged in. Redirecting to login page.");
            // 获取 contextPath,避免硬编码应用名称
            response.sendRedirect(request.getContextPath() + "/login");
            return false; // 阻止当前请求继续处理
        }
    }

    // 在 Controller 方法执行后,视图渲染前调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 可以在这里对 Model 或 View 进行操作
        // System.out.println("AuthInterceptor postHandle...");
    }

    // 在整个请求处理完成后调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 用于资源清理等
        // System.out.println("AuthInterceptor afterCompletion...");
    }
}

拦截器配置 (WebMvcConfig.java):

java 复制代码
package com.yourcompany.bookmanagement.config;

import com.yourcompany.bookmanagement.interceptor.AuthInterceptor; // 引入拦截器类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 引入 WebMvcConfigurer

@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer { // 实现 WebMvcConfigurer

    // 将拦截器注册为 Spring Bean
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    // 重写 addInterceptors 方法配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor()) // 添加拦截器实例
                .addPathPatterns("/**") // 配置需要拦截的路径模式,"/**" 表示拦截所有请求
                .excludePathPatterns("/login", "/logout", "/resources/**", "/webjars/**"); // 配置排除的路径模式,登录、注销、静态资源通常需要排除
    }

    // ... 其他配置,如 ViewResolver, MultipartResolver 等
}

典型应用场景

  • 日志记录 : 在preHandle记录请求信息,在afterCompletion记录处理时间、响应状态等。
  • 权限校验 : 在preHandle检查用户是否已登录、是否有权访问当前资源。未通过则重定向或返回错误。
  • 请求预处理 : 在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:

java 复制代码
package com.yourcompany.bookmanagement.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver; // 引入接口
import org.springframework.web.multipart.support.StandardServletMultipartResolver; // 引入 Standard 实现
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {

    // 配置 StandardServletMultipartResolver
    // Servlet 3.0+ 容器会自动提供 MultipartConfigElement,StandardServletMultipartResolver 基于此工作
    // 文件大小等限制可以在 Servlet 注册时(例如 MyWebAppInitializer)或通过容器配置实现
    @Bean
    public MultipartResolver multipartResolver() {
        return new StandardServletMultipartResolver();
    }

    // ... 其他配置
}

如果在 MyWebAppInitializer.java 中需要配置上传属性 (如文件大小限制),可以重写 customizeRegistration 方法:

java 复制代码
package com.yourcompany.bookmanagement.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.MultipartConfigElement; // 引入
import javax.servlet.ServletRegistration; // 引入

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // ... getRootConfigClasses, getServletConfigClasses, getServletMappings methods

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        // 配置文件上传属性:临时文件存放路径,最大文件大小,最大请求大小,文件阈值大小
        // 这些配置会传递给 StandardServletMultipartResolver
        String fileUploadTempDir = System.getProperty("java.io.tmpdir"); // 使用系统的临时目录
        long maxFileSize = 5 * 1024 * 1024; // 5MB
        long maxRequestSize = 10 * 1024 * 1024; // 10MB
        int fileSizeThreshold = 0; // 所有文件都直接写入临时文件,而不是内存

        MultipartConfigElement multipartConfigElement = new MultipartConfigElement(
                fileUploadTempDir,
                maxFileSize,
                maxRequestSize,
                fileSizeThreshold);

        registration.setMultipartConfig(multipartConfigElement);
    }
}

Controller 处理文件上传:

java 复制代码
package com.yourcompany.bookmanagement.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.IOException;
import java.nio.file.Files; // 使用 NIO.2 进行文件操作
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
@RequestMapping("/files")
public class FileUploadController {

    // 文件保存的根目录 (请根据实际环境修改!)
    // 生产环境不应该硬编码在此处,应从配置读取
    private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!

    @GetMapping("/upload")
    public String showUploadForm() {
        return "uploadForm"; // 返回上传表单视图 (如 uploadForm.html)
    }

    @PostMapping("/upload")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
                                   RedirectAttributes redirectAttributes) {

        // 简单校验:文件是否为空
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
            return "redirect:/files/uploadStatus"; // 重定向到状态页面
        }

        try {
            // 获取文件名
            String fileName = file.getOriginalFilename();
            // 防止路径穿越等安全问题,实际应用中应更严格处理文件名
            Path path = Paths.get(UPLOADED_FOLDER + fileName);

            // 创建目标目录 (如果不存在)
            Files.createDirectories(path.getParent());

            // 将文件保存到目标路径
            Files.copy(file.getInputStream(), path); // 使用 NIO.2 Copy Stream

            redirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + fileName + "'");

        } catch (IOException e) {
            e.printStackTrace();
            redirectAttributes.addFlashAttribute("message", "Failed to upload file: " + e.getMessage());
        }

        return "redirect:/files/uploadStatus"; // 重定向到状态页面显示结果
    }

    @GetMapping("/uploadStatus")
    public String uploadStatus() {
        return "uploadStatus"; // 返回上传状态视图 (如 uploadStatus.html)
    }
}

上传表单视图 (uploadForm.html - Thymeleaf 示例):

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>File Upload</title>
</head>
<body>
    <h1>Upload File</h1>

    <!-- 注意:method 必须是 POST,enctype 必须是 multipart/form-data -->
    <form method="POST" action="/files/upload" enctype="multipart/form-data">
        <div>
            <label for="file">Select File:</label>
            <input type="file" name="file" id="file" required/>
        </div>
        <div>
            <button type="submit">Upload</button>
        </div>
    </form>
</body>
</html>

上传状态视图 (uploadStatus.html - Thymeleaf 示例):

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Upload Status</title>
</head>
<body>
    <h1>Upload Status</h1>
    <!-- 使用 th:text 获取 RedirectAttributes 中的 Flash 属性 -->
    <div th:if="${message}">
        <p th:text="${message}"></p>
    </div>
    <p><a th:href="@{/files/upload}">Upload Another File</a></p>
</body>
</html>

文件下载

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

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

代码示例 (文件下载)

java 复制代码
package com.yourcompany.bookmanagement.controller;

import org.springframework.core.io.InputStreamResource; // 引入资源类型
import org.springframework.core.io.Resource; // 引入资源接口
import org.springframework.http.HttpHeaders; // 引入 HTTP 头部
import org.springframework.http.MediaType; // 引入媒体类型
import org.springframework.http.ResponseEntity; // 引入 ResponseEntity
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest; // 可能需要
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
@RequestMapping("/files")
public class FileDownloadController {

    // 文件存放的根目录 (同上传示例)
    private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!

    // 文件下载方法,返回 ResponseEntity<Resource>
    @GetMapping("/download/{fileName:.+}") // :.+ 匹配文件名,包括点号
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize(); // 构造文件路径
        File file = filePath.toFile();

        // 检查文件是否存在且可读
        if (!file.exists() || !file.canRead()) {
            // 抛出异常或返回 404 响应
            // return new ResponseEntity<>(HttpStatus.NOT_FOUND);
             throw new RuntimeException("File not found or cannot be read: " + fileName); // 示例抛出异常
        }

        try {
            // 确定文件的 Content-Type
            String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
            if (contentType == null) {
                contentType = "application/octet-stream"; // 默认类型
            }

            // 创建 InputStreamResource
            InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

            // 构建响应
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType)) // 设置 Content-Type
                    // 设置 Content-Disposition,inline 表示在线打开,attachment 表示下载
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
                    .contentLength(file.length()) // 设置 Content-Length
                    .body(resource); // 设置响应体
        } catch (IOException ex) {
            // 记录错误日志并返回 500 响应或抛出异常
            ex.printStackTrace();
            // return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
            throw new RuntimeException("Error reading file: " + fileName, ex); // 示例抛出异常
        }
    }
    /*
    // 另一种使用 HttpServletResponse 的方式 (不推荐,因为它绕过了 Spring 的 HttpMessageConverter 等)
    @GetMapping("/download2/{fileName:.+}")
    public void downloadFile2(@PathVariable String fileName, HttpServletResponse response) throws IOException {
         Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();
         File file = filePath.toFile();

         if (!file.exists() || !file.canRead()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
            return;
         }

         String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());
         if (contentType == null) {
             contentType = "application/octet-stream";
         }

         response.setContentType(contentType);
         response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");
         response.setContentLength((int) file.length());

         try (InputStream is = new FileInputStream(file);
              OutputStream os = response.getOutputStream()) {
             byte[] buffer = new byte[1024];
             int len;
             while ((len = is.read(buffer)) != -1) {
                 os.write(buffer, 0, len);
             }
             os.flush();
         } catch (IOException e) {
             // 错误处理
             e.printStackTrace();
             throw e; // 抛出异常让 Spring 的异常处理器处理
         }
    }
    */
}

下载链接示例 (HTML):

html 复制代码
<a th:href="@{/files/download/your_file_name.ext}">Download File</a>

5. 国际化 (i18n) 与本地化 (L10n)

国际化 (i18n) 是使应用程序能够适应不同语言和地区的过程。本地化 (L10n) 是为特定语言和地区定制应用程序的过程,包括翻译文本、调整日期格式等。Spring MVC为国际化提供了强大的支持。

Spring MVC 对国际化的支持

  • LocaleResolver : 用于解析当前用户的区域设置 (Locale)。Spring提供了多种实现:
    • AcceptHeaderLocaleResolver (默认): 根据请求头的Accept-Language确定Locale。
    • SessionLocaleResolver: 将Locale存储在HttpSession中。
    • CookieLocaleResolver: 将Locale存储在Cookie中。
    • FixedLocaleResolver: 固定使用某个Locale。
  • MessageSource : 用于根据Locale加载国际化消息。它从资源文件(如.properties文件)中读取键值对。Spring提供了ResourceBundleMessageSource等实现。

配置资源文件和 Spring Bean

  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:

properties 复制代码
app.title=图书管理系统
book.list.title=图书列表
book.detail.title=图书详情
book.form.add=新增图书
book.form.edit=编辑图书
book.title=标题
book.author=作者
book.isbn=ISBN
book.publicationDate=出版日期
button.save=保存
button.cancel=取消
action.details=详情
action.edit=编辑
action.delete=删除
message.save.success=图书信息保存成功!
message.delete.success=图书删除成功!
error.book.notFound=找不到ID为 {0} 的图书。
validation.NotBlank=字段不能为空
validation.Size=字段长度不符合要求
validation.Pattern=字段格式不正确
validation.PastOrPresent=日期不能晚于今天
login.title=用户登录
login.username=用户名
login.password=密码
login.button=登录
login.error=用户名或密码不正确。
logout.success=您已成功注销。

messages_en.properties:

properties 复制代码
app.title=Book Management System
book.list.title=Book List
book.detail.title=Book Detail
book.form.add=Add Book
book.form.edit=Edit Book
book.title=Title
book.author=Author
book.isbn=ISBN
book.publicationDate=Publication Date
button.save=Save
button.cancel=Cancel
action.details=Details
action.edit=Edit
action.delete=Delete
message.save.success=Book information saved successfully!
message.delete.success=Book deleted successfully!
error.book.notFound=Book not found with ID: {0}.
validation.NotBlank=Field must not be blank
validation.Size=Field size constraints violated
validation.Pattern=Field format is incorrect
validation.PastOrPresent=Date cannot be in the future
login.title=User Login
login.username=Username
login.password=Password
login.button=Login
login.error=Invalid username or password.
logout.success=You have been logged out successfully.

WebMvcConfig.java 配置 MessageSourceLocaleResolver:

java 复制代码
package com.yourcompany.bookmanagement.config;

import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource; // 引入实现类
import org.springframework.web.servlet.LocaleResolver; // 引入 LocaleResolver
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver; // 引入 CookieLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; // 引入 LocaleChangeInterceptor

import java.util.Locale; // 引入 Locale

@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {

    // ... MultipartResolver, AuthInterceptor 等其他 Bean

    // 配置 MessageSource Bean
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        // 设置资源文件的 basename (不带语言和后缀)
        messageSource.setBasename("messages"); // 对应 src/main/resources/messages.properties, messages_en.properties 等
        messageSource.setDefaultEncoding("UTF-8"); // 设置编码
        messageSource.setUseCodeAsDefaultMessage(true); // 如果找不到对应的 code,使用 code 本身作为消息
        return messageSource;
    }

    // 配置 LocaleResolver Bean
    // 使用 CookieLocaleResolver 将用户选择的语言存储在 Cookie 中
    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.CHINA); // 设置默认区域为中国
        localeResolver.setCookieName("mylocale"); // 设置存储 Locale 的 Cookie 名称
        localeResolver.setCookieMaxAge(3600); // Cookie 有效期 (秒)
        return localeResolver;
    }

    // 配置 LocaleChangeInterceptor,用于通过参数切换语言
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        // 设置参数名,例如通过访问 /books?lang=en 或 /books?lang=zh_CN 切换语言
        interceptor.setParamName("lang");
        return interceptor;
    }

    // 将 LocaleChangeInterceptor 注册到拦截器链中
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // ... 注册 AuthInterceptor
        registry.addInterceptor(localeChangeInterceptor()); // 注册语言切换拦截器
    }

    // ... 其他 WebMvcConfigurer 方法
}

在视图和后端使用本地化消息

  • 在视图中 (Thymeleaf) : 使用#messages内置对象和#{...}语法获取消息。

    html 复制代码
    <h1 th:text="#{book.list.title}">图书列表</h1>
    <p th:text="#{message.save.success}"></p>
    <!-- 获取带参数的消息,例如 error.book.notFound=找不到ID为 {0} 的图书。 -->
    <p th:text="#{error.book.notFound(${bookId})}"></p>
    <!-- 生成带语言切换参数的 URL -->
    <a th:href="@{/books(lang='en')}">English</a> | <a th:href="@{/books(lang='zh_CN')}">中文</a>
  • 在视图中 (JSP) : 需要配置Spring标签库,并使用<spring:message>标签。

    jsp 复制代码
    <%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
    <spring:message code="book.list.title"/>
    <spring:message code="error.book.notFound" arguments="${bookId}"/>
    <a href="<spring:url value='/books'><spring:param name='lang' value='en'/></spring:url>">English</a>
  • 在后端代码 (Controller/Service) 中 : 通过注入MessageSource,使用getMessage()方法获取消息。

java 复制代码
package com.yourcompany.bookmanagement.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.i18n.LocaleContextHolder; // 引入 LocaleContextHolder
import org.springframework.stereotype.Service;

import java.util.Locale; // 引入 Locale

@Service
public class MyService {

    private final MessageSource messageSource;

    @Autowired
    public MyService(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    public String getLocalizedGreeting(String name) {
        // 获取当前线程绑定的 Locale (由 LocaleResolver 决定)
        Locale currentLocale = LocaleContextHolder.getLocale();
        // 从 MessageSource 获取消息
        String greeting = messageSource.getMessage("greeting", new Object[]{name}, currentLocale);
        return greeting;
    }

    // 在异常处理中获取本地化消息
    // GlobalExceptionHandler.java (示例)
    // @ExceptionHandler(BookNotFoundException.class)
    // public ResponseEntity<ErrorResponse> handleBookNotFoundForRest(BookNotFoundException ex) {
    //     Locale currentLocale = LocaleContextHolder.getLocale();
    //     String errorMessage = messageSource.getMessage("error.book.notFound", new Object[]{ex.getBookId()}, currentLocale);
    //     return new ResponseEntity<>(new ErrorResponse(HttpStatus.NOT_FOUND.value(), errorMessage, System.currentTimeMillis()), HttpStatus.NOT_FOUND);
    // }
}

6. 与前端框架集成考量

当Spring MVC作为纯后端API服务(使用@RestController),与前端框架(Vue.js, React, Angular等)集成时,主要需要考虑数据交互格式、接口设计和跨域问题。

集成模式

  • 前后端分离: 后端Spring MVC提供RESTful API,前端框架负责整个UI渲染和用户交互。两者通过HTTP请求进行通信。这是目前主流的企业级Web应用开发模式。
  • 后端渲染+前端增强: 后端Spring MVC使用Thymeleaf等模板引擎渲染基础HTML页面,前端框架用于局部增强页面交互(如通过Ajax请求更新部分内容)。图书管理系统案例属于此模式。

CORS (跨域资源共享)

跨域请求指浏览器发起的,目标URL与当前页面URL的协议、域名、端口中任意一个不同的请求。出于安全考虑,浏览器会阻止非同源的HTTP请求,除非服务器明确允许。RESTful API通常部署在与前端不同的域名或端口上,因此需要处理CORS问题。

Spring MVC提供了多种方式处理CORS:

  • @CrossOrigin 注解 : 应用在Controller类或方法上,允许来自指定来源的跨域请求。

    java 复制代码
    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规则。

java 复制代码
package com.yourcompany.bookmanagement.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; // 引入 CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {

    // ... 其他 WebMvcConfigurer 方法

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 配置需要允许跨域的路径模式,例如所有 /api 下的请求
                .allowedOrigins("http://localhost:3000", "http://your-frontend-domain.com") // 允许的来源域名
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法
                .allowedHeaders("*") // 允许的请求头
                .allowCredentials(true) // 是否发送 Cookie 或认证信息
                .maxAge(3600); // 预检请求 (Preflight Request) 的缓存时间 (秒)
    }
}

API 接口设计对前端友好的考量

  • 一致的数据格式: 前端更容易处理一致的JSON结构。保持字段命名、日期格式等规范。
  • 清晰的错误处理: RESTful API应返回明确的状态码,并在响应体中包含统一结构的错误信息,便于前端解析和展示错误。
  • 合理的数据结构: 根据前端页面或组件需要的数据结构来设计API响应,避免过度嵌套或返回冗余字段。
  • 分页与过滤: 对于列表数据,提供分页、排序、过滤等查询参数,让前端能够灵活地获取所需数据。
  • 文档: 提供清晰的API文档(如Swagger/OpenAPI),方便前端开发者理解和使用接口。

7. Spring Security 入门

Spring Security是一个强大且高度可定制的认证和授权框架。它可以轻松地为Spring应用程序提供安全性。

Spring Security 核心概念

  • Authentication (认证): 验证用户身份,证明"你是谁"。通常通过用户名/密码、证书等方式。
  • Authorization (授权): 在身份认证后,确定用户是否有权访问某个资源或执行某个操作。
  • Principal: 当前认证用户的代表,通常包含用户名、密码、权限等信息。
  • GrantedAuthority: 授予Principal的权限或角色(如ROLE_USER, read_permission)。
  • AuthenticationManager: 负责处理认证请求。
  • AccessDecisionManager: 负责处理授权决策。
  • SecurityContextHolder: 存储当前应用程序中Principal详细信息的容器。默认使用ThreadLocal策略,确保每个线程独立。
  • Filter Chain (过滤器链) : Spring Security通过一系列Servlet Filter来实现各种安全功能(如认证、授权、CSRF防护)。这些Filter被组织成一个链。DelegatingFilterProxy是Spring Security的核心Filter,它将Servlet容器的请求委托给Spring Bean中的Security Filter Chain。

基础配置 (JavaConfig)

Spring Security通常通过JavaConfig进行配置。核心是创建一个继承自WebSecurityConfigurerAdapter的配置类(或者使用新的SecurityFilterChain Bean方式,取决于Spring Security版本)。

java 复制代码
package com.yourcompany.bookmanagement.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; // 引入 HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; // 启用 Spring Security
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; // 引入适配器类
import org.springframework.security.core.userdetails.User; // 引入 UserDetails
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; // 引入 UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 引入密码编码器
import org.springframework.security.crypto.password.PasswordEncoder; // 引入接口
import org.springframework.security.provisioning.InMemoryUserDetailsManager; // 引入内存用户存储

// 启用 Spring Security 的 Web 安全功能
@EnableWebSecurity
@Configuration // 标记为配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter { // 继承 WebSecurityConfigurerAdapter (Spring Security 5.7+ 推荐使用 SecurityFilterChain Bean)

    // 配置密码编码器 Bean
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用 BCrypt 强哈希算法
    }

    // 配置用户详情服务 (AuthenticationManager 的一部分)
    // 这里使用内存存储用户,实际应用中通常从数据库加载 (实现 UserDetailsService 接口)
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        // 创建一个内存用户
        UserDetails user = User.builder()
                .username("admin")
                .password(passwordEncoder().encode("password")) // 密码必须编码
                .roles("USER", "ADMIN") // 设置角色
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    // 配置 HTTP 请求的安全性规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 授权配置
            .authorizeRequests()
                // /login, /resources/**, /webjars/** 路径无需认证即可访问
                .antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
                // /books/** 路径需要认证且具有 USER 或 ADMIN 角色才能访问
                .antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
                // /admin/** 路径需要认证且具有 ADMIN 角色才能访问
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 所有其他路径需要认证才能访问
                .anyRequest().authenticated()
            .and()
            // 表单登录配置
            .formLogin()
                .loginPage("/login") // 指定自定义的登录页面 URL
                .permitAll() // 登录页面允许所有用户访问
            .and()
            // 注销配置
            .logout()
                .logoutUrl("/logout") // 指定注销 URL (POST 请求)
                .logoutSuccessUrl("/login?logout") // 注销成功后重定向的 URL
                .permitAll(); // 注销 URL 允许所有用户访问

        // 禁用 CSRF 防护 (仅为简化示例,生产环境应启用并处理)
        // http.csrf().disable();
    }

    // Spring Security 5.7+ 推荐的配置方式 (使用 SecurityFilterChain Bean 替代 WebSecurityConfigurerAdapter)
    /*
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
            // 授权配置
            .authorizeRequests()
                .antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()
                .antMatchers("/books/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            // 表单登录配置
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            // 注销配置
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll();
        return http.build();
    }
     */
}

说明:

  • @EnableWebSecurity注解会加载Spring Security的核心配置类。
  • WebSecurityConfigurerAdapter提供了一个方便的基类来定制安全性配置。
  • configure(HttpSecurity http)方法是配置URL授权规则、表单登录、注销等的核心。
  • userDetailsService()方法配置如何加载用户信息。InMemoryUserDetailsManager用于测试,实际应用需要实现UserDetailsService从数据库等加载。
  • passwordEncoder()配置密码加密器,Spring Security强制要求使用加密后的密码。BCryptPasswordEncoder是推荐的选择。

密码加密

使用PasswordEncoder接口对用户密码进行加密存储和比对。BCryptPasswordEncoder使用BCrypt算法,该算法包含了随机盐值和多次哈希迭代,安全性较高。

  • 存储密码时:String encodedPassword = passwordEncoder.encode(rawPassword);
  • 校验密码时:boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);

方法级别安全注解

除了保护URL路径,Spring Security还支持通过注解保护Controller或Service方法。

  1. 启用方法安全 : 在任何一个@Configuration类上添加@EnableGlobalMethodSecurity注解。

    • prePostEnabled = true: 启用@PreAuthorize@PostAuthorize注解。
    • securedEnabled = true: 启用@Secured注解。
    java 复制代码
    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角色都能访问。

代码示例 (方法安全)

java 复制代码
package com.yourcompany.bookmanagement.controller;

import org.springframework.security.access.annotation.Secured; // 引入 @Secured
import org.springframework.security.access.prepost.PreAuthorize; // 引入 @PreAuthorize
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/secure")
public class SecureController {

    // 需要 ADMIN 角色才能访问
    @GetMapping("/admin")
    @Secured("ROLE_ADMIN") // 或 @PreAuthorize("hasRole('ADMIN')")
    public String adminOnly() {
        return "This content is for ADMINs only!";
    }

    // 需要 USER 或 ADMIN 角色才能访问
    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public String userOrAdmin() {
        return "This content is for logged-in users (USER or ADMIN)!";
    }

    // 示例:基于输入参数的权限检查
    // 只有当请求的 username 与当前认证用户的 username 相同时才能访问
    @GetMapping("/profile/{username}")
    @PreAuthorize("#username == authentication.principal.username")
    public String getUserProfile(@PathVariable String username) {
        return "Viewing profile for user: " + username;
    }
}

保护URL路径访问

通过在HttpSecurity配置中使用antMatchers(), regexMatchers(), anyRequest()等匹配器结合permitAll(), authenticated(), hasRole(), hasAuthority()等方法来定义哪些URL需要哪些权限。这是最常用的保护方式。示例已包含在基础配置代码中。

运行与部署

参考图书管理系统案例中的Maven构建WAR包和部署到Servlet容器步骤。Spring Security作为Filter Chain会自动集成到请求处理流程中。

总结

通过学习这些高级主题,你将能够构建更加健壮、安全、易于维护和集成的企业级Spring MVC应用程序。RESTful API是前后端分离的基石;统一异常处理提升用户体验和开发效率;拦截器提供灵活的请求处理增强;文件处理是常见功能;国际化使应用适应全球用户;Spring Security提供全面的安全保障。

持续学习建议:

  • Spring Security 深入: 用户详情服务 (UserDetailsService)、自定义认证提供者、OAuth2、JWT等。
  • RESTful API 进阶: API文档(Swagger/OpenAPI)、API网关、更复杂的HATEOAS实现。
  • 缓存: 使用Spring Cache提升数据访问性能。
  • 消息队列: 集成RabbitMQ, Kafka等处理异步任务和解耦。
  • 分布式系统: 了解Spring Cloud体系。
  • 最重要:转向 Spring Boot! 将这些学到的原生Spring MVC知识应用到Spring Boot环境中,你会发现开发效率的巨大提升。Spring Boot基于约定大于配置的理念,自动集成了大量常用功能(包括上述大部分高级特性),让你更专注于业务逻辑。

希望这篇文章能帮助你更好地迈向Spring MVC高级开发!

相关推荐
zeijiershuai4 分钟前
SpringBoot Controller接收参数方式, @RequestMapping
java·spring boot·后端
zybsjn15 分钟前
后端项目中静态文案国际化语言包构建选型
java·后端·c#
L2ncE24 分钟前
ES101系列07 | 分布式系统和分页
java·后端·elasticsearch
枣伊吕波41 分钟前
第十二节:第三部分:集合框架:List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理
java·jvm·list
贺函不是涵1 小时前
【沉浸式求职学习day51】【发送邮件】【javaweb结尾】
java·学习
无限大61 小时前
《计算机“十万个为什么”》之前端与后端
前端·后端·程序员
初次见面我叫泰隆1 小时前
Golang——2、基本数据类型和运算符
开发语言·后端·golang
南风lof1 小时前
ReentrantLock与AbstractQueuedSynchronizer源码解析,一文读懂底层原理
后端
你不是我我2 小时前
【Java开发日记】基于 Spring Cloud 的微服务架构分析
java·开发语言
写bug写bug2 小时前
彻底搞懂 RSocket 协议
java·后端