Spring MVC教程

MVC

  • Spring MVC用于实现Servlet标准的Web接口

    1. MVC通过封装原生Servlet对HTTP请求的API,极大地简化了控制器层的开发
    2. 每一个HTTP请求由Servlet 容器的线程池为其分配一个线程,请求结束后线程自动回收
  • SpringBoot引入web启动器即可

xml 复制代码
<!--web启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

HTTP协议传输

  • 短连接与长连接

    1. 短连接之间互相独立,HTTP请求是典型的长连接协议
    2. 长连接之间可以互相感知,WebSocket请求是典型的双向实时通信的长连接请求
    3. 短连接请求适用于【请求-响应】交互场景,长连接请求适用于【维持通信】的场景
  • HTTP请求和HTTPS请求

    1. HTTP 是超文本明文传输,HTTPS在【TPC/IP传输层】 和 【HTTP 应用层】之间加入了 SSL/TLS 安全协议,实现加密传输

    2. HTTP 的端口号是 80,HTTPS 的端口号是 443

    3. HTTPS 协议需要向 CA 申请数字证书,来保证服务器的身份是可信的

请求结构

  • 请求结构:标准的HTTP请求包含了三部分,请求行、请求头、请求体

  • 请求行结构:<Method(请求方法)> <Request-URI(请求资源)> <HTTP-Version(请求协议)>

    1. Method:设置请求方法,常见有GETPUTDELETEPOST
    2. Request-URI:请求资源路径,查询参数用?/连接
    3. HTTP-Version:协议版本
  • 请求头结构:以键值对形式存储数据,用于传递与请求或客户端相关的额外信息

    请求头key 说明 举例
    User-Agent 标识客户端类型 Mozilla/5.0 (Windows NT 10.0; Win64; x64)
    Authorization 传递认证凭证 Bearer <token>Basic <base64(username:password)>
    Cookie 用户相关参数 {sessionid=abc123, user_token=xyz456}
    Content-Type 请求体的数据类型 application/jsonmultipart/form-data
    Content-Length 请求体长度 1024
    Accept 接收的响应类型 text/htmlapplication/jsonimage/png
    X- 自定义请求头约定X-开头 X-token=122345665
  • 请求体:由请求头的属性Content-Type指定,一般是json,其他常见的有文件类型等

apl 复制代码
POST adduser/1 HTTP/1.1             # 请求行
Content-Type: application/json	    # 请求头
{
    "name": "Alice",
    "age": 25,                       # 请求体
    "email": "alice@example.com"
}

响应结构

  • 一个标准的响应包括三部分:响应状态码、响应头、响应体

  • 响应行:协议版本 + 响应状态码

  • 响应头:以键值对形式存储数据

    响应头key 说明 举例
    Content-Type 响应体的数据类型 application/jsonapplication/pdfimage/png
    Content-Length 响应体的数据长度 1024
    Content-Disposition 浏览器处理响应体方式 attachment; filename="example.pdf"(不直接显式下载的文件)
    Location 重定向目标 URL Location: https://example.com/new-path
    Header 自定义响应头 X-Version:1.0
  • 响应体:数据类型和字符集由响应头Content-Type指定,一般响应体中的数据类型是json,文件类型见下方文件传输

apl 复制代码
HTTP/1.1 200 OK  # 状态码
Content-Type: application/json # 响应头
{
    "name": "Alice",
    "age": 25,                       # 响应体
    "email": "alice@example.com"
}

