Spring Boot 到底解决了什么问题

在 Spring Boot 出现之前,用 Spring 搭一个 Web 项目要先配 web.xml、写一堆 XML、管理各种依赖版本,一个「能跑起来的空壳」就能消耗半天。Spring Boot 的核心贡献不是新框架,而是约定优于配置:它把常见场景的默认值全部内置,你只需要声明要开哪些能力。

具体来说,它靠三件事达成这个目标:一是 starter 依赖,用一个坐标拉进一整套经过验证的库版本;二是自动装配,启动时根据类路径里存在什么,自动帮你把对应组件装配好;三是内嵌容器,不用单独装 Tomcat,直接 java -jar 就能跑。

理解 Spring Boot 的关键,是把它看成「Spring 的脚手架 + 默认配置集合」。你随时可以用自定义配置覆盖默认值,但绝大多数时候,默认值已经够用。
本文核心知识点
  • starter一个依赖坐标聚合一组库,版本由父 POM 统一管理。
  • 自动装配启动时按类路径条件判断,自动注册所需的 Bean。
  • 依赖注入对象不再自己 new,而是由容器创建并通过构造方法注入。
  • REST 接口用注解把 Java 方法映射到 HTTP 路径,请求响应自动序列化。
Spring Boot 示意:中央的抽象螺旋弹簧连接周围可插拔的模块方块,右侧延伸出管道连向圆形接口节点
Spring 提供容器与装配能力,Spring Boot 把它的使用门槛压到了几条依赖和一个启动类。

最小工程:依赖与启动类

创建一个 Spring Boot 工程最省事的办法是用官方脚手架,但手工理解依赖结构更有利于排错。最小可运行的 Web 工程只需要父 POM 和两个 starter:spring-boot-starter-web 提供 Web 与内嵌容器,spring-boot-starter-test 提供测试支持。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.4</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

然后是启动类。注意两个细节:@SpringBootApplication 是一个组合注解,它同时包含了配置类、自动装配与组件扫描三件事;而组件扫描默认从启动类所在的包开始向下递归,所以启动类必须放在最外层的包,否则控制器不会被扫到。

package com.jjy.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

常用的 starter 还有几个方向:spring-boot-starter-data-jpa 处理数据库、spring-boot-starter-validation 做参数校验、spring-boot-starter-actuator 暴露健康检查与指标。需要什么加什么,未使用的依赖不要留在工程里。

第一个 REST 接口

REST 接口的写法在 Spring Boot 里非常直观:一个加了 @RestController 的类,方法上加路径映射注解,返回值会被自动序列化成 JSON。不需要手动拼字符串,也不需要在配置里注册映射。

package com.jjy.demo.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/greetings")
public class GreetingController {

    @GetMapping("/{name}")
    public Map<String, Object> hello(@PathVariable String name,
                                      @RequestParam(defaultValue = "cn") String lang) {
        String text = "en".equals(lang) ? "Hello, " + name : "你好," + name;
        return Map.of("lang", lang, "message", text);
    }
}

启动应用后访问 /api/greetings/阿布?lang=cn,就能拿到一段 JSON。几个容易混淆的点值得记牢:@RestController 等于 @Controller 加 @ResponseBody,所以方法返回值直接作为响应体;@RequestMapping 写在类上作为前缀,写在方法上表示具体路径;HTTP 方法注解有 Get / Post / Put / Delete 等,语义上要与其对应。

  • GET 用于查询,应当幂等且不产生副作用。
  • POST 用于创建,请求体携带数据。
  • PUT 用于整体更新,PATCH 用于局部更新。
  • DELETE 用于删除,同样应当幂等。

依赖注入:把对象交给容器

依赖注入是 Spring 的灵魂。它的核心思想是控制反转:对象不再自己 new 出协作者,而是由容器负责创建并「注入」进来。带来的直接好处是解耦——控制器只依赖接口,测试时可以换成替身实现,而不用改动业务代码。

注入方式写法建议
构造方法注入把依赖写成构造参数首选:依赖明确、字段可 final、便于测试
Setter 注入在 setter 上标注解仅用于可选依赖
字段注入直接在字段上标注解尽量避免:隐藏依赖、难以单测

只要类被 @Service、@Component、@Repository 等注解标记,Spring 就会把它注册成 Bean;单构造方法时连 @Autowired 都可以省略。下面是一个标准的服务层写法:

package com.jjy.demo.service;

import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.Locale;

@Service
public class GreetingService {

    private final TimeProvider timeProvider;

    public GreetingService(TimeProvider timeProvider) {   // 构造注入,无需 @Autowired
        this.timeProvider = timeProvider;
    }

    public String greet(String name, String lang) {
        String who = name == null ? "访客" : name.trim();
        String prefix = "en".equals(lang.toLowerCase(Locale.ROOT)) ? "Hello" : "你好";
        return prefix + "," + who + ",今天是 " + timeProvider.today();
    }
}

Bean 默认是单例的,也就是整个容器里只创建一个实例,因此它必须是无状态的——不要在 Bean 里存与单次请求相关的数据,那属于方法内的局部变量。需要不同作用域时,可以显式声明为 prototype 或 request 级别,但要清楚这会带来额外的创建开销。

请求参数、请求体与统一返回

真实接口需要接收结构化数据,也需要稳定统一的响应格式。参数绑定方面,Spring 用注解区分数据来源:@RequestParam 取查询串或表单字段,@PathVariable 取路径占位符,@RequestBody 把 JSON 反序列化成对象,@RequestHeader 取请求头。

package com.jjy.demo.web;

import com.jjy.demo.service.GreetingService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/greetings")
public class GreetingController {

    private final GreetingService service;

    public GreetingController(GreetingService service) {
        this.service = service;
    }

    public record GreetRequest(String name, String lang) { }

    public record ApiResponse(int code, String message) { }

    @PostMapping
    public ApiResponse create(@RequestBody GreetRequest req) {
        String text = service.greet(req.name(), req.lang());
        return new ApiResponse(0, text);
    }
}

把响应包成一个统一结构是工程里最常见的做法,前端只需要判断 code 就能区分成功与失败。record 是 Java 16 引入的语法糖,用来声明不可变的数据载体,非常适合做请求与响应对象。如果字段很多,也可以换成普通的 DTO 类,加上 Lombok 之类的工具减少样板代码。

需要注意 JSON 字段与 Java 属性名的映射:默认按名称匹配,若不一致可以用注解指定。日期时间类型建议统一用 ISO 格式,避免不同客户端解析差异。

配置文件与多环境切换

Spring Boot 支持 properties 与 yml 两种配置格式,yml 层级清晰更受欢迎。配置文件放在 src/main/resources 下,名为 application.yml;通过 spring.profiles.active 可以切换到不同环境的配置文件,避免把生产参数硬编码进代码。

server:
  port: 8080

spring:
  application:
    name: jjy-demo
  profiles:
    active: dev

logging:
  level:
    root: info
    com.jjy.demo: debug

需要区分环境时,再补一个 application-dev.yml,把数据库地址、调试日志等开发专用配置写在里面。读取配置有两种常用方式:

  • @Value("${server.port}"):适合少量、零散的单值注入。
  • @ConfigurationProperties:把一组相关配置绑定到一个类上,类型安全、便于校验,推荐用于成组的业务配置。

一个容易被忽略的细节是配置优先级:命令行参数高于环境变量,环境变量高于配置文件,外部配置文件高于打包进 jar 的配置文件。排查「为什么线上读到的不是这个值」时,按这个顺序逐层确认,通常几步就能定位。