惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
The Last Watchdog
The Last Watchdog
腾讯CDC
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
Last Week in AI
Last Week in AI
量子位
雷峰网
雷峰网
宝玉的分享
宝玉的分享
美团技术团队
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
P
Privacy International News Feed
NISL@THU
NISL@THU
V
V2EX
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
IT之家
IT之家
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
V
Vulnerabilities – Threatpost
V
Visual Studio Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
Scott Helme
Scott Helme
H
Hacker News: Front Page
罗磊的独立博客
Hacker News - Newest:
Hacker News - Newest: "LLM"
K
Kaspersky official blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
The Cloudflare Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
月光博客
月光博客
人人都是产品经理
人人都是产品经理
AI
AI
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
PCI Perspectives
PCI Perspectives
A
Arctic Wolf

czp's blog

Ubuntu 下使用 zsh · HonKit 使用 VNC 来远程管理服务器 · HonKit Linux 下无限试用 JetBrains · HonKit 使用 KMS 激活 Windows · HonKit 为 KVM 中的 Windows 虚拟机启用 VirtIO · HonKit 在 Spring Boot 中处理 MissingKotlinParameterException · HonKit 用 Spring Native 拯救微服务 · HonKit 正确调试 PHP · HonKit Ubuntu 增加最大文件打开数量限制 · HonKit 将 Minecraft 服务端编译到 native · HonKit 用凝聚力购买 P 社游戏 DLC · HonKit 模拟超星网课 Android 客户端 · HonKit 模拟 Bilibili Android 客户端 · HonKit 模拟哔咔 Android 客户端 · HonKit 异步是什么 · HonKit GCP Pub/Sub push 至 IAP 保护的 Endpoint · HonKit 部署 Spring Native 到 GCP App Engine · HonKit 多个 Gradle SubProject 同时编译导致 CI/CD 内存耗尽 · HonKit Google Cloud Build 运行 DIND · HonKit 在 Google Storage 中部署 SPA · HonKit Terraform 不自动更新 CloudRun service · HonKit Telegram Bot 收不到普通群聊消息的问题 · HonKit 加密货币交易 · HonKit 挖掘以太坊 · HonKit 挖掘门罗币 · HonKit 小明学英语 · HonKit 卖程序的小女孩 · HonKit 一个测试工程师走进一家酒吧 · HonKit 我给你们讲个 TCP 笑话吧! · HonKit 我给你们讲个 UDP 笑话吧! · HonKit 格局打开 · HonKit c 语言常见未定义行为 · HonKit 在 Kotlin 使用 SpaceEngineers Remote API · HonKit Kotlin 协程 · HonKit 在 Spring Boot 中正确注册 Jackson Module · HonKit Spring Boot Jpa 使用 Google Cloud SQL · HonKit Spring Boot 无法加载 ClasspathResource 问题 · HonKit 正确打包 Spring Boot 到 war · HonKit czp's blog · HonKit 鸣谢列表 · HonKit 友链 · HonKit
Spring Boot 中配置单页应用 · HonKit
2026-04-11 · via czp's blog

本文撰写时的 Spring 版本: Spring Boot 2.1.0.RELEASE

有的时候我们不得不把一个单页应用(例如 react-router)与 Spring-Boot 后端一起打包. 但是这样就会有一个问题, 一旦用户不在首页刷新页面, 就会看到一个 404 画面, 因为服务端并没有把请求转向 index.html.

所以我们通过一些配置, 让 Spring 在找不到对应的资源文件的情况下, 将请求统统转向到 index.html, 这样用户就可以前端路由了.

我们很容易想到让 Spring 找不到资源文件时抛出一个异常然后我们配置一个 ControllerAdvice, 于是我们搜到了这么一条配置

spring.mvc.throw-exception-if-no-handler-found=true

但是我们很快就发现, 实际上 NoHandlerException 永远不会被抛出.

我们来看一下 Spring 是如何路由请求的, 我们打开 DispatcherServlet 的源码

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

默认情况下, HandlerMapping 有七个, 分别为

SimpleUrlHandlerMapping
WebMvcEndpointHandlerMapping
ControllerEndpointHandlerMapping
RequestMappingHandlerMapping
BeanNameUrlHandlerMapping
WelcomePageHandlerMapping
SimpleUrlHandlerMapping

第一个 SimpleUrlHandlerMapping 用于处理 /favicon.ico 这就是为什么目录里没有 icon, 也会有默认的站点图标.

而最下面的第二个 SimpleUrlHandlerMapping 用于处理资源文件, 而资源文件都是 lazy loading 的, 并不是像 HomePage(index.html)(硬编码) 或者 favicon.ico(硬编码) 那样是一开始就加载好的. 所以第二个 SimpleUrlHandlerMappinggetHandler 方法永远都会有一个指向所有资源文件路径的 HandlerExecutionChain 被返回回来.

因此即使目标资源文件不存在, 我们的 Handler 也不是 null, 永远也不会有 NoHandlerException 被抛出.

而在一些解决方案中, 是把默认的 ResourceHandler 关掉,

spring.resources.add-mappings=false

这样最后一个 HandlerMapping 就不会有了, 异常就会被抛出了.

但是这么做的话, 我们就要面对另一个更大的问题, 没有了默认的 ResourceHandler, 我们的资源文件就不能被正常读取, 我们需要自己编写额外写代码来让他们能被读取, 这完全是重复编码.

所以正确的解决方案只能是让最后一个 SimpleUrlHandlerMapping 在找不到目标资源的情况下, 读取 index.html 交给访问者.

我们通过一个自定义的 ResourceHandler 来实现它

@Configuration
open class SinglePageApplicationWebMvcConfiguration(
        private val resourceProperties: ResourceProperties
) : WebMvcConfigurer {
    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/**")
                .addResourceLocations(*resourceProperties.staticLocations)
                .resourceChain(true)
                .addResolver(object : PathResourceResolver() {
                    override fun getResource(resourcePath: String, location: Resource): Resource? =
                            super.getResource(resourcePath, location) ?: super.getResource("index.html", location)
                })
    }
}

注意, spring.resources.static-locations 并非是硬编码的, 而是在配置文件中可以修改的, 所以我们要从配置文件中得到它.

ResourceHandler 将监听 /** 地址(注意, RequestMappingHandlerMapping 一定是先于最后一个 SimpleUrlHandlerMapping 被执行的, 所以访问 RestFul API 的请求不会进入 ResourceHandler), 当目标资源不存在时, 将返回 index.html, 如果 index.html 也不存在, 将产生 404.

addResourceLocations 添加的资源位置, 会让 Resolver 在每个资源位置都被轮询一次, 所以不会因为用户额外添加了 static-location 而导致错误.

resourceChain(true) 设置了资源位置缓存, 例如本次访问了 index.js 并在 /public 位置被找到, 那么下次将直接从该位置读取该资源文件, Resolver 不会被执行.

这样, 我们就在不重写 Spring 默认逻辑的情况下将所有未识别的访问引导到 index.html, 从而完成了单页应用的配置.

接下去, 用户将在前端被路由, 从而看到正确的页面.