MVC映射

  • 注解匹配请求

    1. 请求方法:@getMapping@postMapping@putMapping@deleteMapping
    2. 请求参数:@RequestParam(入参可为空),@PathVariable(入参不为空)、 @DateTimeFormat(pattern = "yyyy-MM-dd")
    3. 请求头:@RequestHeader@CookieValue
    4. 请求体:@requestBody会将请求体反Json序列化为指定实例
  • HttpServletRequest API

    1. 请求头:getHeadergetCookies
    2. 请求体:getReader()
  • 注解匹配响应

    1. 响应码:@ResponseStatus
    2. 响应体:@ResponseBody会将方法返回值自动序列化为Json字符串写入响应体
  • HttpServletRequest API

    1. 响应码:setStatus()
    2. 响应头:setHeader()setContentType()setCharacterEncoding()
    3. 响应体:getWriter().write()ImageIO.write()
  • ResponseEntity

    1. 响应码:.status()
    2. 响应头:.header().contentType()
    3. 响应体:.body()
  • 最佳实践

    1. 请求方法:注解映射
    2. 请求路径参数:注解映射
    3. 请求体:注解映射
    4. 请求头:【HttpServletRequest API】映射
    5. 响应头:【HttpServletRequest API】形式映射
    6. 响应体:注解形式映射
    7. 简单场景推荐使用ResponseEntity
java 复制代码
@RestController // @ResponseBody响应体自动转化为JSON字符串
@RequestMapping("user")
public class UserController {
    
    @Autowired
    private UserService userService;

    /*
    GET /user/getInfo?id=1 HTTP/1.1
    User-Agent:Mozilla/5.0
    Cookies: [Uid=12d32,Uname=wyh]
    {
    "name": "Alice",
    "age": 25,
    "email": "alice@example.com"
    }
    */ 
    @GetMapping("/getInfo") 
    public ResultJson<User> getInfo( // 使用注解形式
        @RequestParam(value = "id", required = false) Long userId, // @RequestParam匹配请求?连接参数,id可以不传参
        @CookieValue("Uname") String name, // @CookieValue 注解匹配cookie字段
        @RequestBody User user) // @RequestBody 注解匹配请求体
    { 
        System.out.println(name); //wyh
        System.out.println(user.getName()); //Alice
        return new ResultJson<User>(200,new User());
    } 
    
    /*
    GET /user/getUser/{id} HTTP/1.1
    User-Agent:Mozilla/5.0
    Cookies: [Uid=12d32,Uname=wyh]
    */
    @GetMapping("/getUser") 
    public void getUser(HttpServletRequest request, HttpServletResponse response) { // 使用原生servlet API,返回void
        Cookie[] cookies = request.getCookies(); // 获取cookies
        Integer id;
        for (Cookie cookie : cookies) {
            if ("id".equals(cookie.getName())) { // 从cookies中获取id
                id = Long.parseLong(cookie.getValue());
                break;
            }
        }
        response.setContentType(MediaType.APPLICATION_JSON_VALUE); // MediaType新版不再包含字符集声明     
        response.setCharacterEncoding(StandardCharsets.UTF_8.name()); // 单独设置编码
        if(id == null){
            response.getWriter().write(JSON.toJSONString(Result.fail("无效的 id 格式"))); //写入响应体
        }
        User user = userService.getUserById(id);
        response.setHeader("X-Version", "1.0"); //写入自定义响应头
        response.setStatus(HttpServletResponse.SC_OK); //写入响应码
        response.getWriter().write(JSON.toJSONString(Result.ok(user))); //写入响应体
        // 不需要手动关闭IO流,因为 Servlet 容器会在响应结束后自动关闭底层流
    }

    
    /*
    GET /user/getUser2/{id} HTTP/1.1
    User-Agent:Mozilla/5.0
    */    
    @GetMapping("/getUser2/{id}")
    public ResponseEntity<User> getUser2(@PathVariable("id") Long id) { // ResponseEntity泛型控制响应体的类型
        ResponseEntity<User> body = ResponseEntity.status(200)
                .header("X-Request-ID", UUID.randomUUID().toString())
                .header("X-Version", "1.0")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(new ResultJson(200,new User())); // 响应体会自动序列化为JSON字符串
        return body; 
    }
    
    /*
    PUT /user/update/666 HTTP/1.1
	Content-Type: application/json
    {
        "name": "Alice",
        "age": 25,
        "email": "alice@example.com"
    }
    */
    @PutMapping("/updateuser/{id}")  //请求体JSON数据,也可以映射为Map
    public void updateUser(@PathVariable("id") Integer userId, @RequestBody Map<String, Object> map){
        System.out.println(map.get("name")); //Alice,Json映射为Map
    }
}

