嘿,朋友!如果你现在正盯着电脑屏幕,看着满屏红色的报错信息或者一脸茫然地对着“依赖注入”这四个字发呆,别担心,我完全理解你的感受。Spring 就像是一个巨大的、复杂的乐高城堡,刚进去的时候确实容易迷路。但好消息是,一旦你找到了那几块核心的积木——也就是 IOC(控制反转)和 AOP(面向切面编程),整个城堡的建造逻辑就会瞬间清晰起来。

今天,我们不搞那些枯燥的教科书式定义,也不玩文字游戏。我们要像搭积木一样,一步步把 Spring 拆解开,看看它到底是怎么帮你把代码变得优雅、可维护且强大的。我会尽量用大白话,配合真实的代码场景,带你从最基础的“Hello World”环境开始,一直深入到核心原理的实战解析。准备好了吗?让我们开始这场技术探险吧。

第一章:别急着写代码,先给 Spring 安个“家”

很多初学者最大的误区就是:打开 IDE,新建一个 Java 类,然后直接开始 new 对象。在 Spring 出现之前,这确实是标准做法。但在 Spring 的世界里,这种做法太“原始”了。我们需要一个更现代化的方式来管理我们的项目。

1.1 为什么选择 Maven 或 Gradle?

想象一下,如果你的项目需要用到数据库连接池、日志框架、HTTP 客户端等等,难道你要去官网下载一个个 .jar 包,然后手动复制到 lib 文件夹里吗?如果版本冲突了怎么办?如果某个库过时了,更新起来是不是要改几十个文件?

这就是 MavenGradle 存在的意义。它们就像是项目的“管家”,你只需要告诉管家:“我需要 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 保存用户:张三”。

关键点解析:

  1. @Service, @Repository, @RestController: 这些注解都是 @Component 的特化版。它们的作用是标记该类应该被 Spring 容器扫描并实例化。
  2. @Autowired: 这是“依赖注入”的关键。当 Spring 发现 UserService 需要 UserDao 时,它会在容器中查找类型为 UserDao 的 Bean,并自动赋值给 userDao 字段。
  3. 解耦: UserService 根本不需要知道 UserDao 是 JDBC 实现的还是 Redis 实现的。如果需要切换,只需修改 JdbcUserDao 上的注解或新增一个实现类,UserService 代码无需改动。

2.3 IOC 容器的生命周期

理解 IOC 不仅仅是会用注解,还要理解对象是怎么被创建出来的。Spring 容器在启动时,会执行以下步骤:

  1. 实例化 (Instantiation): 通过构造函数或工厂方法创建 Bean 的实例。
  2. 属性填充 (Populate): 注入依赖(即处理 @Autowired)。
  3. 初始化 (Initialization): 调用 @PostConstruct 标注的方法或 InitializingBeanafterPropertiesSet 方法。
  4. 销毁 (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 容器有两个主要接口:BeanFactoryApplicationContext

  • 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(缓存):

  1. singletonObjects: 存放完全初始化好的 Bean。
  2. earlySingletonObjects: 存放原始的 Bean 对象(尚未填充属性),用于解决循环依赖。
  3. singletonFactories: 存放 Bean 工厂,用于生成早期引用。

流程简述:

  1. Spring 创建 ServiceA,实例化后,将其工厂放入 singletonFactories
  2. 开始填充 ServiceA 的属性,发现需要 ServiceB
  3. Spring 创建 ServiceB,实例化后,将其工厂放入 singletonFactories
  4. 开始填充 ServiceB 的属性,发现需要 ServiceA
  5. 此时 ServiceA 还没有完全初始化,但从 singletonFactories 中获取其早期引用(通过 getObject 方法),放入 earlySingletonObjects
  6. ServiceB 成功注入 ServiceA 的早期引用,并完成初始化。
  7. ServiceB 放入 singletonObjects
  8. ServiceA 继续填充剩余属性(虽然 serviceB 已经有了),完成初始化,放入 singletonObjects

注意: 这种机制仅对单例(Singleton)且通过setter 或字段注入(而非构造器注入)有效。如果使用构造器注入,Spring 无法在构造期间提供部分初始化的对象,因此会抛出 BeanCurrentlyInCreationException

4.3 AOP 的动态代理:JDK vs CGLIB

Spring AOP 底层使用动态代理来实现切面的织入。它有两种策略:

  1. JDK Dynamic Proxy: 基于接口。如果目标类实现了接口,Spring 默认使用 JDK 动态代理。生成的代理类实现了相同的接口,并重写了其中的方法。
  2. 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 开始的。加油!