很多 Spring Boot 入门文章只告诉你“把文件放进 static”“页面放进 templates”,却没有解释请求到底经过了谁、自动配置做了什么,以及需要扩展时应该改哪里。本文以一个可运行的小项目为主线,把 Spring Boot Web 开发中最容易混淆的知识点一次讲透。

本文示例基于 Spring Boot 3.5.x + Java 17,核心思路同样适用于 Spring Boot 4.x。文末还整理了 Spring Boot 2.x 升级时最常见的配置变化。


一、先看全局:一个请求在 Spring Boot 中经历了什么?

浏览器发出请求后,背后大致会经过下面这条链路:

浏览器
  ↓
内嵌 Tomcat
  ↓
DispatcherServlet(前端控制器)
  ↓
HandlerMapping(寻找匹配的 Controller)
  ↓
HandlerAdapter(调用目标方法)
  ↓
Controller
  ├─ 返回对象 → HttpMessageConverter → JSON
  └─ 返回视图名 → ViewResolver → Thymeleaf → HTML

过去使用传统 Spring MVC 时,我们经常需要在 web.xml 中注册 DispatcherServlet,再手动配置视图解析器、静态资源处理器和消息转换器。

Spring Boot 做的事情并不是“发明了一套新 MVC”,而是根据当前依赖和配置,帮我们自动装配 Spring MVC 的常用组件。我们只需要:

  1. 引入对应的 starter;
  2. 修改少量配置;
  3. 编写业务代码;
  4. 在确有需要时,通过扩展点覆盖默认行为。

这也是理解 Spring Boot 自动配置最实用的思路:

先使用默认配置;默认配置不满足需求时,优先改配置项;配置项仍不够,再实现官方提供的扩展接口。


二、创建项目:最小可运行依赖

下面使用 Maven 创建一个同时支持 Spring MVC 和 Thymeleaf 的项目。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.16</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>boot-web-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

推荐的目录结构如下:

src/main
├─ java/com/example/bootweb
│  ├─ BootWebApplication.java
│  ├─ config/WebMvcConfig.java
│  ├─ controller/PageController.java
│  └─ interceptor/LoginInterceptor.java
└─ resources
   ├─ static
   │  ├─ css/app.css
   │  ├─ js/app.js
   │  └─ index.html
   ├─ templates
   │  ├─ fragments/header.html
   │  └─ users.html
   └─ application.yml

这里最关键的区别是:

  • static 保存可以被浏览器直接访问的静态文件;
  • templates 保存需要经过模板引擎渲染的页面,不能直接用文件路径访问。

三、静态资源映射:为什么文件放对了却访问不到?

1. 默认静态资源目录

Spring Boot 默认会从 classpath 下的以下位置查找静态资源:

/META-INF/resources/
/resources/
/static/
/public/

实际开发最常用的是 src/main/resources/static

例如放入文件:

src/main/resources/static/css/app.css

默认访问地址为:

http://localhost:8080/css/app.css

注意:URL 中不需要出现 staticstatic 是 classpath 目录名,不是请求路径的一部分。

2. 自定义访问前缀