统一返回体

  • 描述:无论是成功还是失败,后端给前端返回的数据结构是相同的

    1. 强制所有接口返回统一格式,避免因开发者习惯不同导致响应结构混乱,前端也便于统一处理响应
    2. 业务逻辑上的异常返回200,服务器异常返回5xx/4xx
java 复制代码
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result<T> {
    private Boolean status; // 状态标识,快速判断是否正常响应,如果异常再查自定义响应码和描述信息
    private String code; // 自定义内部状态码
    private String msg; // 描述信息
    private T data; // 响应体数据

    public static <T> Result<T> ok(){
        return new Result<>(true, "200", null, null);
    }
    public static <T> Result<T> ok(T data){
        return new Result<>(true, "200", null, data);
    }
    public static <T> Result<T> fail(String msg, String code){
        return new Result<>(false, code, msg, null);
    }
}
  • 请求体Json映射VO使用Jacskon反序列化,默认相同名称映射,也可以自定义映射规则
java 复制代码
@Data
@ToString
@AllArgsConstructor
public class User{

    @JsonProperty("id") // 映射Json数据的"id"键
    private Integer userId;

    @JsonProperty("name") // 映射Json数据的"name"键
    private String userName;

    @JsonProperty("email") // 映射Json数据的"email"键
	private String userEmail;

    @JsonProperty("time") // 映射Json数据的"time"键
    @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh") // 指定映射规则
    private Date time; // 时间类型还是建议以Long类型传递
}

封装HTTP上下文

  • 将业务涉及HTTP数据封装为【Http上下文】,常见于微服务架构,用于对所有子微服务统一管理
java 复制代码
/**
 * 精简版HttpContext - 核心功能封装
 */
public interface HttpContext {
    
    // ========== 请求相关 ==========
    /** 获取原始请求 */
    HttpServletRequest getRequest();
    
    /** 获取原始响应 */
    HttpServletResponse getResponse();
    
    /** 获取请求方法 */
    String getMethod();
    
    /** 获取请求路径 */
    String getPath();
    
    /** 获取客户端IP */
    String getClientIp();
    
    /** 获取请求头 */
    String getHeader(String name);
    
    /** 获取所有请求头 */
    Map<String, String> getHeaders();
    
    /** 获取Query参数 */
    String getQueryParam(String name);
    
    /** 获取所有Query参数 */
    Map<String, String> getQueryParams();
    
    /** 获取请求体(字符串) */
    String getRequestBody();
    
    /** 获取请求体并解析为对象 */
    <T> T getRequestBody(Class<T> clazz);
    
    /** 获取上传文件 */
    <T> T getFile(String name, Class<T> fileType);
    
    // ========== 响应相关 ==========
    /** 设置状态码 */
    void setStatus(int statusCode);
    
    /** 设置响应头 */
    void setHeader(String name, String value);
    
    /** 设置Content-Type */
    void setContentType(String contentType);
    
    /** 写入响应体 */
    void write(String content);
    
    /** 写入JSON响应 */
    void writeJson(Object data);
    
    /** 写入成功响应 */
    void writeSuccess(Object data);
    
    /** 写入错误响应 */
    void writeError(int code, String message);
    
    /** 重定向 */
    void redirect(String url);
    
    /** 是否已提交响应 */
    boolean isCommitted();
    
    // ========== 上下文属性 ==========
    /** 设置属性 */
    void setAttribute(String key, Object value);
    
    /** 获取属性 */
    <T> T getAttribute(String key);
    
    /** 移除属性 */
    void removeAttribute(String key);
}
java 复制代码
/**
 * 标准HttpContext实现
 */
public class HttpContext implements HttpContext {
    
    private final HttpServletRequest request;
    private final HttpServletResponse response;
    private final Map<String, Object> attributes = new ConcurrentHashMap<>();
    
    // 缓存数据
    private String requestBody;
    private Map<String, String> queryParams;
    private Map<String, String> headers;
    
    // 响应状态
    private boolean committed = false;
    private String responseBody;
    private int status = 200;
    
    // 元数据
    private final String requestId;
    private final long startTime;
    
