SpringBoot的MVC自动配置虽然方便,但满足不了实际业务的定制需求,因此Spring提供了WebMvcConfigurer接口作为扩展入口,我们只需实现这个接口并重写对应方法,就能自定义MVC的各种行为。
一、先搞懂「为什么需要扩展」
SpringBoot最大的特点是"自动配置",比如MVC相关的路径映射、JSON转换、参数解析等,框架都帮我们默认配好了。但实际开发中,业务场景千变万化:
- 想让访问
/tx直接跳转到success.html,不想写冗余的Controller; - 前端传的日期字符串(如2026-02-01)想自动转成后端的Date类型,默认格式不匹配;
- 不想用Spring默认的Jackson处理JSON,想换成阿里的FastJSON;
- 想在接口调用前后加日志、做登录校验(拦截器);
这些定制化需求,自动配置覆盖不到,所以Spring提供了WebMvcConfigurer接口------这个接口里全是"空的默认方法"(不用全实现,想用哪个重写哪个),我们只需要创建一个加了@Configuration的配置类,实现这个接口,重写对应方法就能完成扩展。
二、逐个理解核心扩展功能("解决什么问题+怎么做")
1. 注册视图控制器(请求转发)
-
解决的问题 :简单的页面跳转,不用写Controller方法。比如想让用户访问
/tx时,直接跳转到success.html,没必要写一个空的Controller接口。 -
怎么做 :
创建配置类MyMVCCofnig(加@Configuration)实现WebMvcConfigurer,重写addViewControllers方法:java@Configuration public class MyMVCCofnig implements WebMvcConfigurer{ @Override public void addViewControllers(ViewControllerRegistry registry) { // 匹配/tx路径,转发到名为success的视图(比如success.html) registry.addViewController("/tx").setViewName("success"); } } -
效果 :访问
http://xxx/tx,直接跳转到success.html,不用写@GetMapping("/tx")的Controller方法。
2. 注册格式化器(日期格式化)
-
解决的问题:前端传的日期是字符串(如"2026-02-01"),后端接收为Date类型时,默认的解析格式不匹配会报错,需要自定义解析规则。
-
怎么做 :重写
addFormatters方法,添加Date类型的格式化器,指定"字符串转Date"的格式为yyyy-MM-dd:java@Override public void addFormatters(FormatterRegistry registry) { registry.addFormatter(new Formatter<Date>() { // Date转字符串(如果需要前端展示时用,这里返回null可忽略) @Override public String print(Date date, Locale locale) { return null; } // 字符串转Date(核心:把前端传的"2026-02-01"转成Date对象) @Override public Date parse(String s, Locale locale) throws ParseException { return new SimpleDateFormat("yyyy-MM-dd").parse(s); } }); } -
补充:虽然配置文件也能配日期格式,但用Formatter更灵活,能加自定义逻辑(比如校验日期合法性)。
3. 扩展消息转换器(用FastJSON替代默认JSON工具)
-
解决的问题:SpringBoot默认用Jackson处理JSON的序列化/反序列化,但FastJSON在某些场景下(比如性能、定制化)更符合业务需求,需要替换。
-
怎么做 :
第一步:先在pom.xml引入FastJSON依赖:xml<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>第二步:重写
configureMessageConverters方法,添加FastJSON转换器:java@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fc = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); // 配置JSON格式化(比如排版更美观) fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fc.setFastJsonConfig(fastJsonConfig); // 把FastJSON转换器加入MVC的转换器列表 converters.add(fc); } -
进阶 :实体类中可以用
@JSONField注解定制字段,比如指定日期字段的输出格式:@JSONField(format = "yyyy-MM-dd") private Date date;。
4. 注册拦截器(接口请求拦截)
-
解决的问题 :想在接口调用的不同阶段做统一处理,比如:
- 请求处理前:校验用户是否登录;
- 请求处理后:记录接口耗时;
- 请求完成后:清理资源;
-
怎么做 :分两步------先写拦截器,再注册拦截器。
第一步:创建拦截器类,实现HandlerInterceptor接口,重写3个核心方法:javapublic class MyInterceptor implements HandlerInterceptor { // 请求处理前执行(返回true=放行,false=拦截) @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("前置拦截:比如校验登录"); return true; // 放行 } // 请求处理后、视图渲染前执行(比如修改返回数据) @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("后置拦截:比如记录接口耗时"); } // 请求完全完成后执行(比如清理资源) @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("最终拦截:比如记录异常"); } }第二步:重写
addInterceptors方法,注册拦截器并配置拦截规则:java@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns("/hello2"); // 排除/hello2路径(不拦截) } -
关键:拦截器的路径配置很灵活,能精准控制哪些接口需要拦截、哪些跳过。
WebMvcConfigurer是SpringBoot给的MVC扩展入口,通过实现该接口并重写对应方法,可定制MVC行为,无需修改框架源码;- 核心扩展场景:视图跳转(addViewControllers)、参数格式化(addFormatters)、JSON转换(configureMessageConverters)、接口拦截(addInterceptors);
- 所有扩展都需要创建加了
@Configuration的配置类,实现接口后重写对应方法即可生效。
过滤器、拦截器、监听器
先给一个核心比喻,帮你快速建立认知:
把一次HTTP请求比作"去餐厅吃饭":
- 过滤器(Filter):餐厅门口的保安,管所有进出的人(所有请求),不管是吃饭、送外卖、找老板,都要过他这关(最外层);
- 拦截器(Interceptor):餐厅里的服务员,只服务来吃饭的顾客(仅Controller请求),送外卖的不管(中间层);
- 监听器(Listener):餐厅里的烟雾报警器,不干预你吃饭,但检测到烟雾(事件发生)就报警(只监听、不干预)。
一、过滤器(Filter)
1. 核心认知
- 所属:Servlet规范(JavaEE标准),不属于Spring,所有Web框架(SpringBoot、Struts等)都能用;
- 执行时机:请求进入Tomcat后 → 到达Spring MVC的核心DispatcherServlet之前;响应返回时,先过Filter再出Tomcat;
- 作用范围:所有HTTP请求(包括静态资源:js/css/html,以及Controller接口);
- 核心能力:能修改请求/响应内容,可拦截请求(不放行),但拿不到Spring的Bean(比如Service)。
2. 典型业务场景
- 统一设置请求/响应的字符编码(解决中文乱码);
- 全局登录校验(所有请求检查token,白名单除外);
- 防XSS攻击(过滤请求参数中的恶意脚本);
- 记录所有请求的访问日志(URL、IP、耗时)。
3. 实际代码(SpringBoot版)
场景:实现登录校验过滤器------所有请求(除/login接口)都检查token,无token则返回"未登录"。
步骤1:编写过滤器类
java
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 交给Spring管理(也可用@WebFilter注解,需启动类加@ServletComponentScan)
@Component
public class LoginCheckFilter implements Filter {
// 初始化:服务器启动时执行,只执行1次
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("LoginCheckFilter初始化完成");
}
// 核心方法:每次请求都会执行,处理过滤逻辑
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 强转成HTTP请求/响应(因为要操作URL、请求头)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 获取请求URL
String url = request.getRequestURI();
System.out.println("过滤器拦截到请求:" + url);
// 2. 白名单:登录接口不拦截
if (url.contains("/login")) {
filterChain.doFilter(request, response); // 放行:让请求继续往下走
return; // 放行后直接返回,不执行后续逻辑
}
// 3. 检查请求头中的token
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
// 未登录:返回JSON提示,不放行
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录,请先登录!\"}");
return;
}
// 4. token有效,放行到下一个Filter/Controller
filterChain.doFilter(request, response);
}
// 销毁:服务器关闭时执行,只执行1次
@Override
public void destroy() {
System.out.println("LoginCheckFilter已销毁");
}
}
步骤2:启动类(如果用@WebFilter需加注解)
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
// 若用@WebFilter(urlPatterns = "/*")注解,需加此注解扫描Filter
// @ServletComponentScan
public class MySpringBootApp {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApp.class, args);
}
}
步骤3:测试Controller
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
// 白名单接口:不拦截
@PostMapping("/login")
public String login() {
return "登录成功,token:abc123";
}
// 需要token的接口:无token会被拦截
@GetMapping("/user/info")
public String getUserInfo() {
return "用户信息:张三,18岁";
}
}
4. 关键说明
- 必须调用
filterChain.doFilter()才会放行,否则请求会被"卡"在过滤器; - Filter是Tomcat管理的,默认拿不到Spring Bean,若需要可通过
WebApplicationContextUtils手动获取; - 拦截范围通过
urlPatterns控制(如/*拦截所有,/api/*拦截api开头的请求)。
二、拦截器(Interceptor)
1. 核心认知
- 所属:Spring MVC框架特有,依赖Spring环境;
- 执行时机:请求经过Filter后 → 到达DispatcherServlet → 分发到Controller之前;响应返回时,Controller处理完先过Interceptor再到Filter;
- 作用范围:仅拦截Controller请求(静态资源默认不拦截,可配置);
- 核心能力:能拿到Spring Bean(如Service/Mapper),可细粒度控制拦截范围,支持前置/后置/完成三个阶段的处理。
2. 典型业务场景
- 接口级别的登录/权限校验(只拦截特定业务接口);
- 记录每个Controller接口的执行耗时;
- 统一处理接口日志(记录请求参数、返回结果);
- 接口访问频率限制(防刷)。
3. 实际代码(SpringBoot版)
场景:实现接口耗时统计拦截器,同时校验/user开头接口的登录状态。
步骤1:编写拦截器类
java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 交给Spring管理,方便后续注入
@Component
public class TimeCostInterceptor implements HandlerInterceptor {
// 前置拦截:Controller方法执行前调用(返回true放行,false拦截)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 记录请求开始时间,存到request中
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
// 只拦截/user开头的接口,校验token
String url = request.getRequestURI();
if (url.startsWith("/user")) {
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录,无法访问用户接口!\"}");
return false; // 拦截
}
}
System.out.println("【前置拦截】请求URL:" + url);
return true; // 放行
}
// 后置拦截:Controller方法执行后、视图渲染前调用(RestController无视图,此方法仅标记执行完成)
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("【后置拦截】Controller方法执行完成");
}
// 完成拦截:整个请求结束后调用(包括视图渲染),适合统计耗时、清理资源
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 计算接口耗时
long startTime = (long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
long cost = endTime - startTime;
System.out.println("【完成拦截】请求URL:" + request.getRequestURI() + ",耗时:" + cost + "ms");
}
}
步骤2:配置拦截器(注册到Spring MVC)
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 配置类注解
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TimeCostInterceptor timeCostInterceptor; // 注入拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeCostInterceptor)
.addPathPatterns("/**") // 拦截所有Controller请求
.excludePathPatterns("/login", "/static/**"); // 排除登录接口和静态资源
}
}
测试效果
-
访问
/login:不拦截,控制台无token校验日志; -
访问
/user/info(无token):返回401;带token则正常返回,控制台打印:【前置拦截】请求URL:/user/info 【后置拦截】Controller方法执行完成 【完成拦截】请求URL:/user/info,耗时:8ms
4. 关键说明
- Interceptor是Spring管理的,可直接@Autowired注入Service、Mapper等Bean;
- preHandle返回false时,Controller方法不会执行,且postHandle/afterCompletion也不会触发;
- 只拦截DispatcherServlet分发的请求(即Controller接口),静态资源默认不拦截。
三、监听器(Listener)
1. 核心认知
- 所属:Servlet规范(JavaEE标准);
- 执行逻辑:不干预请求流程,而是"事件驱动"------当特定事件发生时(如服务器启动/关闭、Session创建/销毁),自动触发对应逻辑;
- 核心能力:监听不同维度的事件(应用级、会话级、请求级),做初始化、统计、清理等操作。
2. 典型业务场景
- 服务器启动时初始化全局数据(如加载字典数据到内存);
- 统计在线用户数(监听Session的创建/销毁);
- 监听请求创建,记录所有请求的访问日志;
- 服务器关闭时清理资源(如关闭连接池)。
3. 实际代码(SpringBoot版)
场景:监听服务器启动/关闭(初始化全局数据)+ 监听Session创建/销毁(统计在线人数)。
步骤1:编写监听器类
java
import org.springframework.stereotype.Component;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
// 方式1:@WebListener(需启动类加@ServletComponentScan)
// 方式2:@Component(交给Spring管理,无需额外注解)
@Component
@WebListener
public class MyListener implements ServletContextListener, HttpSessionListener {
// 在线人数统计(全局共享)
private int onlineUserCount = 0;
// ========== 监听应用启动/关闭(ServletContext是应用全局上下文) ==========
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("【应用启动】服务器启动,初始化全局字典数据");
// 模拟初始化全局字典(应用级别,所有用户共享)
sce.getServletContext().setAttribute("dict_data", "性别:1-男,2-女;状态:0-禁用,1-启用");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("【应用关闭】服务器关闭,清理资源");
sce.getServletContext().removeAttribute("dict_data");
}
// ========== 监听Session创建/销毁(Session是用户级别) ==========
// 用户第一次访问时创建Session,触发此方法
@Override
public void sessionCreated(HttpSessionEvent se) {
onlineUserCount++;
System.out.println("【用户上线】当前在线人数:" + onlineUserCount);
// 在线人数存入全局上下文,供所有请求访问
se.getSession().getServletContext().setAttribute("onlineUserCount", onlineUserCount);
}
// 用户退出/会话超时(默认30分钟),触发此方法
@Override
public void sessionDestroyed(HttpSessionEvent se) {
if (onlineUserCount > 0) {
onlineUserCount--;
}
System.out.println("【用户下线】当前在线人数:" + onlineUserCount);
se.getSession().getServletContext().setAttribute("onlineUserCount", onlineUserCount);
}
}
步骤2:测试Controller(查看在线人数)
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
public class OnlineCountController {
@GetMapping("/online/count")
public String getOnlineCount(HttpServletRequest request) {
// 获取全局在线人数和字典数据
Integer count = (Integer) request.getServletContext().getAttribute("onlineUserCount");
String dict = (String) request.getServletContext().getAttribute("dict_data");
return "当前在线人数:" + count + "<br/>全局字典:" + dict;
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 手动销毁Session,触发sessionDestroyed方法
session.invalidate();
return "退出成功,当前在线人数已减少";
}
}
测试效果
- 启动服务器:控制台打印「【应用启动】服务器启动,初始化全局字典数据」;
- 第一次访问
/online/count:控制台打印「【用户上线】当前在线人数:1」,接口返回在线人数1; - 访问
/logout:控制台打印「【用户下线】当前在线人数:0」。
4. 关键说明
- 监听器的核心是"事件":ServletContextEvent(应用事件)、HttpSessionEvent(会话事件)、ServletRequestEvent(请求事件);
- Session默认超时时间30分钟,超时后自动销毁,触发sessionDestroyed;
- 监听器只"监听"不"干预",不会影响请求的正常流程。
四、核心对比
| 特性 | 过滤器(Filter) | 拦截器(Interceptor) | 监听器(Listener) |
|---|---|---|---|
| 所属框架 | Servlet(JavaEE) | Spring MVC(依赖Spring) | Servlet(JavaEE) |
| 执行层面 | Tomcat容器层(最外层) | Spring MVC层(中间层) | 事件驱动(无固定执行顺序) |
| 作用范围 | 所有请求(静态资源+Controller) | 仅Controller请求 | 不干预请求,只监听事件 |
| 能否用Spring Bean | 需手动获取 | 直接注入(Spring管理) | Spring管理的Listener可直接用 |
| 核心用途 | 全局预处理(编码、跨域、粗粒度校验) | 业务拦截(细粒度权限、耗时统计) | 事件监听(初始化、统计、清理) |
总结
- 过滤器:Servlet层面的全局拦截,管所有请求,适合做"粗粒度"的统一处理(如编码、全局登录校验);
- 拦截器:Spring MVC层面的精准拦截,只管业务接口,适合做"细粒度"的业务逻辑(如接口耗时、权限校验);
- 监听器:事件驱动的观察者,不干预请求流程,适合做初始化、统计、资源清理等被动操作。
实际开发中,三者常配合使用:Filter做全局编码/跨域 → Interceptor做接口权限/耗时 → Listener做应用初始化/在线人数统计。
springboot使用外置的servlet容器
核心原理说明
SpringBoot默认使用内置Tomcat,要使用外置容器,核心是:
- 将项目打包为
war包(而非默认的jar包) - 排除SpringBoot内置的Tomcat依赖
- 让SpringBoot应用适配外置Servlet容器的启动规范(继承
SpringBootServletInitializer)
一、环境准备
- JDK 8+(和你的外置Tomcat版本匹配,比如Tomcat 9推荐JDK8+)
- Maven/Gradle(这里用Maven演示)
- 外置Tomcat(下载地址:https://tomcat.apache.org/ ,推荐Tomcat 9)
- IDE(如IntelliJ IDEA)
二、具体演示步骤
步骤1:创建基础SpringBoot项目
先创建一个最简单的SpringBoot Web项目,包含一个测试接口:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class ExternalTomcatDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalTomcatDemoApplication.class, args);
}
// 测试接口
@GetMapping("/hello")
public String hello() {
return "Hello, External Tomcat!";
}
}
步骤2:修改pom.xml配置(核心)
这一步是关键,需要调整打包方式、排除内置Tomcat、添加Servlet API依赖:
xml
<?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>2.7.18</version> <!-- 稳定版本,适配Tomcat 9 -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>external-tomcat-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 1. 打包方式改为war(默认是jar) -->
<packaging>war</packaging>
<name>external-tomcat-demo</name>
<description>Demo project for Spring Boot with external Tomcat</description>
<dependencies>
<!-- Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 2. 排除内置的Tomcat依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 3. 添加Servlet API依赖(provided表示打包时不包含,由外置容器提供) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- 测试依赖(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>external-tomcat-demo</finalName> <!-- 打包后的war包名称 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
步骤3:修改启动类,适配外置容器
让启动类继承SpringBootServletInitializer并重写configure方法(这是外置容器启动SpringBoot应用的入口):
java
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;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
// 继承SpringBootServletInitializer,适配外置Servlet容器
public class ExternalTomcatDemoApplication extends SpringBootServletInitializer {
// 内置容器启动入口(保留不影响,外置容器不会走这个方法)
public static void main(String[] args) {
SpringApplication.run(ExternalTomcatDemoApplication.class, args);
}
// 外置容器启动入口:重写configure方法
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
// 指定SpringBoot主类
return application.sources(ExternalTomcatDemoApplication.class);
}
// 测试接口
@GetMapping("/hello")
public String hello() {
return "Hello, External Tomcat!";
}
}
步骤4:打包项目为war包
- 在IDEA中打开「Maven」面板(右侧),找到项目→Lifecycle→
clean(清理旧包)→package(打包)。 - 打包完成后,在项目的
target目录下会生成external-tomcat-demo.war文件。
步骤5:部署到外置Tomcat并运行
- 解压下载的外置Tomcat到任意目录(如
D:\apache-tomcat-9.0.80)。 - 将
external-tomcat-demo.war复制到Tomcat的webapps目录下(如D:\apache-tomcat-9.0.80\webapps)。 - 启动Tomcat:双击Tomcat目录下
bin文件夹中的startup.bat(Windows)/startup.sh(Linux)。 - 访问测试:打开浏览器,访问
http://localhost:8080/external-tomcat-demo/hello(注意路径包含war包名称)。- 成功的话,页面会显示:
Hello, External Tomcat!
- 成功的话,页面会显示:
步骤6:验证(可选)
- 关闭内置容器验证:如果直接运行启动类的
main方法,会报错(因为已经排除了内置Tomcat),证明项目不再依赖内置容器。 - 停止Tomcat:双击
bin目录下的shutdown.bat(Windows)/shutdown.sh(Linux),访问接口会提示无法连接。
总结
- 核心配置 :将打包方式改为
war,排除SpringBoot内置Tomcat依赖,添加javax.servlet-api的provided依赖。 - 启动适配 :启动类必须继承
SpringBootServletInitializer并重写configure方法,让外置容器能识别SpringBoot应用。 - 部署运行 :将打包后的war包放入外置Tomcat的
webapps目录,启动Tomcat即可访问(访问路径需包含war包名称)。
通过以上步骤,你就能完整演示SpringBoot项目从「内置容器」切换到「外置Servlet容器」的全过程,核心是让SpringBoot应用适配Servlet容器的启动规范,而非依赖自身的内置容器。
SpringBoot使用外置Servlet容器(如Tomcat)的实际价值和适用场景,简单来说,这么做的核心目的是适配企业级生产环境的部署、运维和性能需求------内置容器适合快速开发,而外置容器解决了生产环境的"可控性、统一性、扩展性"问题。
一、核心作用:解决生产环境的实际痛点
1. 企业级统一运维管理
这是最核心的价值。
- 问题:如果每个SpringBoot应用都用内置Tomcat,每个应用都是独立的进程,需要单独管理端口、日志、启动/停止、监控,运维团队要维护成百上千个"小Tomcat",成本极高。
- 解决 :外置容器可以将多个SpringBoot应用(打包成war包)部署到同一个Tomcat实例 (或Tomcat集群)中,运维只需要管理容器本身,不用逐个维护应用的内置容器:
- 比如公司有10个微服务,用外置Tomcat只需部署到2-3个Tomcat节点,统一配置端口、日志路径、JVM参数;
- 启停、重启、监控都针对Tomcat容器操作,而非单个应用,效率提升数倍。
2. 精细化的容器性能调优
SpringBoot内置容器的配置非常简化(只能通过application.yml改端口、基础线程数),但生产环境需要更深度的调优:
- 外置容器支持丰富的调优项 :比如Tomcat的
server.xml可以配置线程池(maxThreads/minSpareThreads)、连接超时、IO模型(NIO/APR)、SSL证书、会话共享、访问日志切割等; - 举例 :要优化高并发场景下的Tomcat性能,你可以调整
maxConnections(最大连接数)、acceptCount(请求队列长度),这些参数在内置容器中要么无法配置,要么配置极不灵活,而外置Tomcat可以精细化控制。
3. 兼容老系统与多应用共存
很多企业还在维护基于SSM/Servlet的老项目(这些项目本身就是war包部署到外置Tomcat):
- 如果新的SpringBoot项目用外置容器,就能和老项目部署在同一个Tomcat实例中,共享容器资源(比如数据源、会话、线程池);
- 避免系统架构割裂,也方便将老系统逐步迁移到SpringBoot(先把老项目和新项目部署在同一容器,再逐步替换)。
4. 灵活的容器版本升级/切换
- 内置容器:SpringBoot版本和内置容器版本强绑定(比如SpringBoot 2.7.x绑定Tomcat 9),如果Tomcat出安全漏洞需要升级,必须升级SpringBoot版本,可能引发兼容性问题;
- 外置容器:只需替换Tomcat安装包(比如从Tomcat 9升到10),重启容器即可,应用代码完全不用改,极大降低升级风险。
- 此外,还能灵活切换容器(比如从Tomcat换成Jetty/Undertow),只需调整外置容器,不用修改SpringBoot项目。
5. 满足企业级合规与标准化要求
大型企业有严格的运维规范:
- 要求所有Web应用必须部署到指定版本的Servlet容器中;
- 容器需统一配置SSL证书、接入日志平台(如ELK)、做安全加固(比如禁用不安全的协议);
- 内置容器无法满足这种"统一管控",因为每个应用的容器是独立的,运维无法批量配置。
二、什么时候不用外置容器?
并非所有场景都需要外置容器,这些情况用内置容器更合适:
- 开发/测试阶段:内置容器启动快、零配置,直接
main方法运行即可,提升开发效率; - 微服务容器化部署(Docker/K8s):每个微服务打包成Docker镜像,内置容器+镜像隔离,替代传统的外置容器集群;
- 简单的小型应用:无高并发、无复杂调优需求,内置容器足够用。
总结
- 核心价值 :外置Servlet容器解决了企业级部署的统一运维、性能调优、兼容老系统 三大核心痛点,是生产环境的主流选择;
- 适用场景:多应用部署、需要精细化调优容器、与老Servlet项目共存、企业有统一容器运维规范;
- 对比内置容器:内置容器胜在"快"(开发测试),外置容器胜在"稳"(生产运维)。
简单来说,内置容器是"为开发者服务",外置容器是"为运维和生产环境服务"。
拓展:@JsonField和@DateFormat
想搞清楚@JsonField和@DateFormat(实际开发中常用的是Spring的@DateTimeFormat)这两个注解的核心区别。
第一步:先认清两个注解的"本质"
这两个注解看似都和"日期格式"有关,但所属的库不同、解决的问题场景完全不一样,先从核心定位讲起:
1. @JsonField(阿里fastjson)
- 所属库 :阿里巴巴的 fastjson/fastjson2(包路径:
com.alibaba.fastjson2.annotation.JsonField) - 核心定位 :处理 JSON ↔ Java对象 之间的转换(序列化/反序列化)。
你可以把它理解为"JSON翻译官"------把Java对象转成前端能看懂的JSON字符串(序列化),或把前端传的JSON字符串转回Java对象(反序列化)。 - 能力范围 :
- 不仅能设置日期字段的JSON格式(比如把Java的
Date转成2026-02-01); - 还能改JSON字段名(比如Java字段
createTime,JSON里想叫create_time); - 控制字段是否参与转换(比如敏感字段不想返回给前端);
- 甚至能指定JSON字段的排序。
- 不仅能设置日期字段的JSON格式(比如把Java的
2. @DateTimeFormat(Spring框架,常被误称@DateFormat)
- 所属库 :Spring 核心框架(包路径:
org.springframework.format.annotation.DateTimeFormat) - 核心定位 :处理 前端表单/URL参数 ↔ Java方法参数 之间的转换。
你可以把它理解为"参数解析员"------前端传字符串格式的日期(比如2026-02-01),后端Controller方法要接收Date/LocalDate类型时,Spring能正确识别,不会报格式错误。 - 能力范围 :
- 只针对"日期类型"的参数解析;
- 仅管"前端→后端"的参数传递,不管后端返回JSON给前端的格式。
第二步:核心区别对比(表格更清晰)
| 对比维度 | @JsonField(fastjson) | @DateTimeFormat(Spring) |
|---|---|---|
| 所属框架 | 阿里巴巴fastjson/fastjson2 | Spring 核心框架 |
| 核心场景 | JSON ↔ Java对象(序列化/反序列化) | 前端表单/URL参数 ↔ Java方法参数(参数绑定) |
| 作用方向 | 双向(Java→JSON、JSON→Java) | 单向(仅前端→后端解析) |
| 适用字段类型 | 所有Java字段(日期、字符串、数字等) | 仅日期类型(Date、LocalDate、LocalDateTime) |
| 核心能力 | 日期格式、字段名映射、序列化开关、排序等 | 仅日期格式解析(前端传参) |
第三步:代码演示(可直接运行)
我用Spring Boot项目写完整示例,你能直观看到两者的效果:
1. 准备依赖(pom.xml)
xml
<!-- Spring Boot Web(提供Controller、@DateTimeFormat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.2</version>
</dependency>
<!-- fastjson2(提供@JsonField) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.45</version>
</dependency>
<!-- Lombok(简化代码,可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 演示@JsonField(JSON序列化/反序列化)
实体类(控制JSON的日期格式和字段名):
java
import com.alibaba.fastjson2.annotation.JsonField;
import lombok.Data;
import java.util.Date;
@Data
public class User {
private Long id;
private String name;
// @JsonField:JSON字段名改为create_time,日期格式指定为yyyy-MM-dd HH:mm:ss
@JsonField(name = "create_time", format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
Controller测试JSON序列化:
java
import com.alibaba.fastjson2.JSON;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class JsonFieldDemoController {
@GetMapping("/testJsonField")
public String testJsonField() {
// 1. 创建Java对象
User user = new User();
user.setId(1L);
user.setName("张三");
user.setCreateTime(new Date()); // 当前时间
// 2. 序列化为JSON(@JsonField生效)
String jsonStr = JSON.toJSONString(user);
return "JSON结果:" + jsonStr;
}
}
测试结果 :
访问 http://localhost:8080/testJsonField,返回的JSON如下(日期格式是指定的,字段名也改了):
json
{"create_time":"2026-02-01 15:30:00","id":1,"name":"张三"}
如果去掉@JsonField,默认会返回时间戳(比如1748899800000),字段名是createTime。
3. 演示@DateTimeFormat(前端参数解析)
Controller测试参数解析:
java
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
public class DateTimeFormatDemoController {
// 前端传字符串日期,后端用Date接收(@DateTimeFormat指定格式)
@GetMapping("/testDateTimeFormat")
public String testDateTimeFormat(
@RequestParam("name") String name,
// 关键:指定前端传的日期格式为yyyy-MM-dd
@RequestParam("birthday") @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday
) {
return "姓名:" + name + ",生日:" + birthday;
}
}
测试方式&结果 :
访问 http://localhost:8080/testDateTimeFormat?name=李四&birthday=2000-01-01,返回:
姓名:李四,生日:Sat Jan 01 00:00:00 CST 2000
如果去掉@DateTimeFormat,访问同样URL会报400错误(Spring默认不认识2000-01-01格式)。
4. 实际开发:两者结合使用
前端既传日期参数(需要@DateTimeFormat解析),后端返回JSON又要指定日期格式(需要@JsonField),这是最常见的场景:
java
import com.alibaba.fastjson2.annotation.JsonField;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class Order {
private Long orderId;
private String orderNo;
// @DateTimeFormat:解析前端传的日期参数(比如2026-02-01)
// @JsonField:返回JSON时,日期格式为2026-02-01,字段名改为order_date
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonField(name = "order_date", format = "yyyy-MM-dd")
private Date orderDate;
}
总结
- @JsonField(fastjson) :管
JSON↔Java对象的转换,能控制日期格式、字段名等,双向生效,适用所有字段类型。 - @DateTimeFormat(Spring) :管
前端参数↔Java方法参数的解析,仅处理日期类型,单向生效(前端→后端),不影响JSON返回格式。 - 实际开发中两者常结合:
@DateTimeFormat解析前端传入的日期,@JsonField控制返回给前端的JSON日期格式。
一、先明确两者的核心定位(本质区别)
| 工具/注解 | 核心定位 | 作用范围 |
|---|---|---|
DateFormat |
Java基础的通用日期格式化工具,处理「日期对象 ↔ 字符串」的相互转换 | 所有场景(Web/非Web、JSON/非JSON) |
@JSONField |
FastJSON的JSON序列化/反序列化专用注解,仅控制「对象 ↔ JSON」时的日期格式 | 仅JSON交互场景(前后端JSON传参/返回) |
二、分场景对比:哪些事DateFormat能做,@JSONField做不了?
场景1:HTTP请求参数的日期转换(GET/POST表单)
比如你之前的/test/date?date=2026-02-01------这是URL参数/表单参数 (不是JSON),前端传的是普通字符串,后端要转成Date对象:
- ✅
DateFormat能做:你配置的addFormatters里的SimpleDateFormat("yyyy-MM-dd")就是干这个的,SpringMVC用它把URL里的字符串转Date; - ❌
@JSONField做不了:@JSONField只认JSON格式的参数,对URL/表单参数完全无感,哪怕你在实体类加了@JSONField(format = "yyyy-MM-dd"),这个接口依然会报日期格式错误。
场景2:非Web场景的日期处理
比如本地程序把Date对象转成字符串写入日志/文件,或读取文本里的日期字符串转Date:
- ✅
DateFormat能做:这是它的"本职工作",通用且无场景限制; - ❌
@JSONField做不了:脱离了JSON交互场景,这个注解就是"摆设",完全不生效。
三、哪些事两者都能做(重叠场景)
仅在JSON序列化/反序列化 的日期处理场景,两者都能实现,只是@JSONField更便捷:
场景1:后端返回JSON时,Date字段转指定格式字符串
比如实体类的createTime字段是Date类型,想返回"createTime":"2026-02-01 15:00:00":
- 用
DateFormat:需要手动格式化(比如在Controller里new SimpleDateFormat().format(user.getCreateTime())),代码繁琐; - 用
@JSONField:直接在字段上加@JSONField(format = "yyyy-MM-dd HH:mm:ss"),FastJSON自动序列化,无需手动处理,更简洁。
场景2:前端POST JSON参数,日期字符串转后端Date字段
比如前端传{"createTime":"2026-02-01"},后端实体类的createTime是Date类型:
- 用
DateFormat:需要在Controller里手动解析(new SimpleDateFormat().parse(jsonStr)),再赋值给实体类; - 用
@JSONField:在实体类字段上加@JSONField(format = "yyyy-MM-dd"),FastJSON自动把JSON里的字符串转Date,无需手动解析。
四、举个实际例子,帮你彻底分清
假设你有两个接口:
java
@RestController
public class TestController {
// 场景1:GET请求(URL参数)------只能用DateFormat
@GetMapping("/test/date")
public String testDate(@RequestParam Date date) {
return "URL参数转换的Date:" + date;
}
// 场景2:POST请求(JSON参数)------@JSONField生效
@PostMapping("/test/user")
public User testUser(@RequestBody User user) {
return user; // 返回JSON时,createTime会按@JSONField格式展示
}
}
// 实体类
public class User {
private String name;
// 仅对JSON交互生效,对URL参数无效
@JSONField(format = "yyyy-MM-dd")
private Date createTime;
}
- 访问
/test/date?date=2026-02-01:必须靠addFormatters里的DateFormat,@JSONField完全不管; - POST
/test/user传{"name":"张三","createTime":"2026-02-01"}:@JSONField自动把字符串转Date,返回JSON时又自动转成2026-02-01格式。
总结
- 核心区别 :
DateFormat是通用日期格式化工具,覆盖所有场景;@JSONField仅针对JSON交互的日期格式,是"专用工具"; - 重叠场景 :仅JSON序列化/反序列化时的日期转换,两者都能做,
@JSONField更便捷; - 不可替代场景 :URL/表单参数的日期转换、非Web场景的日期处理,只能用
DateFormat(或Spring的日期格式化器)。
简单记:JSON相关的日期格式用@JSONField,非JSON的日期转换用DateFormat。