嘿,朋友!如果你现在正盯着电脑屏幕,看着满屏红色的报错信息或者一脸茫然地对着“依赖注入”这四个字发呆,别担心,我完全理解你的感受。Spring 就像是一个巨大的、复杂的乐高城堡,刚进去的时候确实容易迷路。但好消息是,一旦你找到了那几块核心的积木——也就是 IOC(控制反转)和 AOP(面向切面编程),整个城堡的建造逻辑就会瞬间清晰起来。
今天,我们不搞那些枯燥的教科书式定义,也不玩文字游戏。我们要像搭积木一样,一步步把 Spring 拆解开,看看它到底是怎么帮你把代码变得优雅、可维护且强大的。我会尽量用大白话,配合真实的代码场景,带你从最基础的“Hello World”环境开始,一直深入到核心原理的实战解析。准备好了吗?让我们开始这场技术探险吧。
第一章:别急着写代码,先给 Spring 安个“家”
很多初学者最大的误区就是:打开 IDE,新建一个 Java 类,然后直接开始 new 对象。在 Spring 出现之前,这确实是标准做法。但在 Spring 的世界里,这种做法太“原始”了。我们需要一个更现代化的方式来管理我们的项目。
1.1 为什么选择 Maven 或 Gradle?
想象一下,如果你的项目需要用到数据库连接池、日志框架、HTTP 客户端等等,难道你要去官网下载一个个 .jar 包,然后手动复制到 lib 文件夹里吗?如果版本冲突了怎么办?如果某个库过时了,更新起来是不是要改几十个文件?
这就是 Maven 或 Gradle 存在的意义。它们就像是项目的“管家”,你只需要告诉管家:“我需要 Spring Web 和 Spring JDBC”,管家就会自动去中央仓库下载所有相关的依赖,甚至包括它们依赖的其他库。
1.2 手把手搭建第一个 Spring Boot 项目
为了让你最快感受到 Spring 的魅力,我们使用 Spring Boot。它是 Spring 家族的“加速器”,默认配置好了大部分繁琐的设置,让你能专注于业务逻辑。
步骤一:初始化项目
你可以访问 start.spring.io,这是 Spring 官方提供的快速启动器。
- Project: Maven Project (推荐)
- Language: Java
- Spring Boot: 选择一个较新的稳定版本(比如 3.2.x 或 3.3.x)
- Group: com.example (或者你的公司域名倒序)
- Artifact: demo (项目名称)
- Dependencies:
- Spring Web (用于构建 REST API)
- Lombok (减少样板代码,可选但强烈推荐)
- Spring Data JPA (如果你打算连数据库,可选)
点击 “Generate”,下载 ZIP 包,解压后用 IntelliJ IDEA 或 Eclipse 打开。
步骤二:目录结构初探
当你打开项目时,你会看到这样的结构:
src/main/java/com/example/demo/
├── DemoApplication.java # 启动类
└── controller/ # 控制器层
└── service/ # 服务层
└── model/ # 模型层
src/main/resources/
├── application.properties # 配置文件
DemoApplication.java 是这个项目的入口。它的代码非常简单:
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);
}
}
注意那个 @SpringBootApplication 注解。它其实是一个组合注解,包含了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。简单来说,它告诉 Spring:“嘿,从这里开始扫描,自动配置一切,并把这些类注册到容器中。”
点击运行,控制台会输出类似 Started DemoApplication in 2.5 seconds 的信息。恭喜你,你的第一个 Spring Boot 应用已经跑起来了!
第二章:IOC 容器——你的“超级管家”来了
现在,我们来聊聊 Spring 最核心的概念:IOC (Inversion of Control,控制反转)。
2.1 什么是“控制反转”?
在传统开发中,如果你需要一个对象,你会自己创建它:
// 传统方式:你自己管理依赖
public class UserService {
private UserDao userDao = new UserDao(); // 耦合度高,难以测试,难以替换
}
这种写法的问题在于,UserService 紧紧绑定了 UserDao 的具体实现。如果有一天你想把 UserDao 换成 JdbcUserDao 或者 RedisUserDao,你得修改 UserService 的代码。这在大型项目中简直是灾难。
IOC 的核心思想是: 不再由程序主动创建和管理对象,而是将对象的创建权交给一个第三方容器(即 Spring 容器)。程序只需要告诉容器:“我需要什么类型的对象”,容器就会负责把它找出来并塞给你。
2.2 实战:用 @Autowired 实现解耦
让我们重构上面的代码。
首先,定义接口和实现:
package com.example.demo.model;
public interface UserDao {
void save(String name);
}
package com.example.demo.model;
import org.springframework.stereotype.Repository;
@Repository // 告诉 Spring:我是一个组件,请把我管起来
public class JdbcUserDao implements UserDao {
@Override
public void save(String name) {
System.out.println("通过 JDBC 保存用户:" + name);
}
}
接着,修改 UserService:
package com.example.demo.service;
import com.example.demo.model.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 告诉 Spring:我是一个服务组件
public class UserService {
// Spring 会自动寻找实现了 UserDao 接口的 Bean 注入到这里
@Autowired
private UserDao userDao;
public void register(String name) {
System.out.println("开始注册用户...");
userDao.save(name); // 调用 DAO 层
System.out.println("注册完成");
}
}
最后,创建一个 Controller 来触发这个过程:
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/register")
public String register() {
userService.register("张三");
return "注册成功";
}
}
当你访问 http://localhost:8080/register 时,你会看到控制台打印出“通过 JDBC 保存用户:张三”。
关键点解析:
- @Service, @Repository, @RestController: 这些注解都是
@Component的特化版。它们的作用是标记该类应该被 Spring 容器扫描并实例化。 - @Autowired: 这是“依赖注入”的关键。当 Spring 发现
UserService需要UserDao时,它会在容器中查找类型为UserDao的 Bean,并自动赋值给userDao字段。 - 解耦:
UserService根本不需要知道UserDao是 JDBC 实现的还是 Redis 实现的。如果需要切换,只需修改JdbcUserDao上的注解或新增一个实现类,UserService代码无需改动。
2.3 IOC 容器的生命周期
理解 IOC 不仅仅是会用注解,还要理解对象是怎么被创建出来的。Spring 容器在启动时,会执行以下步骤:
- 实例化 (Instantiation): 通过构造函数或工厂方法创建 Bean 的实例。
- 属性填充 (Populate): 注入依赖(即处理
@Autowired)。 - 初始化 (Initialization): 调用
@PostConstruct标注的方法或InitializingBean的afterPropertiesSet方法。 - 销毁 (Destruction): 当容器关闭时,调用
@PreDestroy标注的方法。
你可以尝试添加以下代码来观察这个过程:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Service
public class UserService {
@PostConstruct
public void init() {
System.out.println("UserService 初始化完成");
}
@PreDestroy
public void destroy() {
System.out.println("UserService 即将被销毁");
}
}
重启应用并观察控制台,你会看到清晰的初始化日志。
第三章:AOP 切面编程——代码的“横切关注点”
如果说 IOC 解决了对象创建的问题,那么 AOP (Aspect-Oriented Programming,面向切面编程) 则解决了代码重复和横切关注点的问题。
3.1 什么是“横切关注点”?
想象一下,你在开发一个电商系统。每个业务方法(如下单、支付、查询库存)可能都需要记录日志、检查权限、或者进行事务管理。
如果没有 AOP,你可能会在每个方法里都写一遍:
public void placeOrder() {
log.info("开始下单"); // 重复代码
checkPermission(); // 重复代码
try {
// 业务逻辑
} catch (Exception e) {
log.error("下单失败", e);
throw e;
}
}
如果有一百个方法,你就得复制粘贴一百次。这不仅累,而且容易出错,维护起来更是噩梦。
AOP 的思想是: 把这些通用的、跨越多处业务的逻辑(如日志、事务、权限)抽取出来,做成一个“切面”,然后在需要的时候动态地织入到业务代码中。业务代码只关心业务,不关心日志和事务。
3.2 实战:实现一个简单的日志切面
我们将使用 Spring AOP 来实现自动日志记录。
第一步:引入依赖
在 pom.xml 中确保有 spring-boot-starter-aop 依赖(通常 Spring Boot Starter Web 已经包含,但显式声明更安全):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
第二步:定义切面类
创建一个名为 LogAspect 的类,并使用 @Aspect 和 @Component 注解。
package com.example.demo.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
/**
* 定义切入点:匹配 com.example.demo.controller 包下所有类的公共方法
*/
@Around("execution(* com.example.demo.controller..*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 获取方法名和参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
logger.info(">>> 开始执行方法: {}, 参数: {}", methodName, Arrays.toString(args));
// 执行目标方法
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
logger.info("<<< 方法执行结束: {}, 耗时: {} ms", methodName, (end - start));
return result;
}
}
第三步:理解关键注解
- @Aspect: 标识这是一个切面类。
- @Component: 让 Spring 管理这个切面类。
- @Around(“execution(…)”): 这是通知类型的一种。
execution表达式用于指定哪些方法会被拦截。*: 返回任意类型com.example.demo.controller..*.*: 该包及其子包下的任意类*(..): 任意名称、任意参数的方法
- ProceedingJoinPoint: 代表被拦截的连接点(即业务方法)。调用
proceed()方法会真正执行业务逻辑。
第四步:测试
再次访问 http://localhost:8080/register,查看控制台日志:
2023-10-27 10:00:00 INFO c.e.d.aspect.LogAspect - >>> 开始执行方法: register, 参数: []
2023-10-27 10:00:00 INFO c.e.d.model.JdbcUserDao - 通过 JDBC 保存用户:张三
2023-10-27 10:00:00 INFO c.e.d.aspect.LogAspect - <<< 方法执行结束: register, 耗时: 15 ms
看!你的业务代码 UserController 里没有任何日志相关的代码,但日志功能完美实现了。这就是 AOP 的强大之处:关注点分离。
3.3 进阶:事务管理也是 AOP
你可能经常听到 @Transactional 注解。其实,Spring 的事务管理底层也是基于 AOP 实现的。当你在方法上加 @Transactional 时,Spring 会生成一个代理对象,在方法执行前开启事务,执行后提交或回滚事务。
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional // 开启事务
public void createOrder(Order order) {
orderRepository.save(order);
// 如果这里抛出异常,之前的 save 操作会自动回滚
if (order.getAmount() < 0) {
throw new IllegalArgumentException("金额不能为负");
}
}
}
第四章:深入原理——Spring 是如何做到这一切的?
作为专家,我不能只教你怎么用,还得告诉你背后的魔法是怎么发生的。理解了原理,你才能写出更健壮、性能更好的代码。
4.1 BeanFactory vs ApplicationContext
Spring 容器有两个主要接口:BeanFactory 和 ApplicationContext。
- BeanFactory: 基础的 IoC 容器,采用懒加载(Lazy Loading)。只有当你第一次调用
getBean()时,才会创建 Bean。适合资源受限的环境。 - ApplicationContext:
BeanFactory的子接口,增加了更多企业级功能,如事件发布、国际化支持、AOP 集成等。最重要的是,它在容器启动时默认预实例化所有单例 Bean。
我们在 Spring Boot 中使用的是 ApplicationContext 的实现(通常是 AnnotationConfigServletWebServerApplicationContext)。
4.2 循环依赖问题:Spring 怎么解决“鸡生蛋,蛋生鸡”?
假设你有两个类:
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
当 Spring 尝试创建 ServiceA 时,它发现需要 ServiceB;创建 ServiceB 时,又发现需要 ServiceA。这就陷入了死循环。
Spring 的解决方案:三级缓存
Spring 使用了一个巧妙的机制来解决这个问题,主要涉及三个 Map(缓存):
- singletonObjects: 存放完全初始化好的 Bean。
- earlySingletonObjects: 存放原始的 Bean 对象(尚未填充属性),用于解决循环依赖。
- singletonFactories: 存放 Bean 工厂,用于生成早期引用。
流程简述:
- Spring 创建
ServiceA,实例化后,将其工厂放入singletonFactories。 - 开始填充
ServiceA的属性,发现需要ServiceB。 - Spring 创建
ServiceB,实例化后,将其工厂放入singletonFactories。 - 开始填充
ServiceB的属性,发现需要ServiceA。 - 此时
ServiceA还没有完全初始化,但从singletonFactories中获取其早期引用(通过 getObject 方法),放入earlySingletonObjects。 ServiceB成功注入ServiceA的早期引用,并完成初始化。ServiceB放入singletonObjects。ServiceA继续填充剩余属性(虽然serviceB已经有了),完成初始化,放入singletonObjects。
注意: 这种机制仅对单例(Singleton)且通过setter 或字段注入(而非构造器注入)有效。如果使用构造器注入,Spring 无法在构造期间提供部分初始化的对象,因此会抛出 BeanCurrentlyInCreationException。
4.3 AOP 的动态代理:JDK vs CGLIB
Spring AOP 底层使用动态代理来实现切面的织入。它有两种策略:
- JDK Dynamic Proxy: 基于接口。如果目标类实现了接口,Spring 默认使用 JDK 动态代理。生成的代理类实现了相同的接口,并重写了其中的方法。
- CGLIB (Code Generation Library): 基于继承。如果目标类没有实现接口,Spring 会使用 CGLIB 生成一个子类。
如何强制使用 CGLIB?
在 application.properties 中添加:
spring.aop.proxy-target-class=true
区别与选择:
- JDK 代理: 性能稍好,但要求类必须实现接口。
- CGLIB 代理: 性能略低(因为涉及字节码生成和类加载),但可以代理没有接口的类。
在 Spring Boot 2.x 及以后,默认情况下,如果类实现了接口,优先使用 JDK 代理;如果没有接口,则使用 CGLIB。
第五章:实战进阶——整合 MyBatis 与数据库
光有 IOC 和 AOP 还不够,实际项目中我们离不开数据库。让我们把 Spring Boot 与 MyBatis 结合起来,看看完整的 CRUD 流程。
5.1 引入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
这里我们使用 H2 内存数据库,方便演示,无需安装 MySQL。
5.2 配置数据源
在 application.properties 中:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
mybatis.configuration.map-underscore-to-camel-case=true
5.3 编写 Mapper 接口
package com.example.demo.mapper;
import com.example.demo.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(Long id);
@Select("SELECT * FROM users")
List<User> findAll();
// 注意:H2 语法可能略有不同,实际生产中请使用 XML 或更复杂的 SQL
}
5.4 编写 Service 和 Controller
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
return userMapper.findById(id);
}
public List<User> getAllUsers() {
return userMapper.findAll();
}
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.getUser(id);
}
@GetMapping
public List<User> getAll() {
return userService.getAllUsers();
}
}
5.5 初始化数据
为了让查询有结果,我们可以创建一个 DataInitializer 类:
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class DataConfig {
@Bean
public CommandLineRunner initData(JdbcTemplate jdbcTemplate) {
return args -> {
// 创建表
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(255))");
// 插入数据
jdbcTemplate.update("INSERT INTO users (id, name) VALUES (?, ?)", 1, "Alice");
jdbcTemplate.update("INSERT INTO users (id, name) VALUES (?, ?)", 2, "Bob");
};
}
}
现在,启动应用,访问 /users,你应该能看到 JSON 格式的数组:[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]。
第六章:避坑指南与最佳实践
作为过来人,我必须提醒你一些常见的陷阱和优化建议。
6.1 避免 @Autowired 滥用
虽然 @Autowired 很方便,但过度依赖会导致类之间的隐式耦合。
- 推荐: 使用构造器注入。这是 Spring 官方推荐的方式,因为它保证了依赖不可变,且便于单元测试。
@Service
public class UserService {
private final UserMapper userMapper;
// 构造器注入
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
- 不推荐: 字段注入 (
@Autowired在字段上)。这会让测试变得困难,且掩盖了依赖关系。
6.2 事务的传播行为
@Transactional 有不同的传播行为(Propagation)。默认是 REQUIRED,表示如果当前存在事务,则加入;否则新建一个事务。
- REQUIRED: 最常见,嵌套调用共享同一个事务。
- REQUIRES_NEW: 挂起当前事务,开启新事务。常用于日志记录等独立操作。
- NOT_SUPPORTED: 以非事务方式执行,如果当前有事务则挂起。
注意: 在同一个类中,内部方法调用不会触发 AOP 代理,因此 @Transactional 可能失效。例如:
@Service
public class UserService {
public void methodA() {
methodB(); // 内部调用,事务不会生效!
}
@Transactional
public void methodB() {
// 业务逻辑
}
}
要解决这个问题,可以将 methodB 提取到另一个 Service 类中,或者使用 AopContext.currentProxy() 获取代理对象调用。
6.3 性能优化:懒加载 vs 预加载
Spring Boot 默认预加载所有单例 Bean。对于大型应用,这可能导致启动时间过长。
- 解决方案: 对于非核心 Bean,可以使用
@Lazy注解,使其在第一次使用时才初始化。
@Service
@Lazy
public class HeavyService { ... }
结语:Spring 是一场马拉松,不是短跑
到这里,我们已经从环境搭建聊到了 IOC 和 AOP 的原理,甚至实战了数据库整合。你可能会觉得内容很多,但请记住,Spring 的设计哲学始终是简化开发。
IOC 让你从对象的创建中解放出来,专注于业务逻辑;AOP 让你从重复的代码中解脱出来,专注于横切关注点的抽象。当你掌握了这两块基石,你会发现,Spring 不仅仅是一个框架,更是一种设计思维的体现。
不要试图一次性记住所有细节。就像学游泳一样,先在浅水区(小项目)练习划水(IOC),再慢慢尝试换气(AOP)。多写代码,多看源码,多思考“为什么这么设计”。
希望这篇文章能成为你 Spring 之旅的一块垫脚石。如果你在后续的学习中遇到任何问题,欢迎随时回来回顾这些基础。记住,每一个资深工程师,都是从 Hello World 开始的。加油!