    public HttpContext(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;
    }

    // ========== 请求相关 ==========
    @Override
    public HttpServletRequest getRequest() {
        return request;
    }
    
    @Override
    public HttpServletResponse getResponse() {
        return response;
    }
    
    @Override
    public String getMethod() {
        return request.getMethod();
    }
    
    @Override
    public String getPath() {
        return request.getRequestURI();
    }
    
    @Override
    public String getClientIp() {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
    
    @Override
    public String getHeader(String name) {
        return request.getHeader(name);
    }
    
    @Override
    public Map<String, String> getHeaders() {
        if (headers == null) {
            headers = new LinkedHashMap<>();
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                headers.put(name, request.getHeader(name));
            }
        }
        return headers;
    }
    
    @Override
    public String getQueryParam(String name) {
        return request.getParameter(name);
    }
    
    @Override
    public Map<String, String> getQueryParams() {
        if (queryParams == null) {
            queryParams = new LinkedHashMap<>();
            Map<String, String[]> params = request.getParameterMap();
            for (Map.Entry<String, String[]> entry : params.entrySet()) {
                String[] values = entry.getValue();
                if (values != null && values.length > 0) {
                    queryParams.put(entry.getKey(), values[0]);
                }
            }
        }
        return queryParams;
    }
    
    @Override
    public String getRequestBody() {
        if (requestBody == null) {
            StringBuilder body = new StringBuilder();
            try (BufferedReader reader = request.getReader()) {
                String line;
                while ((line = reader.readLine()) != null) {
                    body.append(line);
                }
                requestBody = body.toString();
            } catch (IOException e) {
                throw new RuntimeException("Failed to read request body", e);
            }
        }
        return requestBody;
    }
    
    @Override
    public <T> T getRequestBody(Class<T> clazz) {
        String body = getRequestBody();
        if (body == null || body.isEmpty()) {
            return null;
        }
        try {
            // 伪代码,实际应使用JSON库
            return parseJson(body, clazz);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse request body", e);
        }
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getFile(String name, Class<T> fileType) {
        // 简化处理,实际应根据fileType返回不同类型的文件对象
        if (request instanceof org.springframework.web.multipart.MultipartHttpServletRequest) {
            org.springframework.web.multipart.MultipartHttpServletRequest multipartRequest = 
                (org.springframework.web.multipart.MultipartHttpServletRequest) request;
            return (T) multipartRequest.getFile(name);
        }
        return null;
    }
    
    // ========== 响应相关 ==========
    @Override
    public void setStatus(int statusCode) {
        this.status = statusCode;
        response.setStatus(statusCode);
    }
    
    @Override
    public void setHeader(String name, String value) {
        response.setHeader(name, value);
    }
    
    @Override
    public void setContentType(String contentType) {
        response.setContentType(contentType);
    }
    
    @Override
    public void write(String content) {
        if (committed) {
            throw new IllegalStateException("Response already committed");
        }
        
        try {
            response.setStatus(status);
            PrintWriter writer = response.getWriter();
            writer.write(content);
            writer.flush();
            
            this.responseBody = content;
            this.committed = true;
        } catch (IOException e) {
            throw new RuntimeException("Failed to write response", e);
        }
    }
    
    @Override
    public void writeJson(Object data) {
        setContentType("application/json;charset=UTF-8");
        String json = toJson(data);
        write(json);
    }
    
    @Override
    public void writeSuccess(Object data) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("code", 200);
        result.put("data", data);
        result.put("timestamp", System.currentTimeMillis());
        writeJson(result);
    }
    
    @Override
    public void writeError(int code, String message) {
        setStatus(code);
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("code", code);
        result.put("message", message);
        result.put("timestamp", System.currentTimeMillis());
        writeJson(result);
    }
    
    @Override
    public void redirect(String url) {
        if (committed) {
            throw new IllegalStateException("Response already committed");
        }
        try {
            response.sendRedirect(url);
            this.committed = true;
        } catch (IOException e) {
            throw new RuntimeException("Failed to redirect", e);
        }
    }
    
