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

推荐订阅源

Forbes - Security
Forbes - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Palo Alto Networks Blog
Martin Fowler
Martin Fowler
T
Threatpost
D
Docker
S
Schneier on Security
M
MIT News - Artificial intelligence
G
Google Developers Blog
L
LINUX DO - 热门话题
J
Java Code Geeks
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
V
Vulnerabilities – Threatpost
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
人人都是产品经理
人人都是产品经理
Spread Privacy
Spread Privacy
T
Tailwind CSS Blog
爱范儿
爱范儿
阮一峰的网络日志
阮一峰的网络日志
U
Unit 42
C
CERT Recently Published Vulnerability Notes
The GitHub Blog
The GitHub Blog
Simon Willison's Weblog
Simon Willison's Weblog
NISL@THU
NISL@THU
MongoDB | Blog
MongoDB | Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Heimdal Security Blog
Recorded Future
Recorded Future
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
P
Privacy International News Feed
P
Proofpoint News Feed
O
OpenAI News
B
Blog
腾讯CDC
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
T
Tor Project blog
H
Hacker News: Front Page
Project Zero
Project Zero
Hugging Face - Blog
Hugging Face - Blog
C
Cisco Blogs
S
Security Affairs

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, 从而完成了单页应用的配置.

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