如果希望所有静态资源都通过 /assets/** 访问,可以配置:

spring:
  mvc:
    static-path-pattern: /assets/**

此时 static/css/app.css 的访问地址会变成:

http://localhost:8080/assets/css/app.css

3. 自定义资源位置

还可以修改 Spring Boot 查找静态资源的位置:

spring:
  web:
    resources:
      static-locations:
        - classpath:/static/
        - classpath:/public/
        - file:./uploads/

这里有一个很容易踩的坑:

spring.web.resources.static-locations 会替换默认位置,而不是在默认列表后简单追加。自定义时要把仍需保留的目录一起写上。

把本地上传目录直接映射为公开静态目录也要谨慎。用户上传的文件可能包含恶意 HTML、脚本或伪装文件,生产环境更推荐通过受控的下载接口或对象存储访问。

4. 欢迎页与 favicon

访问 / 时,Spring Boot 会优先查找静态资源目录中的 index.html;如果没有,再查找模板目录中的 index 模板。

同理,将 favicon.ico 放在静态资源目录中即可作为站点图标。

5. WebJars 是什么?

WebJars 可以把 jQuery、Bootstrap 等前端资源以 JAR 依赖的方式放入 classpath,默认通过 /webjars/** 访问。

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.7.1</version>
</dependency>

访问示例:

/webjars/jquery/3.7.1/jquery.min.js

在前后端分离项目中,静态依赖通常交给 npm、pnpm 或 CDN 管理;WebJars 更适合传统服务端模板项目。


四、Thymeleaf:从 Controller 到 HTML 的完整过程

1. 默认模板规则

引入 spring-boot-starter-thymeleaf 后,Spring Boot 会自动配置 Thymeleaf。默认规则可以简单理解为:

前缀:classpath:/templates/
后缀:.html

因此 Controller 返回:

return "users";

最终会解析:

classpath:/templates/users.html

开发阶段建议关闭模板缓存,修改 HTML 后能立即看到效果:

spring:
  thymeleaf:
    cache: false

生产环境不要关闭缓存,否则会增加模板解析开销。

2. 编写 Controller

package com.example.bootweb.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class PageController {

    @GetMapping("/users")
    public String users(Model model) {
        List<UserView> users = List.of(
                new UserView(1L, "张三", 92, true),
                new UserView(2L, "李四", 78, true),
                new UserView(3L, "王五", 56, false)
        );

        model.addAttribute("pageTitle", "用户列表");
        model.addAttribute("users", users);
        return "users";
    }

    public record UserView(Long id, String username, int score, boolean enabled) {
    }
}

这里必须使用 @Controller。如果写成 @RestController,字符串 users 会直接写入响应体,而不会被当成视图名。

3. 编写 Thymeleaf 模板

创建 src/main/resources/templates/users.html

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title th:text="${pageTitle}">用户列表</title>
    <link rel="stylesheet" th:href="@{/css/app.css}">
</head>
<body>
<h1 th:text="${pageTitle}">用户列表</h1>

<table>
    <thead>
    <tr>
        <th>序号</th>
        <th>用户名</th>
        <th>分数</th>
        <th>等级</th>
        <th>状态</th>
    </tr>
    </thead>
    <tbody>
    <tr th:each="user, stat : ${users}" th:object="${user}">
        <td th:text="${stat.count}">1</td>
        <td th:text="*{username}">张三</td>
        <td th:text="*{score}">92</td>
        <td th:text="*{score >= 85 ? '优秀' : (score >= 60 ? '合格' : '待提升')}">优秀</td>
        <td>
            <span th:if="*{enabled}">启用</span>
            <span th:unless="*{enabled}">禁用</span>
        </td>
    </tr>
    </tbody>
</table>
</body>
</html>

4. 五类必须掌握的表达式

表达式 作用 示例
${...} 读取变量 ${user.username}
*{...} 读取 th:object 选中的对象 *{username}
#{...} 读取国际化消息 #{page.title}
@{...} 生成上下文相关 URL @{/users/{id}(id=${user.id})}
~{...} 引用模板片段 ~{fragments/header :: siteHeader}

th:each 还可以声明状态变量:

<li th:each="user, stat : ${users}">
    <span th:text="${stat.index}"></span>  <!-- 从 0 开始 -->
    <span th:text="${stat.count}"></span>  <!-- 从 1 开始 -->
    <span th:text="${stat.first}"></span>
    <span th:text="${stat.last}"></span>
</li>

5. th:textth:utext 的区别

<div th:text="${content}"></div>
<div th:utext="${content}"></div>
  • th:text 会转义 HTML,默认更安全;
  • th:utext 会把内容当作 HTML 输出。

如果 content 来自用户输入,使用 th:utext 可能产生 XSS 漏洞。除非内容已经经过可靠的 HTML 清洗,否则优先使用 th:text


五、Spring MVC 自动配置到底帮我们做了什么?

引入 spring-boot-starter-web 后,Spring Boot 会自动提供或管理一批常用能力,包括:

  • DispatcherServlet 前端控制器;
  • Controller 请求映射;
  • 静态资源处理;
  • 欢迎页支持;
  • HttpMessageConverter 消息转换;
  • ConverterGenericConverterFormatter 注册;
  • 视图解析器;
  • Multipart 文件上传支持。

所以项目中通常不需要再写 web.xml,也不需要照搬传统 Spring MVC 的 XML 配置。

自动配置也不是黑盒。排查问题时,可以通过以下方式观察它:

debug=true

启动日志中的 CONDITIONS EVALUATION REPORT 会说明哪些自动配置生效、哪些没有生效,以及判断条件是什么。生产环境不建议长期打开。


六、正确扩展 Spring MVC:WebMvcConfigurer

实际开发中,我们经常需要增加视图跳转、拦截器、格式化器、静态资源规则或跨域配置。推荐实现 WebMvcConfigurer

package com.example.bootweb.config;

import com.example.bootweb.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Configuration(proxyBeanMethods = false)
public class WebMvcConfig implements WebMvcConfigurer {

    private static final DateTimeFormatter DATE_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd");

    private final LoginInterceptor loginInterceptor;

    public WebMvcConfig(LoginInterceptor loginInterceptor) {
        this.loginInterceptor = loginInterceptor;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/home", "/");
    }

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(String.class, LocalDate.class,
                source -> LocalDate.parse(source, DATE_FORMATTER));
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/login",
                        "/error",
                        "/css/**",
                        "/js/**",
                        "/images/**",
                        "/webjars/**"
                );
    }
}

一个极其重要的坑:不要随手加 @EnableWebMvc

如果只是想在 Spring Boot 默认配置之上增加功能,实现 WebMvcConfigurer 即可,通常不要添加 @EnableWebMvc

@EnableWebMvc 表示你要全面接管 Spring MVC 配置。添加之后,一些由 Spring Boot 提供的默认行为可能不再生效,静态资源突然 404 就是常见后果之一。

只有当你明确希望放弃 Spring Boot MVC 自动配置、完全自己管理 MVC 时,才应该使用它。


七、拦截器:登录校验应该怎么注册?

先创建拦截器。Spring Boot 3.x 使用的是 jakarta.servlet 包,而不是旧版的 javax.servlet

package com.example.bootweb.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {

        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser != null) {
            return true;
        }

        response.sendRedirect(request.getContextPath() + "/login");
        return false;
    }
}

然后在上一节的 addInterceptors 中注册。

有两点值得注意:

  1. 不要在配置中直接 new LoginInterceptor(),否则拦截器中的依赖注入可能失效;
  2. 记得排除登录页、错误页和静态资源,否则页面可能因为 CSS、JS 也被拦截而显示异常。

真实项目如果已经使用 Spring Security,身份认证和授权应优先交给 Spring Security,而不是自行维护一套简化拦截器。


八、日期格式:请求参数与 JSON 序列化不是一回事

这是一个非常高频的坑。

假设前端传入:

/report?date=2026-06-30

Controller 接收的是请求参数绑定,可以这样统一配置:

spring:
  mvc:
    format:
      date: yyyy-MM-dd
      time: HH:mm:ss
      date-time: yyyy-MM-dd HH:mm:ss

也可以在单个参数上使用:

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.ResponseBody;

@ResponseBody
@GetMapping("/report")
public String report(
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
    return date.toString();
}

而对象转换为 JSON 属于 Jackson 的职责。例如:

import com.fasterxml.jackson.annotation.JsonFormat;

public class UserResponse {

    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthday;

    // getter、setter
}

可以这样记:

  • 表单、查询参数绑定:看 Spring MVC Formatter;
  • JSON 请求体和响应体:看 Jackson;
  • 二者不是同一个转换链。

另外,旧教程常见的 Fastjson 1.x 全局替换消息转换器写法不建议继续照搬。Spring Boot 已默认配置 Jackson;如果确需调整转换器,优先使用配置项、Jackson 定制器或 extendMessageConverters,避免用 configureMessageConverters 不慎覆盖默认转换器列表。


九、文件上传:能跑只是第一步,安全落盘更重要

1. 上传表单

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" required>
    <button type="submit">上传</button>
</form>

enctype="multipart/form-data" 不能省略,输入框的 name 也要与后端参数名一致。

2. 限制上传大小

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB
  • max-file-size:单个文件大小上限;
  • max-request-size:一次 multipart 请求的总大小上限。

如果前面还有 Nginx、网关或云负载均衡,也要同步检查上游的请求体大小限制。

3. 安全保存文件

package com.example.bootweb.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

@RestController
public class UploadController {

    private static final Set<String> ALLOWED_EXTENSIONS =
            Set.of("jpg", "jpeg", "png", "pdf");

    private final Path uploadRoot = Paths.get("uploads")
            .toAbsolutePath()
            .normalize();

    @PostMapping("/upload")
    public ResponseEntity<Map<String, String>> upload(
            @RequestParam("file") MultipartFile file) throws IOException {

        if (file.isEmpty()) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "请选择要上传的文件"));
        }

        String originalName = StringUtils.cleanPath(
                file.getOriginalFilename() == null ? "" : file.getOriginalFilename());
        String extension = getExtension(originalName);

        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "不支持的文件类型"));
        }

        Files.createDirectories(uploadRoot);

        String storedName = UUID.randomUUID() + "." + extension;
        Path target = uploadRoot.resolve(storedName).normalize();

        if (!target.startsWith(uploadRoot)) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "非法文件路径"));
        }

        try (InputStream input = file.getInputStream()) {
            Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING);
        }

        return ResponseEntity.ok(Map.of(
                "message", "上传成功",
                "fileName", storedName
        ));
    }

    private String getExtension(String fileName) {
        int index = fileName.lastIndexOf('.');
        return index < 0 ? "" : fileName.substring(index + 1).toLowerCase();
    }
}

这段代码做了几件比“直接拼接原文件名”更重要的事:

  • 不信任客户端提供的文件名;
  • 使用 UUID 生成落盘名称,避免覆盖和路径冲突;
  • 规范化目标路径并阻止目录穿越;
  • 对扩展名做白名单校验;
  • 使用 try-with-resources 正确关闭输入流。

生产环境还应增加 MIME 检测、文件内容检测、病毒扫描、访问权限控制和独立存储。只检查扩展名并不能证明文件真实类型。


十、配置内嵌服务器

spring-boot-starter-web 默认包含内嵌 Tomcat,因此应用可以直接打成可执行 JAR:

mvn clean package
java -jar target/boot-web-demo-0.0.1-SNAPSHOT.jar

常见服务器配置如下:

server:
  port: 8081
  servlet:
    context-path: /demo
  shutdown: graceful

配置后,应用访问前缀变成:

http://localhost:8081/demo

如果 8080 端口经常冲突,测试时还可以设置:

server:
  port: 0

系统会选择一个可用随机端口,实际端口可以从启动日志中查看。

将 Tomcat 换成 Jetty

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

替换容器后,Controller 等业务代码通常不需要修改。


十一、注册 Servlet、Filter、Listener 的两种方式

Spring Boot 项目通常没有 web.xml,但仍然可以注册 Servlet 三大组件。

方式一:注解扫描

组件分别使用:

@WebServlet
@WebFilter
@WebListener

启动类增加:

@SpringBootApplication
@ServletComponentScan
public class BootWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootWebApplication.class, args);
    }
}

注意导入的是 jakarta.servlet.annotation.*

方式二:RegistrationBean

也可以在配置类中声明:

@Bean
ServletRegistrationBean<MyServlet> myServlet() {
    return new ServletRegistrationBean<>(new MyServlet(), "/my-servlet");
}

@Bean
FilterRegistrationBean<MyFilter> myFilter() {
    FilterRegistrationBean<MyFilter> bean =
            new FilterRegistrationBean<>(new MyFilter());
    bean.addUrlPatterns("/api/*");
    bean.setOrder(1);
    return bean;
}

@Bean
ServletListenerRegistrationBean<MyListener> myListener() {
    return new ServletListenerRegistrationBean<>(new MyListener());
}

对于普通 Spring MVC 业务,优先使用 Controller、HandlerInterceptor 和 Spring Security;只有确实需要接入 Servlet 底层生命周期时,才使用这些原生组件。


十二、什么时候需要部署 WAR?

大多数新项目直接使用可执行 JAR 或容器镜像即可。只有公司统一维护外部 Tomcat、旧系统改造等场景,才通常需要 WAR 部署。

1. 修改打包方式

<packaging>war</packaging>

2. 将 Tomcat 标记为 provided

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

3. 继承 SpringBootServletInitializer

package com.example.bootweb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class BootWebApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(
            SpringApplicationBuilder application) {
        return application.sources(BootWebApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(BootWebApplication.class, args);
    }
}

4. 打包并部署

mvn clean package

target 下生成的 WAR 放入外部 Tomcat 的 webapps 目录即可。

两种启动方式的核心区别是:

  • JAR:main 方法启动 Spring Boot,再创建内嵌 Servlet 容器;
  • WAR:外部 Servlet 容器先启动,再通过 SpringBootServletInitializer 初始化 Spring 容器。

十三、Spring Boot 2.x 升级到 3.x 的高频变化

如果你正在看较早的教程,下面这些变化尤其需要注意:

旧写法 新写法
javax.servlet.* jakarta.servlet.*
server.context-path server.servlet.context-path
spring.resources.* spring.web.resources.*
spring.resources.static-locations spring.web.resources.static-locations
WebMvcConfigurerAdapter 直接实现 WebMvcConfigurer

还要注意:Spring Boot 3.x 至少需要 Java 17。升级不只是改一个版本号,涉及 Servlet API、依赖兼容性和配置项变化时,应结合官方迁移指南逐项检查。


十四、最常见的 8 个问题

1. static 下的文件为什么 404?

检查 URL 是否错误地包含了 /static;检查是否自定义了 static-locations;检查是否误加了 @EnableWebMvc;如果启用了 Spring Security,还要检查授权规则。

2. Controller 返回页面名,浏览器却只显示字符串?

确认使用的是 @Controller,而不是 @RestController;方法上也不要添加 @ResponseBody

3. Thymeleaf 页面修改后不生效?

开发环境将 spring.thymeleaf.cache 设置为 false,并确认 IDE 已把修改后的资源复制到 classpath。

4. th:text 为什么不渲染 HTML?

因为 th:text 会转义内容。不要为了省事直接换成 th:utext,先确认内容是否可信并经过清洗。

5. 拦截器为什么把 CSS 和登录页也拦了?

注册时没有通过 excludePathPatterns 排除公共路径。

6. 日期配置了仍然报格式错误?

先确认数据来自查询参数、表单还是 JSON。MVC 参数绑定和 Jackson JSON 转换使用的是不同机制。

7. 上传大文件为什么还没到 Controller 就失败?

请求可能先被 Spring Boot、Tomcat、Nginx 或网关的大小限制拒绝,需要逐层检查。

8. 自定义消息转换器后,原来的 JSON 或字符串转换失效?

很可能是重写 configureMessageConverters 后覆盖了默认列表。多数场景优先使用配置项、单独声明定制 Bean,或使用 extendMessageConverters


总结

Spring Boot Web 开发真正需要掌握的,不是背诵自动配置类名,而是分清三个层次:

  1. 约定:静态资源、模板和欢迎页应该放在哪里;
  2. 配置:端口、路径、上传大小和日期格式如何调整;
  3. 扩展:什么时候使用 WebMvcConfigurer、拦截器和 Servlet 组件。

记住一个原则:

能用 Spring Boot 配置项解决的,不急着写代码;能用 WebMvcConfigurer 扩展的,不急着全面接管 MVC;能用可执行 JAR 部署的,不必先折腾外部 Tomcat。

理解这条边界之后,静态资源 404、模板不渲染、拦截器误伤、日期转换失败等问题,都会从“玄学报错”变成可以顺着请求链定位的普通问题。


官方参考

如果这篇文章帮你把 Spring Boot Web 的主线串起来了,建议收藏后配合一个小项目逐段验证。代码跑通一次,比零散地记十个配置项更有效。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