    @Override
    public boolean isCommitted() {
        return committed;
    }
    
    // ========== 上下文属性 ==========
    @Override
    public void setAttribute(String key, Object value) {
        attributes.put(key, value);
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getAttribute(String key) {
        return (T) attributes.get(key);
    }
    
    @Override
    public void removeAttribute(String key) {
        attributes.remove(key);
    }
}

文件传输

  • 描述:请求体是文件数据【文件上传】,响应体是文件数据【文件下载】
yaml 复制代码
# SpringBoot默认已配置MultipartResolver的Bean,可以使用统一配置文件或者配置类修改属性
spring:
  servlet:
    multipart:	
      max-file-size: 10MB
      max-request-size: 50MB
      file-size-threshold: 1MB
      enabled: true
      location: /tmp/uploads
java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver resolver = new CommonsMultipartResolver();
        resolver.setMaxUploadSize(10485760); // 10MB
        resolver.setDefaultEncoding("UTF-8");
        return resolver;
    }
}

文件上传

  • 前端一般通过表单传输文件,一个键代表一种类型的文件,可以是一个,也可以是多个文件
  • 后端mvc接收:使用MultipartFile类接收,使用@RequestParam指定文件(组)
java 复制代码
@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("upload-file")
    public void insertFiles(
            @RequestParam("files") MultipartFile[] files,
            @RequestParam("file") MultipartFile file
    ) {
        // 1. 检查并创建上传目录
        if (!new File("upload").mkdir()) {
            throw new RuntimeException("mkdir failed");
        }
        // 2. 处理单个文件(files-2)
        if (!file.isEmpty()) {
            saveFile(file, "upload");
        }
        // 3. 处理多个文件(files)
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                saveFile(file, "upload");
            }
        }
    }

    /**
     * 保存文件到指定目录(避免代码重复)
     * @param file MultipartFile 对象
     * @param uploadDir 目标目录
     */
    private void saveFile(MultipartFile file, String uploadDir) {
        try {
            // 1. 获取文件名
            String originalFilename = file.getOriginalFilename(); // 可能返回 "C:\Users\test.jpg"
            // 2. 防止路径遍历攻击
            String safeFilename = Paths.get(originalFilename).getFileName().toString(); //去除路径,即"test.jpg"
            Path targetPath = Paths.get(uploadDir, safeFilename); //组成文件路径
            // 3. 使用 NIO 方式保存文件(更高效)
            Files.copy(
                file.getInputStream(),
                targetPath,
                StandardCopyOption.REPLACE_EXISTING // 如果文件已存在则覆盖
            );
        } catch (IOException e) {
            throw new RuntimeException("文件保存失败: " + file.getOriginalFilename(), e);
        }
    }
}

文件下载

java 复制代码
@RestController
@RequestMapping("user")
public class UserController {
  
  @GetMapping("{id}/show-file") //原生HttpServletResponse构造响应
  public void showFile(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
      // 返回的文件
      File file = new File("upload/JavaSE.pdf");
      // 设置响应头
      response.setContentType("application/pdf");
      // 客户端默认直接显示文件需要手动下载,可以设置为直接下载文件
      response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
      response.setContentLength((int) file.length());
      // 读取文件并写入响应输出流
      try (InputStream inputStream = new FileInputStream(file);
           OutputStream outputStream = response.getOutputStream()) {
          byte[] buffer = new byte[4096];
          while (true) {
              int length = inputStream.read(buffer);
              if (length == -1) {
                  break;
              }
              outputStream.write(buffer, 0, length);
              outputStream.flush();
          }
      }
  }

  @GetMapping("{id}/download-file") //ResponseEntity构造响应,普通文件建议使用Resource泛型
  public ResponseEntity<Resource> downLoadFile(@PathVariable("id") Long id) throws IOException {        
      Resource resource = new UrlResource("upload/JavaSE.pdf");
      return ResponseEntity.ok()
          .contentType(MediaType.APPLICATION_OCTET_STREAM) // 通用二进制流类型
          //设置为直接下载文件
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
          .body(resource);
  }

