Spring Boot 简化了 Java Web 应用的构建与配置,但"开箱即用"背后仍有一套明确机制。本文集中讲解四个常见问题:可执行 JAR 为什么能直接启动、后端如何配置 CORS、静态资源如何映射,以及 @ConfigurationProperties 和 @Value 应该如何选择。
一、Spring Boot JAR 为什么可以直接运行?
普通类库 JAR 通常只包含当前项目编译后的类,第三方依赖需要额外放入 classpath。Spring Boot Maven 或 Gradle 插件会重新打包应用,将代码、依赖和启动器放入同一个可执行 JAR。
典型结构如下:
app.jar
├── META-INF/MANIFEST.MF
├── org/springframework/boot/loader/...
└── BOOT-INF
├── classes # 项目类和资源
└── lib # 第三方依赖 JAR
MANIFEST.MF 中通常包含:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.Application
Main-Class 并不是业务启动类,而是 Spring Boot Loader 提供的 JarLauncher;Start-Class 才是包含 main() 方法的应用类。
执行:
java -jar app.jar
JVM 首先运行 JarLauncher。它建立能够访问 BOOT-INF/classes 和 BOOT-INF/lib 中嵌套依赖的类加载环境,然后调用 Start-Class 的 main() 方法。随后 SpringApplication.run() 创建 Spring 容器并启动内嵌 Tomcat。因此,一个文件中同时具备应用代码、依赖、加载器和 Web 容器,部署时不需要单独安装 Tomcat。
整个过程可以概括为:
java -jar app.jar
↓
JVM 读取 MANIFEST.MF
↓
执行 Main-Class,也就是 JarLauncher
↓
建立嵌套 JAR 类加载环境
↓
调用 Start-Class.main()
↓
创建 Spring 容器并启动内嵌服务器
这也解释了为什么不能简单地把 Spring Boot JAR 当成普通依赖 JAR 使用:它经过了专门的目录重组。构建插件一般还会保留一个未重新打包的原始 JAR,具体名称取决于构建配置。排查"本地能运行、打包后不能运行"时,应先检查使用的是否为重新打包后的文件,再检查清单中的启动类和 BOOT-INF/lib 依赖是否完整。
可以通过以下命令观察内部结构:
jar -tf target/app.jar
unzip -p target/app.jar META-INF/MANIFEST.MF
二、Spring Boot 如何支持跨域请求?
浏览器的同源策略会限制页面脚本访问不同协议、域名或端口的资源。例如前端运行在 http://localhost:5173,后端运行在 http://localhost:8080,二者端口不同,属于跨域。
CORS 的本质是后端在响应中加入 Access-Control-Allow-* 等响应头,告诉浏览器哪些来源、方法和请求头可以访问。它不是关闭浏览器的安全策略。
1. 局部配置:@CrossOrigin
只允许某个 Controller 跨域时,可以使用:
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "https://www.example.com")
public class UserController {
@GetMapping
public List<String> list() {
return List.of("Tom", "Jerry");
}
}
也可以把 @CrossOrigin 放在单个方法上。它适合少量特殊接口,但接口较多时容易分散配置。
2. 全局配置:WebMvcConfigurer
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
其中 maxAge 表示浏览器缓存预检结果的时间。对于非简单跨域请求,浏览器通常先发送 OPTIONS 预检请求,确认服务器允许后再发送真实请求。
如果开启 allowCredentials(true) 以携带 Cookie 等凭证,不能同时把 allowedOrigins 配置为 "*",应明确列出可信域名。生产环境也不应为了快速解决报错而无条件允许所有来源。
三、Spring Boot 如何处理静态资源映射?
Spring Boot 默认会从 classpath 下的以下目录提供静态资源:
src/main/resources/static
src/main/resources/public
src/main/resources/resources
src/main/resources/META-INF/resources
例如文件:
src/main/resources/static/images/logo.png
默认可通过下面的地址访问:
http://localhost:8080/images/logo.png
Spring MVC 使用 ResourceHttpRequestHandler 处理这些请求,默认路径模式是 /**。需要统一增加访问前缀时,可以配置:
spring:
mvc:
static-path-pattern: /assets/**
此时资源访问路径变成 /assets/images/logo.png。如果希望更换资源存放位置,可以使用:
spring:
web:
resources:
static-locations: classpath:/web/,file:/opt/app/uploads/
还可以通过 Java 配置映射外部目录:
@Configuration
public class ResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:/opt/app/uploads/");
}
}
四、@ConfigurationProperties 和 @Value 的区别
两者都能读取 Spring Environment 中的配置,但定位不同。
1. 使用 @Value
@Value 适合注入少量、独立的配置:
payment:
timeout: 3s
@Component
public class PaymentClient {
@Value("${payment.timeout:5s}")
private Duration timeout;
}
它写法直接,支持占位符和 SpEL,但配置一多,注解会散落在多个类中,不便于统一管理和校验。
2. 使用 @ConfigurationProperties
对于一组成体系的配置,更适合绑定为类型安全对象:
payment:
base-url: https://pay.example.com
timeout: 3s
retry-count: 2
@Validated
@ConfigurationProperties(prefix = "payment")
public record PaymentProperties(
@NotBlank String baseUrl,
@NotNull Duration timeout,
@Min(0) int retryCount) {
}
在启动类上启用扫描:
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
}
它支持层级对象、集合、类型转换、校验和宽松绑定。例如 base-url 可以绑定到 Java 的 baseUrl,环境变量也可以按规则映射。
除了 @ConfigurationPropertiesScan,还可以通过 @EnableConfigurationProperties(PaymentProperties.class) 明确注册配置类。前者适合统一扫描项目内多个配置对象,后者适合显式启用少量配置。配置类注册为 Bean 后,应通过构造器注入到业务组件中,而不是到处重复读取字符串配置:
@Service
public class PaymentService {
private final PaymentProperties properties;
public PaymentService(PaymentProperties properties) {
this.properties = properties;
}
}
使用专门的配置对象还能在应用启动时集中发现缺失值或非法值。相比之下,多个类各自使用 @Value,很容易出现键名重复、默认值不一致和配置含义分散的问题。