我们先从一个很具体的场景说起。
以前用 SSH 组合(Spring MVC + Spring + MyBatis)做个后台接口,光是配置文件就能让人整理一上午。web.xml 要配监听器、配 Spring 容器加载路径、配 DispatcherServlet 拦截规则,Spring 配置文件里要配数据源、配事务管理器、配 MyBatis 的 SqlSessionFactory,然后还要把 mapper 扫描进去,每一步都得小心翼翼,漏一个标签整个项目直接启动失败,而且报错信息还不直观,经常是 Tomcat 都启动了,结果请求一进来才告诉你哪个 bean 没装配。后来切到 Spring Boot,同样一个需求,核心代码不过几十行,配置用 application.properties 或 application.yml 就能搞定,依赖只要引入 starter 包,运行方式从"部署到 Tomcat 再访问"变成了"直接执行 main 方法"。这篇是 Spring Boot 从入门到精通系列的第一篇,适合刚接触 Spring Boot 的初学者、想从传统 SSH/SSM 迁移过来的开发者,也适合那些已经把 Spring Boot 用起来但一直没搞懂"为什么一个注解就能自动装配"的写码人。讲的内容不多,但全都是用得上的核心认知。
1. 我决定学 Spring Boot 前的那些痛点
1.1 配置地狱不是开玩笑的
我第一次用 Spring 写项目是在大学时期做课程设计,踩过的坑至今记忆犹新。没错,那时候还没有 Spring Boot,是 Spring 3.x 的时代。一个最简单的 SSM 项目,需要创建一大堆 XML 配置文件。从 applicationContext.xml 到 springMVC.xml,再到 mybatis-config.xml,每个文件都要手敲大段大段的 bean 定义。当时还没有现在这么方便的自动补全,一不小心写错一个 bean 的 class 路径,或者漏了一个属性值,启动日志只会在控制台打出一长串BeanCreationException,然后让你自己去从二十多行堆栈信息里猜哪里出了问题。
那时候同学们之间流传着一个段子:配置一个 Spring 项目需要的 XML 行数,和代码行数差不多。看代码还看半天,发现大部分时间其实都在配置文件里折腾。后来 Spring 推出了@Configuration、@Bean这类注解式配置,情况好转了不少,但数据库连接池、事务、JSON 序列化、日志框架、拦截器这些东西依然要写一堆配置类或者依赖第三方工具去辅助集成,还是麻烦。
1.2 Spring Boot 出现后,事情简化的不只是代码量
Spring Boot 的出现本质上解决的是"集成成本"的问题。它不管你业务代码怎么写,而是把你原来手动完成的"环境组装"工作拿过去做了。你只要在 pom.xml 里加一个spring-boot-starter-web,它就能自动把内嵌 Tomcat、Spring MVC、Jackson JSON 处理这些组件统统给你配好;你只要在 application.properties 里写几句话配置数据源,它就能通过自动配置机制创造出一个可以用的数据源 bean。
用大白话解释:以前你是去超市买菜回家自己做饭,Spring Boot 现在是给你一套带基础配料的预制菜,你只需要自己加主菜下锅,几分钟就能出锅。不用再纠结用什么牌子的油、放多少克盐,因为基础的调味它已经帮你做好了。
1.3 从实际项目看,Spring Boot 到底改变了什么
在公司做过的几个项目中,从传统 SSM 迁移到 Spring Boot 之后,感受最深的有这几点:
- 启动方式简化:项目不用再打成 war 包丢进 Tomcat 的 webapps 目录,一个
java -jar命令就能跑起来,这在部署环节省了大量的人力。 - 配置管理更清晰:Spring Boot 支持 application.yml 中多环境配置(开发、测试、生产),通过
spring.profiles.active一行配置切换环境,不用再像以前一样维护多套 XML 文件。 - 生态集成开箱即用:连接 Redis、接入消息队列、集成定时任务,都只需要引入对应的 starter 依赖,再写极少量的配置就能工作。
当然,Spring Boot 也不是银弹。它帮你隐藏了很多底层细节,这也导致很多新手只停留在"会用"的阶段,出了问题根本不知道如何去排查。所以这个系列我不会只讲怎么做,还会把背后的机制拆开讲透。第一篇咱们先把地基打好,从环境准备到跑通一个最基础的项目。
2. 搭建你的第一个 Spring Boot 应用
2.1 环境准备:JDK、Maven、IDE 的版本选型
开始动手之前,先把环境搞清楚。这里我给出的版本是基于我自己长期使用的组合,稳定性优先。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 或 17 | Spring Boot 2.x 支持 JDK 8 以上,Spring Boot 3.x 要求 JDK 17 以上。建议新项目直接上 JDK 17 + Boot 2.7 或 Boot 3.x |
| Maven | 3.6.3+ | 管理依赖和构建打包,IDE 内嵌的也行 |
| IDE | IntelliJ IDEA 或 Eclipse | IDEA 社区版就够用,收费版体验更好,不过可以自行斟酌 |
说白了,版本不需要太纠结。如果你只是想先跑通官方 Demo,JDK 8 + Spring Boot 2.x 就可以。如果你是新开项目,建议直接 JDK 17 + Spring Boot 3.x,毕竟 Boot 3 全面拥抱了 Jakarta EE,底层 Spring Framework 6 也带来了不少提升。不过注意,Boot 3 对 JDK 版本有硬性要求,JDK 8 环境下是跑不起来的。
2.2 用 Spring Initializr 创建项目的两种方式
第一种方式最直接,打开浏览器访问 Spring Initializr 官网。选好项目类型(Maven),语言(Java),Spring Boot 版本,然后填写项目元数据(Group、Artifact),再勾选你需要的依赖(Dependencies),点击 Generate 生成压缩包下载,解压后用 IDEA 打开即可。
第二种方式是直接在你用的 IDE 里创建。以 IntelliJ IDEA 为例,新建项目时选择 Spring Initializr,界面会引导你填写和网页上一样的信息。我不太建议大家为了追求 "秒建" 而去手动创建 Maven 项目再自己加依赖,因为 Initializr 生成的目录结构和依赖版本都是经过校验的,不容易出错。手动写 pom 的时候,不同 starter 之间版本兼容问题会折磨死人,而 Initializr 帮你锁定好了一个已知可用的版本组合。
提示:如果你所在网络环境访问 Spring Initializr 官网慢,可以在 IDEA 设置里把 Spring Initializr URL 替换成国内镜像地址。这不算什么大问题,但能省下不少等待时间。
2.3 从生成的代码开始:看懂主启动类
生成的项目里,有一个很关键的类,命名通常是XxxApplication,我拿一个模拟项目来举例,结构大致如此:
package com.example.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); } }这个主启动类是 Spring Boot 应用的入口。main方法里的SpringApplication.run(...)会启动整个内嵌的 Web 容器(默认 Tomcat),同时加载 Spring 容器。
@SpringBootApplication是一个组合注解,它等于@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan。简单理解就是:它告诉 Spring Boot 启动自动配置机制,并且扫描当前类所在包及其子包下的所有组件(Controller、Service、Repository 等)。所以你的 Controller 不能放在主启动类包结构之外,否则扫描不到,接口 404 你都不知道为什么。
2.4 写一个最简单的接口:先跑起来再说
创建好项目之后,我们在主启动类的子包下建一个 controller 包,然后写一个 Controller:
package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } }然后直接运行DemoApplication的main方法,控制台会出现类似下面的输出:
Tomcat started on port(s): 8080 (http) Started DemoApplication in 2.314 seconds浏览器地址栏输入http://localhost:8080/hello,你就能看到返回的字符串 Hello, Spring Boot!。到这里,一个最小的 Spring Boot 应用就跑通了。
如果端口 8080 被占用,你可以在application.properties里加一行:
server.port=8081或者用 yml 格式:
server: port: 8081改完重启,访问 8081 端口即可。这是入门第一个会踩的坑,先记住这个配置项。
3. Spring Boot 自动配置到底做了什么
3.1 一句话解释自动配置
Spring Boot 的自动配置,就是在你引入某个 starter 依赖之后,它根据 classpath 里有没有对应的类,加上你配置的属性,自动创建一组默认情况下能工作的 bean。比如你引入spring-boot-starter-web,它检测到 classpath 下有DispatcherServlet,就会自动帮你创建 DispatcherServlet 并注册到内嵌 Tomcat 中去。
这个过程怎么实现的呢?核心机制是 Spring Boot 在spring-boot-autoconfigure这个包下放了一堆以*AutoConfiguration结尾的配置类。例如WebMvcAutoConfiguration、DataSourceAutoConfiguration、RedisAutoConfiguration等。每个配置类上都标了@ConditionalOnClass、@ConditionalOnMissingBean之类的条件注解。
@ConditionalOnClass的意思是"当 classpath 下存在某个类时,这个配置类才生效",@ConditionalOnMissingBean的意思是"当容器里没有自定义的某个 bean 时,自动配置才会生效"。正是因为这两个条件注解的配合,Spring Boot 能知道什么时候该自动配置,也给了你手动覆盖的机会。
3.2 自动配置的运行流程
你可以把 Spring Boot 的自动配置理解成一个"委派"过程。当 Spring Boot 启动时,它扫描META-INF/spring.factories文件(Boot 2.x 时代)或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(Boot 2.7 之后和 Boot 3 采用的方式),拿到所有需要加载的自动配置类的全限定名,然后逐个去解析这些配置类上的条件注解。
这里有一个很多人容易误解的点:不是引入了相关依赖,自动配置就必然生效。生效的前提是条件注解的评估结果全部为 true。比如引入了spring-boot-starter-data-redis,但你自己的代码里没有提供任何 RedisTemplate 相关的 bean,自动配置才会帮你创建一个默认的RedisTemplate;一旦你手动在配置文件或代码里定义了一个RedisTemplate类型的 bean,自动配置就会退避三舍,不会覆盖你定义的内容。
3.3 一个最有代表性的自动配置场景:数据源
拿数据源举例。如果你在 pom.xml 里引入了spring-boot-starter-data-jpa或者spring-boot-starter-jdbc,并且在 application.yml 里配了数据库连接信息:
spring: datasource: url: jdbc:mysql://localhost:3306/test_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver那么DataSourceAutoConfiguration会根据 classpath 里存在的连接池实现(比如 HikariCP),利用配置的 url、username、password 自动创建出一个 DataSource bean,并且注入到 Spring 容器中。它还会顺带帮你创建JdbcTemplate、PlatformTransactionManager等常用 bean。
如果你什么都不配,Spring Boot 会默认使用一个内嵌的 H2 数据库(前提是引入了 H2 依赖),否则它会因为没有有效的数据源而报错。这也是很多新手第一次连接真实数据库时报Failed to configure a DataSource的原因——把 starter 引了,但没配数据源信息,或者配了但键值写错了。
3.4 用手动覆盖打破"黑盒"
自动配置虽然省事,但在实际开发里你还是要掌握覆盖它的方法。最常见的场景是:你想自定义 ObjectMapper(Jackson 的 JSON 处理核心),或者想自定义一个拦截器。
如果你要在 Spring Boot 里自定义拦截器,通常做法是写一个类实现WebMvcConfigurer,再手动注册拦截器。这里有个前提:你自己的配置类上不要加@EnableWebMvc注解。一旦加了,Spring Boot 的WebMvcAutoConfiguration会自动失效,等于你把整个 Spring MVC 的默认配置关掉了,静态资源映射、消息转换器这些都会"失踪"。类似这种副作用,我在实际项目中踩过不止一次。
正确的做法是:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()).addPathPatterns("/api/**"); } }这样既能使用 Spring Boot 提供的原生默认 MVC 配置,又能额外增加你自己的逻辑。记住这个原则:如需扩展配置,优先实现 WebMvcConfigurer;如需彻底推翻默认配置,再用 @EnableWebMvc。前者是日常开发的主要方式,后者是针对特殊改造需求的快捷开关。
4. 起步依赖(starter)不是一个神秘的东西
4.1 starter 的本质是什么
starter 是 Spring Boot 进行依赖聚合的一种方案。它本质上只是「一堆 pom 依赖的组合」。拿spring-boot-starter-web来说,它的 pom 里面依赖了 spring-web、spring-webmvc、spring-boot-starter-tomcat、jackson-databind、spring-boot-starter 等组件。你只要引一个 web starter,就等于把这串依赖全部引入项目。
对于开发者而言,starter 的价值不是"省去写依赖"这么简单,而是消除版本冲突的问题。每个 starter 的父亲都是spring-boot-dependencies,这是 Spring Boot 官方维护的 BOM(Bill of Materials)。BOM 里锁定了几百个常用第三方库的版本号,你引入这些库时不需要再手动写 version,就能获得一组经过兼容性测试的版本组合。
注意:如果你在 pom.xml 里手动声明了某个第三方依赖的 version,那么你的显式声明优先级高于 BOM。这意味着如果你引入的某个库版本有兼容问题,不要怪 Spring Boot,先检查自己是不是手动指定了一个不兼容的版本。
4.2 用哪个 starter,怎么选
初学者最大的困惑就是不知道项目里要引哪些 starter。我的建议是:白手起家时先引最小的集合,不要一上来就塞一堆。下面列表里前三个基本是大多数项目的标配:
| 需求 | starter | 备注 |
|---|---|---|
| Web 接口开发 | spring-boot-starter-web | 含内嵌 Tomcat、Spring MVC、JSON 序列化 |
| 数据库操作(JDBC 层面) | spring-boot-starter-jdbc | 配合 SQL 执行使用,一般搭配 MyBatis/JPA |
| 连接池 | 不用单独引 starter | Boot 默认用 HikariCP,性能最好 |
| JPA 操作 | spring-boot-starter-data-jpa | 如果你使用 JPA 方案 |
| Redis 缓存 | spring-boot-starter-data-redis | 含 Lettuce 客户端 |
| 参数校验 | spring-boot-starter-validation | 方便做 Bean Validation 校验 |
| 测试 | spring-boot-starter-test | 包含 JUnit、Mockito 等 |
选型逻辑其实不复杂:你需要什么功能,就去找对应的 starter。Spring Boot 官方提供了大量关于 starter 的文档,社区里也有各种第三方 starter(比如 MyBatis 的mybatis-spring-boot-starter)。但有一点要注意:第三方 starter 的质量参差不齐,版本发布节奏也完全不一样,引入时尽量看官方文档推荐的版本号,别用最新版冲进去。
4.3 排除依赖和瘦身
用 starter 的另一个麻烦是:引来的依赖里,有些你用不上,而且还会造成冲突。例如spring-boot-starter-web默认引入了 Jackson 作为 JSON 序列化工具,但如果你项目里使用的是 Fastjson,或者 Gson,又不想要 Jackson,你可以在 spring-boot-starter-web 的依赖声明里排除掉 jackson 相关组件。在 pom.xml 中可以这样写:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency>不过我不建议新手一上来就替换默认的 JSON 库。Jackson 作为 Spring Boot 的默认 JSON 处理工具,和 Spring MVC 的适配是非常顺畅的。除非团队明确要求统一 JSON 规范,否则折腾排除依赖带来的收益非常有限,反而可能因为缺了某个转换器导致接口返回格式异常。
4.4 处理版本冲突的实战思路
版本冲突是最容易劝退新手的点。出现这类问题的典型表现是:项目能编译但运行报NoClassDefFoundError、NoSuchMethodError、或者ClassNotFound之类的错误。
遇到这种问题,第一步是到 IDEA 的 Maven 面板里查看依赖树。在右侧边栏打开 Maven 工具窗,找到当前项目,点击 "Show Dependencies" 或者运行mvn dependency:tree,看看到底是哪个依赖把冲突的版本带进来的。第二步是针对冲突的坐标在 pom.xml 里手动排除掉旧版本,或统一声明一个版本号。 注意一个原则:依赖版本以你显式声明的为主,最高版本不一定是对的选择。Spring Boot 的 BOM 里选用的不是一定最新,而是经过大量测试的稳定版本。第三方 starter 如果要求你引入某个版本,尽量按它的要求来,而不是自行升级到最新版。
5. 配套配置与日志输出:让应用按照你想要的方式运行
5.1 application.yml 的常用配置项和分层思想
Spring Boot 的配置文件默认是有优先级概念的:在 classpath 下的application.yml是最基础的,然后可以通过启动命令--spring.profiles.active=dev指定当前激活的 profile,进而加载application-dev.yml里的配置。没有 profile 激活时,只读application.yml。
实际项目中,我推荐至少维护三个环境的配置:application-dev.yml(开发环境)、application-test.yml(测试环境)、application-prod.yml(生产环境)。公共项(应用名、端口、编码、日志格式)放在基础配置里,各环境有差异的项(数据库地址、Redis 地址、日志级别)放到各自的 profile 配置文件里。
以下是一个比较常用的基础配置样例:
spring: application: name: my-service server: port: 8080 logging: level: root: info com.example.demo: debug这里也稍微说下 console 输出时的日志颜色问题,控制台默认情况下有关键信息着色,不要把这个当成什么错误,只是日志美化功能。真正需要区分的是logging.level.root这种日志级别控制。
5.2 端口动态配置的使用场景
在本地开发时,固定端口是合适的。但在自动化测试或者微服务环境里,固定端口经常导致端口冲突。Spring Boot 支持随机端口配置:
server: port: ${PORT:8080}上面这种方式更实用。它表示:读取环境变量 PORT,如果该环境变量不存在,则默认使用 8080。这样在容器部署时,你只需要在环境变量里设置 PORT 就能控制端口,而不需要重新打包。这种配置组织方式在做 CI/CD 流水线时非常常见。
5.3 日志配置的一个实用小技巧
很多新手不知道,Spring Boot 将logging.level的粒度精确到了包级别。你可以只给某个包单独设置 debug 级别,而其他包保持 info 级别。比如:
logging: level: com.example.demo.mapper: debug这样设置后,MyBatis 的 SQL 日志就能输出到控制台,方便你日常调试 SQL 语句,但不会把 Spring 框架底层的噪音日志全部打印出来。生产环境不需要开 debug,避免刷屏和性能损耗。
另外,Spring Boot 3 里默认的日志框架是 Logback。如果你不想用 Logback 而是想换成别的日志框架,同样需要做排除依赖处理。不过这类需求比较少见,这里先不展开。
6. 入门阶段最常见的几个错误与排查方法
6.1 Failed to configure a DataSource:数据源配置缺失
这个错误在 Spring Boot 项目里最常见,尤其是你引入了spring-boot-starter-data-jpa或者spring-boot-starter-jdbc,但是在 application.yml 里没有配置任何数据库连接信息。Spring Boot 尝试自动配置 DataSource,classpath 下又没有内嵌数据库(H2、H2 内存库等)可供使用,于是抛出:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.遇到这个错误不用慌,按顺序排查三步:
- 确认你的 pom.xml 里是否引入了需要数据源的 starter。
- 确认 application.yml 或 application.properties 中是否写了
spring.datasource.url、username、password。 - 确认
driver-class-name里指定的驱动类是否存在于 classpath。例如 MySQL 8 要用com.mysql.cj.jdbc.Driver,MySQL 5.x 时代则用com.mysql.jdbc.Driver。如果驱动类不存在,即使 url 配了也会启动失败。
6.2 端口被占用和启动失败
启动时提示Port 8080 was already in use或者Web server failed to start,这个问题在多人同时开发一台服务器、或者你本地还有旧实例没关掉时特别常见。解决方式不复杂,主要是两种:一是直接换个端口,二是找出占用进程把它停掉。
以 Linux 或者 macOS 为例,查看占用 8080 端口的进程:
lsof -i:8080找到 PID 后:
kill -9 PID如果你是 Windows 环境,用netstat -ano | findstr 8080查看 PID,然后用taskkill /PID 目标PID /F结束进程。 不过我更推荐在项目配置里把端口做灵活处理,而不是强杀进程。毕竟有时候那个进程是别的同事正在跑的服务,你把它杀了会影响他人。
6.3 自动配置失效导致静态资源 404
还有一类问题,项目能启动,Controller 也能访问,但 static 目录下的静态资源(图片、JS、CSS)全部 404。这通常是因为某处使用了@EnableWebMvc,导致 Spring Boot 的WebMvcAutoConfiguration静态资源配置全部失效。排查的时候,搜索整个项目里有没有这个注解,如果发现是自己的自定义配置类里写了,删掉它并改用WebMvcConfigurer实现即可。
6.4 注解扫描不到:包结构放错的坑
新手最容易忽略的是包结构。Spring Boot 默认从主启动类所在的包向下扫描,如果你把一个 Controller 或者 Service 写在主启动类上级目录或者外部目录,Spring 容器根本感知不到它,于是接口返回 404,或者用@Autowired注入时直接报NoSuchBeanDefinitionException。
解决办法有两个:一是把代码都放在主启动类子包下,这是最推荐的方式;二是在主启动类上用@ComponentScan手动指定需要扫描的包路径,但这一步容易覆盖默认扫描行为,不建议新手使用。
6.5 配置文件格式不当导致的解析异常
有时候 application.yml 里缩进错乱、某个值没加引号,会导致启动时报Failed to bind properties under 'spring.datasource'之类的问题。这里有个小窍门:YAML 文件对缩进极其敏感,千万不要用 Tab,统一用空格缩进。字符串值最好加引号,尤其是包含特殊字符(冒号、井号、特殊符号)的字符串。一对一配键值时,key: value冒号后面必须有一个空格,否则解析成字符串的一部分,最终赋值和你预期完全不一样。
我还见过同学把 application.properties 内容复制到 application.yml 里,一行一行复制没毛病,但.properties里的.分隔符在 yml 里要转成层级缩进。比如spring.datasource.url=xxx在 yml 里是:
spring: datasource: url: xxx这个转换经常被忽略,导致配置不生效,排查起来容易走弯路。
7. 让这个项目有"工程感":加一个 REST 接口的典型流程
7.1 分层:Controller、Service、Mapper 的职责划分
跑通 Hello World 之后,我们需要把项目从"演示代码"升级为"工程代码"。第一步是分清楚三层架构中每个层级的职责。
- Controller 层:负责接收 HTTP 请求,解析参数,调用 Service,返回统一封装结果。
- Service 层:负责业务逻辑,处理事务、校验数据、编排仓储层接口。
- Mapper 层(数据访问层):负责和数据库打交道,执行 SQL 或者通过 JPA 操作实体。
不少初学者喜欢把业务逻辑直接堆在 Controller 里,图个方便。但做一段时间你就会发现,这样做不仅 Controller 类越来越臃肿,而且将来加事务处理时寸步难行。因为事务边界通常应该放在 Service 方法上,如果逻辑都在 Controller,你只能把事务加到 Controller 方法上,这在 MVC 架构里显得不伦不类。
7.2 一个典型的新增接口长什么样
假设我们要做一个用户注册的功能。Entity 定义先建一个简单的用户表对应类:
package com.example.demo.entity; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // 省略 getter/setter }如果你用的是 JPA,Repository 层可以这样写:
package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }Service 层处理注册逻辑:
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } @Transactional public User register(String username, String password) { if (userRepository.findByUsername(username) != null) { throw new RuntimeException("用户名已存在"); } User user = new User(); user.setUsername(username); user.setPassword(password); return userRepository.save(user); } }Controller 层接收参数和返回结果:
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping("/register") public User register(@RequestBody User request) { return userService.register(request.getUsername(), request.getPassword()); } }这样一个简单的注册接口就完成了。你对 Spring Boot 的"自动依赖注入"会有一个直观感受:UserService中没有写new UserRepository(),而是通过构造器注入拿到一个已有的 bean,这是 Spring IoC 的核心思想。
7.3 构造器注入的好处
我在项目里统一使用构造器注入,而不是字段@Autowired。原因是:
- 构造器注入能清晰表达这个类的依赖有哪些,代码读完一眼就明白。
- 方便单元测试,Mockito 之类工具可以直接把 mock 对象传入构造器。
- 能防止循环依赖的问题暴露在运行期。如果两个 Bean 互相依赖,构造器注入会在启动时就报错,而字段注入往往要等到调用时才发现问题。
当然,字段@Autowired写起来更短,有时候我也图省事在简单场景里用一下,但一旦类里的依赖超过两个,还是老老实实写构造器注入。
7.4 参数校验:别让非法数据闯进来
上面的例子没有做参数校验,如果 username 为空或者 password 长度太短,就直接打数据库了。实际项目中一定要加校验。Spring Boot 提供了spring-boot-starter-validation,可以用注解声明校验规则:
public class UserRegisterRequest { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3到20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 32, message = "密码长度必须在6到32之间") private String password; // getter/setter }Controller 里加@Valid,启动校验:
@PostMapping("/register") public User register(@Valid @RequestBody UserRegisterRequest request) { return userService.register(request.getUsername(), request.getPassword()); }这样参数不合法时 Spring MVC 会自动抛出异常并返回 400,不用你在方法里层层判断。更规范的做法是配合一个全局异常处理器@RestControllerAdvice,把校验失败的信息统一格式化为 JSON 返回给前端。这块后面系列文章会详细展开,这篇先做个铺垫。
8. 第一篇的最后一站:调试与日常开发小技巧
8.1 热部署:改代码不用重启
每次都手动重启项目,哪怕是快,时间也白白浪费。Spring Boot 提供了 devtools 依赖,引入后修改代码可以自动重启,当然真正意义上的热更新是 JVM 层面的,devtools 靠的是类加载器机制,已经足够日常开发使用。
在 pom.xml 里加入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>IDEA 中还需要开启自动编译功能。具体操作是:Settings -> Build, Execution, Deployment -> Compiler,勾选 Build project automatically;然后按Ctrl + Shift + A,搜索Registry,打开后勾选compiler.automake.allow.when.app.running。这样修改代码并保存后,应用会自动重启。
devtools 只作用在本地开发阶段,打包生产 jar 时不会打入(scope 是 runtime 且 optional 标记,实际部署时基本不会带着它)。这属于一个开发期工具,别忘了这点。
8.2 读取配置值的方式
业务代码里经常需要读取自定义配置项,比如第三方接口的 base-url、某些开关的开关值等。最简单的做法是:
@Value("${my.config.url:https://default-url.com}") private String baseUrl;@Value里冒号后面的写法表示默认值。当你需要有一组前缀相同的配置项时,用@ConfigurationProperties更优雅:
package com.example.demo.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "my.config") public class MyConfigProperties { private String url; private String token; private boolean enabled; // getter/setter }然后在 application.yml 里:
my: config: url: https://api.example.com token: abc123 enabled: true启动时 Spring Boot 会把my.config前缀下的配置绑定到这个类的属性上。这种方式比满屏的@Value更整洁,但要注意一点:属性名匹配会有松绑定规则,url对应url,token对应token,字符串校验不区分类字段是骆驼拼写还是下划线风格,不过这是受默认规则控制的。阅读 Spring Boot 文档时重点看relaxed binding这一节,能避免不少灵异现象。
8.3 使用 curl 快速验证接口
调试 Spring Boot 接口时,浏览器访问 GET 接口还行,遇到 POST、PUT、DELETE 就不太方便了。我最常用的还是 curl 命令。比如验证注册接口:
curl -X POST http://localhost:8080/register \ -H "Content-Type: application/json" \ -d '{"username":"testuser","password":"123456"}'返回结果就是 JSON 格式。学会举一反三,用同样的方式可以快速测试所有 REST 接口。这也是做接口联调时最直接的手段,不依赖 Postman 等图形化软件。
我自己在日常开发中也用 Postman 收集接口调试,但 curl 在无界面的服务器环境里尤其有用,特别是排查线上环境接口是否可达时,一条 curl 就解决问题。
8.4 单元测试的引入
Spring Boot 项目标配测试框架,不过很多人写项目时从没写过测试。第一个项目的测试没必要特别复杂,跑一个核心 Service 的测试就足够建立信心。给UserService写测试时,可以借助 Mockito mock 掉UserRepository。这也是构造器注入好用的地方之一。
package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; @Test void testRegisterWithDuplicateUsername() { when(userRepository.findByUsername("testuser")).thenReturn(new User()); UserService userService = new UserService(userRepository); assertThrows(RuntimeException.class, () -> userService.register("testuser", "123456")); } }这里不需要启动 Spring 容器,只需要UserService和它的依赖 Mock 对象即可。当你有了一定的工程意识后,这样的测试会让重构和后续扩展都更有底气。
9. 这一篇的收获和下一篇的方向
我把第一篇的重点压缩成了四个认知:第一,Spring Boot 简化的是配置和集成,但它没有改变 Spring 的核心编程模型;第二,自动配置是理解一切现象的钥匙,你要知道什么时候它在起作用、什么时候它退场;第三,起步依赖帮你管理依赖版本,但排除依赖、调整 version 时会带来不可预知的冲突;第四,遇到问题时不要乱试,按配置-依赖-扫描路径-端口这些顺序去排查,多数入门问题都能解决。
对于这些要点,我自己的实践心得是:不管用什么版本、什么框架组合,"能跑起来"是第一目标。先把项目跑通,再回头去看自动配置的源码和官方文档,比一开始就陷入各种理论要高效得多。毕竟 Spring Boot 的官方文档很完善,等你知道自己要找什么了再去查,事半功倍。这个系列是从零到精通的路程,后面关于配置文件的高级玩法、全局异常处理、接口文档集成、与数据库框架结合、以及如何把项目安全地部署上线,还会有很多具体的内容展开。这一篇的内容如果你都跑通了,下一站见吧。我的建议很简单:别看文章多,真正上手敲一遍,比读十篇教程都强。