  @GetMapping("{id}/download-bigfile") //ResponseEntity构造响应,大文件使用fileInputStream泛型,防止内存溢出
  public ResponseEntity<InputStreamResource> downLoadBigFile(@PathVariable("id") Long id) throws IOException {
      // 返回的文件
      File file = new File("upload/JavaSE.pdf");
      FileInputStream fileInputStream = new FileInputStream(file);
      // 设置内容类型和响应头
      return ResponseEntity.ok()
          .contentType(MediaType.APPLICATION_PDF)
          //设置为直接下载文件
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
          .body(new InputStreamResource(fileInputStream));
  }
}

Web配置类

  • 目的:如果要进一步设置Spring MVC高级功能,可以通过实现WebMvcConfigurer接口来创建MVC配置类

  • 使用方法

    1. 实现组件接口,编写逻辑内容
    2. WebMvcConfigurer配置类中的重写方法中引用组件接口
  • 常用组件方法

    1. addInterceptors拦截器
    2. addCorsMappings跨域设置
    3. configureContentNegotiation:内容协商设置
    4. addResourceHandlers:静态资源处理器
    5. resourceViewResolver:视图解析器(前后端分离项目中已不再使用)
    6. configureMessageConverters/extendMessageConverters:信息转换器,处理HTTP请求/响应体与Java对象之间的转换
java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) { //静态资源处理器,直接返回本地资源
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/public/") // 本地资源
                .setCachePeriod(3600); // 缓存时间(秒)
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) { //跳转指定页面
        registry.addViewController("/home").setViewName("home"); // 访问/home跳转到home.html
        registry.addRedirectViewController("/old", "/new");     // 重定向
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { //信息转换器
        converters.add(new FastJsonHttpMessageConverter()); // 使用FastJson转换
    }
}

拦截器

  • 目的:在控制器接收请求前,拦截请求,先进行指定逻辑语句

  • 替代方案

    1. 过滤器:Servlet容器规范组件,不依赖于Spring框架
    2. AOP:业务层级别的接口边界控制
java 复制代码
//拦截器
public class Interceptor01 implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(
        HttpServletRequest request, 
        HttpServletResponse response, 
        Object handler //handler一般为HandlerMethod子类,用于获取请求的控制器方法
    ) throws Exception {
        //TODO
        return true; //返回true代表请求会继续向下执行,返回false会拦截请求直接返回响应
    }
    
    @Override
    public void postHandle( // 控制器方法执行后(响应还未返回客户端)
        HttpServletRequest request, 
        HttpServletResponse response, 
        Object handler, 
        ModelAndView modelAndView
    ) throws Exception {
        //TODO
    }

    @Override
    public void afterCompletion( // 请求完成时(响应已返回客户端)
        HttpServletRequest request, 
        HttpServletResponse response, 
        Object handler, Exception ex
    ) throws Exception {
        //TODO
    }
}
java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截器1
        registry.addInterceptor(new Interceptor01())
            .addPathPatterns("/**") //拦截哪些请求
            .excludePathPatterns( //不拦截哪些请求
                "/user/code",
                "/voucher/**"
        	)
            .order(0); //order设置执行顺序,值越小越先执行
        //拦截器2
        registry.addInterceptor(new Interceptor02()).order(1);  
    }
}
  • 举例:动态参数的路径跳转
java 复制代码
public class MoveInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) throws Exception {
        response.sendRedirect("/newAddress"); // 302 重定向,实现网址迁移
        return false; // 终止后续处理
    }
}

跨域处理

  • 描述:前端访问后端时,因为同源规则导致后端禁止前端访问

  • 原理

    1. 同源指的是:用户访问前端的请求 和 前端发送给后端的请求 之间的域名、端口、协议必须一样
    2. 跨域访问本质上不是错误,而是浏览器的限制行为(例如React访问Tomcat,但二者部署在不同的IP上)
  • 解决方法

    1. 前端直接设位置服务器代理
    2. 前端由Nginx托管,一定是同源的
    3. 服务/网关设置跨域

