SpringBoot整合SpringMVC(末)

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个核心方法:

    java 复制代码
    public 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路径(不拦截)
    }
  • 关键:拦截器的路径配置很灵活,能精准控制哪些接口需要拦截、哪些跳过。

  1. WebMvcConfigurer是SpringBoot给的MVC扩展入口,通过实现该接口并重写对应方法,可定制MVC行为,无需修改框架源码;
  2. 核心扩展场景:视图跳转(addViewControllers)、参数格式化(addFormatters)、JSON转换(configureMessageConverters)、接口拦截(addInterceptors);
  3. 所有扩展都需要创建加了@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可直接用
核心用途 全局预处理(编码、跨域、粗粒度校验) 业务拦截(细粒度权限、耗时统计) 事件监听(初始化、统计、清理)

总结

  1. 过滤器:Servlet层面的全局拦截,管所有请求,适合做"粗粒度"的统一处理(如编码、全局登录校验);
  2. 拦截器:Spring MVC层面的精准拦截,只管业务接口,适合做"细粒度"的业务逻辑(如接口耗时、权限校验);
  3. 监听器:事件驱动的观察者,不干预请求流程,适合做初始化、统计、资源清理等被动操作。

实际开发中,三者常配合使用:Filter做全局编码/跨域 → Interceptor做接口权限/耗时 → Listener做应用初始化/在线人数统计。

springboot使用外置的servlet容器

核心原理说明

SpringBoot默认使用内置Tomcat,要使用外置容器,核心是:

  1. 将项目打包为war包(而非默认的jar包)
  2. 排除SpringBoot内置的Tomcat依赖
  3. 让SpringBoot应用适配外置Servlet容器的启动规范(继承SpringBootServletInitializer

一、环境准备

  1. JDK 8+(和你的外置Tomcat版本匹配,比如Tomcat 9推荐JDK8+)
  2. Maven/Gradle(这里用Maven演示)
  3. 外置Tomcat(下载地址:https://tomcat.apache.org/ ,推荐Tomcat 9)
  4. 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包
  1. 在IDEA中打开「Maven」面板(右侧),找到项目→Lifecycle→clean(清理旧包)→package(打包)。
  2. 打包完成后,在项目的target目录下会生成external-tomcat-demo.war文件。
步骤5:部署到外置Tomcat并运行
  1. 解压下载的外置Tomcat到任意目录(如D:\apache-tomcat-9.0.80)。
  2. external-tomcat-demo.war复制到Tomcat的webapps目录下(如D:\apache-tomcat-9.0.80\webapps)。
  3. 启动Tomcat:双击Tomcat目录下bin文件夹中的startup.bat(Windows)/startup.sh(Linux)。
  4. 访问测试:打开浏览器,访问http://localhost:8080/external-tomcat-demo/hello(注意路径包含war包名称)。
    • 成功的话,页面会显示:Hello, External Tomcat!
步骤6:验证(可选)
  • 关闭内置容器验证:如果直接运行启动类的main方法,会报错(因为已经排除了内置Tomcat),证明项目不再依赖内置容器。
  • 停止Tomcat:双击bin目录下的shutdown.bat(Windows)/shutdown.sh(Linux),访问接口会提示无法连接。

总结

  1. 核心配置 :将打包方式改为war,排除SpringBoot内置Tomcat依赖,添加javax.servlet-apiprovided依赖。
  2. 启动适配 :启动类必须继承SpringBootServletInitializer并重写configure方法,让外置容器能识别SpringBoot应用。
  3. 部署运行 :将打包后的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镜像,内置容器+镜像隔离,替代传统的外置容器集群;
  • 简单的小型应用:无高并发、无复杂调优需求,内置容器足够用。

总结

  1. 核心价值 :外置Servlet容器解决了企业级部署的统一运维、性能调优、兼容老系统 三大核心痛点,是生产环境的主流选择;
  2. 适用场景:多应用部署、需要精细化调优容器、与老Servlet项目共存、企业有统一容器运维规范;
  3. 对比内置容器:内置容器胜在"快"(开发测试),外置容器胜在"稳"(生产运维)。

简单来说,内置容器是"为开发者服务",外置容器是"为运维和生产环境服务"。

拓展:@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字段的排序。
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;
}

总结

  1. @JsonField(fastjson) :管JSON↔Java对象的转换,能控制日期格式、字段名等,双向生效,适用所有字段类型。
  2. @DateTimeFormat(Spring) :管前端参数↔Java方法参数的解析,仅处理日期类型,单向生效(前端→后端),不影响JSON返回格式。
  3. 实际开发中两者常结合:@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"},后端实体类的createTimeDate类型:

  • 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格式。

总结

  1. 核心区别DateFormat是通用日期格式化工具,覆盖所有场景;@JSONField仅针对JSON交互的日期格式,是"专用工具";
  2. 重叠场景 :仅JSON序列化/反序列化时的日期转换,两者都能做,@JSONField更便捷;
  3. 不可替代场景 :URL/表单参数的日期转换、非Web场景的日期处理,只能用DateFormat(或Spring的日期格式化器)。

简单记:JSON相关的日期格式用@JSONField,非JSON的日期转换用DateFormat

相关推荐
_周游2 小时前
Java8 API 文档搜索引擎_2.索引模块(程序)
java·搜索引擎·intellij-idea
小马爱打代码2 小时前
Spring Boot:邮件发送生产可落地方案
java·spring boot·后端
BD_Marathon2 小时前
设计模式——接口隔离原则
java·设计模式·接口隔离原则
空空kkk2 小时前
SSM项目练习——hami音乐(二)
java
闻哥2 小时前
深入理解 ES 词库与 Lucene 倒排索引底层实现
java·大数据·jvm·elasticsearch·面试·springboot·lucene
2 小时前
java关于引用
java·开发语言
三水不滴2 小时前
SpringBoot+Caffeine+Redis实现多级缓存
spring boot·redis·笔记·缓存
弹简特2 小时前
【JavaEE04-后端部分】Maven 小介绍:Java 开发的构建利器基础
java·maven
计算机毕设指导63 小时前
基于微信小程序的智能停车场管理系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea