开篇:别急着敲代码,先听听“过来人”的血泪史
我见过太多新手朋友,打开IDE,信心满满地写下 System.out.println("Hello World"),结果还没等到输出,一片红得刺眼的报错先糊脸上了。有的说是JDK版本不对,有的说是Maven仓库炸了,还有的看着那一长串 NullPointerException 和 ClassNotFoundException 怀疑人生。
其实,Spring Boot 并不是什么高不可攀的黑魔法,它只是把原本需要我们手动繁琐配置的“体力活”,变成了几行注解就能搞定的“魔法”。但魔法是有代价的——如果你不懂它的原理,当魔法失灵时(比如依赖冲突),你连鬼在哪都找不到。
今天,我们不讲那些枯燥的官方文档复制粘贴,咱们像老朋友聊天一样,把这些坑一个个填平,顺便把那个让人闻风丧胆的 IoC(控制反转)讲得连你家楼下卖菜的大爷都能听懂。
第一章:环境配置的“拦路虎”——JDK 与 Maven 的那些坑
很多教程一上来就让你 mvn spring-boot:run,但你往往连第一步 java -version 就卡住了。
1.1 JDK 版本的选择:别太激进,也别太保守
Spring Boot 2.7.x 及更低版本支持 JDK 8 和 11,但 Spring Boot 3.x 强制要求 JDK 17 及以上。
新手常见错误:
装了 JDK 17,结果用的还是 Spring Boot 2.7,编译报错说
requires Java 17。 或者装了 JDK 8,强行上 Spring Boot 3,直接运行报错UnsupportedClassVersionError。
解决方案: 去 Oracle 官网 或 Adoptium 下载 LTS(长期支持)版本。推荐初学者直接使用 JDK 17 搭配 Spring Boot 3.2+,这是目前最稳定的组合。
安装完后,在终端执行:
java -version
javac -version
确保两者版本一致。如果显示 1.8.0_xxx,说明你还在用 Java 8,需要去环境变量里改 JAVA_HOME 指向 JDK 17 的路径。
1.2 Maven:那个总是在下载依赖时卡死的家伙
Maven 是国内开发者最头疼的工具之一,因为默认的中央仓库是国外的,下载速度慢到怀疑人生。
必做配置:更换阿里云镜像
在你的 Maven 安装目录下的 conf/settings.xml 文件中,找到 <mirrors> 节点,加入以下内容:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
这一步能解决 90% 的“下载依赖超时”问题。
IDEA 中的配置: 如果你用的是 IntelliJ IDEA,最好直接在 IDEA 设置里配置 Maven:
Settings->Build Tools->MavenMaven home directory: 指向你本地安装的 MavenUser settings file: 指向刚才修改过的settings.xml
第二章:第一次接触 Spring Boot——Hello World 的正确姿势
别再用传统的 Spring MVC 那一套了(web.xml、spring-mvc.xml、dispatcher-servlet.xml… 想想就头大)。Spring Boot 的核心就是约定优于配置。
2.1 快速搭建项目
推荐使用 Spring Initializr 或者 IDEA 直接创建:
- 新建项目 -> Spring Initializr
- 项目元数据:
- Group:
com.example - Artifact:
demo - Type: Maven Project (别选 Gradle,新手先吃米饭)
- Java: 17
- Group:
- 依赖选择:
Spring WebLombok(可选,但强烈推荐,省得写 getter/setter)
2.2 代码实战
生成项目后,你会看到一个 DemoApplication.java。接下来,我们写一个简单的接口:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // 这个注解很关键,表示这是一个控制器,且返回的是 JSON 数据而非页面
public class HelloWorldController {
@GetMapping("/hello") // 映射 GET 请求
public String sayHello() {
return "Hello, Spring Boot! 你终于入门了!";
}
}
然后,直接运行 DemoApplication.main() 方法。
打开浏览器访问 http://localhost:8080/hello。
如果看到了 Hello, Spring Boot! 你终于入门了!,恭喜你,你的环境配置基本没问题。
常见坑点:
- 端口冲突:如果 8080 被占用(比如你的浏览器开太多标签页,或者有个 Tomcat 在后台跑),应用会启动失败。
- 解决:在
src/main/resources/application.properties中修改端口:
server.port=8081 - 解决:在
第三章:依赖冲突——Spring Boot 的“噩梦”
这是新手遇到最多的报错之一:
Caused by: java.lang.NoSuchMethodError: org.springframework.core.annotation.AnnotationUtils.findAnnotation...
或者
Failed to start bean 'webServerStartStop'...
这些错误看起来很深奥,但 99% 的原因都是依赖冲突。
3.1 什么是依赖冲突?
想象一下,你请了 A 师傅和 B 师傅来装修。A 师傅带的锤子是 20cm 长的,B 师傅带的锤子是 30cm 长的。结果你家里只有一个锤子架,只能放一种尺寸。当 A 师傅需要用到锤子时,却发现架子上放的是 B 师傅的锤子,于是吵架了。
在 Java 里,这就叫:不同的 Jar 包依赖了同一库的不同版本,而最终生效的版本可能不兼容。
3.2 如何排查依赖冲突?
方法一:IDEA 的 Maven Helper 插件(最推荐)
- 安装插件:
Settings->Plugins-> 搜索Maven Helper-> 安装。 - 打开任意一个
pom.xml文件。 - 底部会出现一个
Dependency Analyzer标签页。 - 选中某个冲突的依赖,它会用红色高亮显示冲突路径。
方法二:命令行排查
在项目根目录执行:
mvn dependency:tree -Dverbose
这会打印出所有依赖树。找到冲突的地方,比如:
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:3.2.0:compile
[INFO] +- (org.springframework:spring-core:jar:6.1.1:compile - managed from 6.0.10; omitted for duplicate)
[INFO] - org.springframework:spring-web:jar:6.1.1:compile
[INFO] - (org.springframework:spring-core:jar:6.1.1:compile - managed from 6.0.10; omitted for duplicate)
[INFO] \- org.springframework:spring-core:jar:5.3.20:compile (conflict)
[INFO] +-\ org.springframework:spring-beans:jar:5.3.20:compile
[INFO] | - (org.springframework:spring-core:jar:5.3.20:compile - version managed from 6.1.1; omitted for duplicate)
[INFO] - org.springframework:spring-context:jar:5.3.20:compile
[INFO] - (org.springframework:spring-core:jar:5.3.20:compile - version managed from 6.1.1; omitted for duplicate)
这里显示 spring-core 有两个版本:6.1.1 和 5.3.20。由于你用的是 Spring Boot 3(需要 Spring 6),如果某个第三方库强制拉入了 Spring 5 的包,就会炸。
3.3 如何解决?使用 <exclusions>
在 pom.xml 中,找到引入冲突依赖的地方,使用 <exclusions> 排除掉错误的版本。
例子:
假设你引入了一个旧版的工具包 old-lib,它依赖了 spring-core:5.3.20,而你项目需要 6.1.1。
<dependency>
<groupId>com.example</groupId>
<artifactId>old-lib</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
这样,Maven 就不会再带那个旧的 spring-core 进来了,冲突解决。
第四章:IoC 通俗解析——为什么你要把“控制”交出去?
很多新手写代码是这样的:
@Service
public class UserService {
private UserDao userDao = new UserDaoImpl(); // 自己 new 出来
public void addUser() {
userDao.insert();
}
}
问题在哪?
如果你以后想换个数据库实现,比如 OracleUserDaoImpl,你得去改 UserService 的代码。这就叫耦合。你的业务逻辑紧绑定了具体实现。
4.1 什么是 IoC(控制反转)?
IoC = Inversion of Control
通俗解释:
以前,你需要一个对象(比如 UserDao),你得自己去市场上买(new 出来)。这就是“控制”在你手里。
现在,你告诉 Spring:“我需要一个人来帮我管数据库操作,随便谁行,反正你看着办。”然后 Spring 帮你把这个人找来,塞到你手里。
控制权从你(开发者)转移到了Spring 容器。这就是反转。
4.2 什么是 DI(依赖注入)?
DI = Dependency Injection,是 IoC 的一种实现方式。 Spring 怎么把对象塞给你呢?
- 构造器注入(推荐)
- Setter 注入
- 字段注入(
@Autowired,最省事但不够优雅)
例子:
@Service
public class UserService {
// 通过构造器注入,Spring 启动时会自动找到 UserDao 的实现类,传入这里
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void addUser() {
userDao.insert();
}
}
好处:
- 解耦:
UserService不关心UserDao是怎么实现的,只关心它有没有insert()方法。 - 易测试:单元测试时,你可以直接传一个 Mock 的
UserDao进去,不用启动整个 Spring 容器。 - 易替换:想换 Oracle 数据库?写个
OracleUserDaoImpl实现UserDao接口,加上@Repository注解,Spring 自动接管,业务代码一行不用改。
4.3 bean 是什么?
Spring 管理的那些对象,叫 Bean。
@Component:通用的组件@Service:服务层组件(放在UserService上)@Repository:数据访问层组件(放在UserDao上)@Controller:控制层组件(放在HelloWorldController上)
这些注解本质上都是 @Component 的派生,告诉 Spring:“嘿,这个类是我的人,你帮我管着,要用的时候直接给你。”
第五章:实战进阶——一个完整的 CRUD 示例
光说不练假把式。我们来做一个简单的“用户管理”接口。
5.1 项目结构
src/main/java/com/example/demo/
├── DemoApplication.java
├── controller/
│ └── UserController.java
├── service/
│ ├── UserService.java
│ └── impl/
│ └── UserServiceImpl.java
├── repository/
│ ├── UserDao.java
│ └── impl/
│ └── UserDaoImpl.java
└── model/
└── User.java
5.2 实体类
package com.example.demo.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data // 自动生成 getter, setter, toString, equals, hashCode
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
}
5.3 数据访问层 (Repository)
package com.example.demo.repository;
import com.example.demo.model.User;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Repository;
@Repository // 告诉 Spring 这是数据访问层
public class UserDaoImpl implements UserDao {
// 模拟数据库,用 Map 存储
private final Map<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
@Override
public List<User> findAll() {
return List.copyOf(users.values());
}
@Override
public User findById(Long id) {
return users.get(id);
}
@Override
public User save(User user) {
if (user.getId() == null) {
user.setId(idGenerator.getAndIncrement());
}
users.put(user.getId(), user);
return user;
}
@Override
public void deleteById(Long id) {
users.remove(id);
}
}
5.4 业务逻辑层 (Service)
package com.example.demo.service;
import com.example.demo.model.User;
import java.util.List;
public interface UserService {
List<User> findAll();
User findById(Long id);
User save(User user);
void deleteById(Long id);
}
package com.example.demo.service.impl;
import com.example.demo.model.User;
import com.example.demo.repository.UserDao;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // 告诉 Spring 这是服务层
public class UserServiceImpl implements UserService {
// 构造器注入,符合最佳实践
private final UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Override
public List<User> findAll() {
return userDao.findAll();
}
@Override
public User findById(Long id) {
return userDao.findById(id);
}
@Override
public User save(User user) {
return userDao.save(user);
}
@Override
public void deleteById(Long id) {
userDao.deleteById(id);
}
}
5.5 控制层 (Controller)
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController // 同时具备 @Controller 和 @ResponseBody
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
// 构造器注入
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAll() {
return userService.findAll();
}
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User create(@RequestBody User user) {
return userService.save(user);
}
@PutMapping("/{id}")
public User update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.save(user);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
userService.deleteById(id);
}
}
5.6 测试
启动应用,用 Postman 或 curl 测试:
# 创建用户
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" -d '{"name":"张三","age":25}'
# 查询所有用户
curl http://localhost:8080/api/users
第六章:更多实战技巧与避坑指南
6.1 配置文件:application.yml vs properties
推荐使用 application.yml,层级结构更清晰:
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
6.2 全局异常处理
别让错误信息直接扔给前端。写一个全局异常处理器:
”`java package com.example.demo.config;
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap; import java.util.Map;
@RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
Map<String, Object> body = new HashMap
