解决 Spring Boot 应用中静态资源 404 问题的完整指南

在部署 Spring Boot 应用时,经常遇到静态资源(如图片、CSS、JavaScript 文件)无法访问的问题,浏览器返回 404 错误。本文将详细介绍如何诊断和解决这类问题,通过一个真实案例展示完整的排查和修复过程。

问题场景

最近在部署一个二手交易平台项目时遇到了这样的问题:

  • 后端是基于 Spring Boot 开发的应用
  • 前端通过 NGINX 反向代理访问后端 API
  • 用户访问图片资源时返回 404 错误
  • 控制台显示路径为:/mnt/projeckDisk/projeck/template/tab41_mall/src/main/resources/static/img/

访问地址类似:

1
http://服务器ip:8214/api/img/bd743b44-725a-4e66-89cb-2ba0fc38cdf6.jpg

初步分析

首先,我们需要理解项目的结构和静态资源配置:

项目结构

1
2
3
4
5
6
7
8
9
/mnt/projeckDisk/projeck/template/tab41_mall/
├── src/
│ └── main/
│ └── resources/
│ └── static/
│ └── img/
├── dist/
├── SecondStore-0.0.1-SNAPSHOT.jar
└── ...

Spring Boot 静态资源配置

在 Spring Boot 中,静态资源默认由ResourceHttpRequestHandler处理。通过继承WebMvcConfigurationSupport类并重写addResourceHandlers方法,我们可以自定义静态资源的映射规则。

原始配置代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String imagePath = System.getProperty("user.dir") + "/" + Util.IMAGE_PATH;
// 在Windows系统中,需要将路径中的 \ 替换为 / 以避免问题
imagePath = imagePath.replace("\\", "/");
System.out.println("addResourceHandlers=======>" + imagePath);
// 使用与图片上传相同的路径配置
registry.addResourceHandler("/img/**").addResourceLocations("file:/" + imagePath);
// 添加API路径的映射,以便通过/api/img访问图片
registry.addResourceHandler("/api/img/**").addResourceLocations("file:/" + imagePath);
}
}

其中Util.IMAGE_PATH常量定义为:

1
public final static String IMAGE_PATH = "src/main/resources/static/img/";

问题诊断

通过查看应用日志,发现以下错误信息:

1
java.net.UnknownHostException: mnt

这个错误表明 Spring 在解析资源路径时,错误地将路径中的mnt部分识别为主机名,试图建立网络连接而不是读取本地文件。

问题原因

问题的根本原因是路径构造不当。在原始代码中:

  1. System.getProperty("user.dir")返回的是/mnt/projeckDisk/projeck/template/tab41_mall
  2. 加上Util.IMAGE_PATH后得到/mnt/projeckDisk/projeck/template/tab41_mall/src/main/resources/static/img/
  3. 最终构造的路径为file:///mnt/projeckDisk/projeck/template/tab41_mall/src/main/resources/static/img/

虽然看起来正确,但在某些环境或 Spring 版本中,这种路径构造方式可能导致解析错误。

解决方案

为了解决这个问题,我们采用了更加规范和安全的路径构造方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.SecondStore.config;

import com.example.SecondStore.util.Util;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.io.File;
import java.nio.file.Paths;

@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 使用Paths.get()方法正确构建路径,确保跨平台兼容性
String imagePath = Paths.get(System.getProperty("user.dir"), Util.IMAGE_PATH).toString();
// 明确使用file:///协议前缀,确保Spring能够正确识别这是一个本地文件路径
String imageLocation = "file:///" + imagePath + "/";
// 在Windows系统中,需要将路径中的 \ 替换为 / 以避免问题
imageLocation = imageLocation.replace("\\", "/");
System.out.println("addResourceHandlers=======>" + imagePath);
// 使用与图片上传相同的路径配置
registry.addResourceHandler("/img/**").addResourceLocations(imageLocation);
// 添加API路径的映射,以便通过/api/img访问图片
registry.addResourceHandler("/api/img/**").addResourceLocations(imageLocation);
}
}

关键改进点

  1. 使用Paths.get()方法:这个方法能够正确处理不同操作系统的路径分隔符,提高代码的跨平台兼容性。

  2. 明确的协议前缀:使用file:///确保 Spring 将其识别为本地文件系统路径而不是网络资源。

  3. 路径结尾斜杠:确保路径以/结尾,避免路径拼接问题。

  4. 保持转义逻辑:继续保留将反斜杠替换为正斜杠的逻辑,确保在 Windows 系统中的兼容性。

NGINX 配置优化

除了修复 Spring Boot 应用中的路径问题外,还需要确保 NGINX 正确地将请求代理到应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
server {
listen 8214;
server_name localhost 服务器ip;

# 专门处理图片请求的location块
location /api/img/ {
proxy_pass http://服务器ip:8314/img/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
}

# 其他API请求的通用处理
location /api {
rewrite ^.+api/?(.*)$ /$1 break;
include uwsgi_params;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 18000s;
proxy_send_timeout 30s;
proxy_pass http://服务器ip:8314;
client_max_body_size 1024M;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
}

# 前端静态文件
location / {
root /mnt/projeckDisk/projeck/template/tab41_mall/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

部署步骤

  1. 重新构建应用

    1
    mvn clean package
  2. 上传新的 JAR 文件到服务器

  3. 确保目录结构存在

    1
    mkdir -p src/main/resources/static/img
  4. 重启应用服务

  5. 重新加载 NGINX 配置

    1
    2
    sudo nginx -t  # 测试配置文件
    sudo nginx -s reload # 重新加载配置

总结

通过本次问题解决,我们学到了几个重要经验:

  1. 路径处理要规范:使用标准库函数(如Paths.get())处理文件路径,避免手动拼接字符串。

  2. 协议前缀要明确:对于文件系统路径,明确使用file:///协议前缀。

  3. 日志是最好的调试工具:通过详细的日志输出,能够快速定位问题根源。

  4. 跨平台兼容性很重要:特别是在 Linux 和 Windows 之间切换部署时,要注意路径分隔符的差异。

  5. NGINX 配置要精细:针对不同的资源类型配置专门的 location 块,能够更好地控制请求路由。

通过上述修改,我们的 Spring Boot 应用现在能够正确处理静态资源请求,用户可以正常访问图片等文件了。