全局统一配置(推荐)

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 匹配指定访问路径,一般匹配所有请求
			   .allowedOrigins("*")  // 浏览器允许所有的域访问,注意allowCredentials(true)时,allowedOrigins不能为 *
                .allowCredentials(true)   // 允许带cookie访问
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*") // 允许跨域请求携带认证凭据 (如Cookie、Authorization、SSL 证书)
                .maxAge(3600); // 预检请求(OPTIONS)的缓存时间(秒)
    }
}

局部跨域设置

java 复制代码
//作用于控制器方法
@CrossOrigin(origins = {"http://localhost:8080", "http://localhost:8081"})
@getMapping("test")
public String test() {
    return "ok";
}

//作用于控制器类
@RestController
@RequestMapping("user")
@CrossOrigin(origins = {"*"})  //表示所有域都可以访问
public class TestController {					
	...
}

全局异常处理器

  • 描述:对Controller层异常处理的兜底逻辑

    1. 业务层异常一般会一直传递到控制器层,因此可以认为是整个Web项目的异常兜底
    2. 异步任务等场景异常默认不会向上传递,全局异常处理器无法捕获,应内部处理或者CompleteFuture链式处理
    3. servlet前置操作(如过滤器)用于请求未到达控制器层,其中抛出的异常也不会捕获,应内部处理
  • 注解说明

    1. @ControllerAdvice:指定异常扫描路径
    2. @ExceptionHandler:用于指定异常类型
    3. @RestControllerAdvice = @ControllerAdvice + @ResponseBody
java 复制代码
@RestControllerAdvice(basePackages = {"com.wyh.controller"})
@Slf4j
public class ControllerExceptionHandler {
    
    //处理权限异常,可以一次定义多个:@ExceptionHandler({xxxException.class, xxxException.class})
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ResultDto<Object>> handleAccessDenied(AccessDeniedException e) {
        return ResponseEntity.status(403)
            .contentType(MediaType.APPLICATION_JSON)
            .body(new ResultDto<>("500", e.getCause().getClass().toString(), null));
    }
    
    // 处理请求体校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<JsonResult> handleValidException(MethodArgumentNotValidException ex) {
        JsonResult fail = JsonResult.fail("method valid fail", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(fail);
    }
    
    // 处理参数校验异常
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, String>> handleValidationException(ConstraintViolationException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getConstraintViolations().forEach(violation -> {
            String fieldName = violation.getPropertyPath().toString();
            String message = violation.getMessage();
            errors.put(fieldName, message);
        });
        return ResponseEntity.badRequest().body(errors);
    }
    
    @ExceptionHandler(Exception.class)
    public void globalHandle(HttpServletResponse res, Exception e) {
        res.setContentType("application/json; charset=utf-8");
        ResultDto<Object> resultDto = new ResultDto<>("500", e.getCause().getClass().toString(), null);
        String jsonResult = JSON.toJSONString(resultDto);
        try {
            res.getWriter().write(jsonResult);
        } catch (IOException ex) { // Exception处理器是最后的兜底,不能再抛异常了
            log.error(ex.getMessage(), ex);
        }
    }
}
相关推荐
CodersCoder2 小时前
SpringBoot整合Spring-AI并使用Redis实现自定义上下文记忆对话
人工智能·spring boot·spring
北慕阳2 小时前
背诵-----------------------------
java·服务器·前端
没有bug.的程序员2 小时前
AOT 与 GraalVM Native Image 深度解析
java·jvm·测试工具·aot·gc·gc调优·graalvm native
零雲2 小时前
java面试:怎么保证消息队列当中的消息丢失、重复问题?
java·开发语言·面试
冬夜戏雪2 小时前
【java学习日记】【12.11】【11/60】
java·开发语言
用户2190326527352 小时前
实现Spring Cloud Sleuth的Trace ID追踪日志实战教程
java·后端
vx_bisheyuange2 小时前
基于SpringBoot的在线互动学习网站设计
java·spring boot·spring·毕业设计
roman_日积跬步-终至千里2 小时前
【源码分析】StarRocks TRUNCATE 语句执行流程:从 SQL 到数据清空的完整旅程
java·数据库·sql