嘿,朋友!看到你点了这个标题,我猜你正坐在电脑前,面对满屏的 import 语句和那个让你既爱又恨的 ApplicationContext 感到有点头大。别慌,把咖啡放下,咱们不聊那些枯燥的官方文档定义,就像两个老朋友在工位旁聊天一样,我把这几年来带新人踩过的坑、攒下的经验,一股脑儿都倒给你。

Spring 5 确实是个大杂烩,功能多到离谱,但咱们今天不贪多,就盯着两个最核心的“老伙计”:IOC(控制反转)DI(依赖注入)。只要搞懂了这两个,你就相当于拿到了 Spring 世界的入场券。

第一章:为什么要搞这么麻烦的“反转”?

在聊技术之前,咱们先讲个生活中的故事。

假设你是个超级大厨,你想做一道“西红柿炒鸡蛋”。 在没有 Spring 的世界(Java SE)里: 你要自己买西红柿、自己买鸡蛋、自己去菜市场挑、自己洗、自己切。如果有一天你想做“酸辣土豆丝”,你还得重新去市场买土豆、买醋。更可怕的是,如果你炒菜的时候,发现鸡蛋坏了,你得先把“买鸡蛋”这个逻辑写得很死,才能知道哪有问题。这就像以前的 Java 项目,代码之间耦合度极高,改一处,牵全身,维护起来让人想砸键盘。

有了 Spring 之后(IOC 思想): Spring 就像是一个超级保姆或者高级餐厅。你只需要站在窗口,喊一声:“我要一份西红柿炒鸡蛋(TomatoEggService)!” 然后,Spring 就会帮你把处理好的、洗好的、切好的食材递给你。你不用管西红柿是哪买的,鸡蛋是哪下的,你只管“接收”和“使用”。

这就是 IOC(Inversion of Control,控制反转)

  • 控制:对象的创建权。
  • 反转:从你自己创建对象,反转给 Spring 容器来创建。

DI(Dependency Injection,依赖注入) 是实现 IOC 的手段。Spring 把依赖的对象“注入”到你的类里,而不是让你自己去 new 出来。

第二章:手把手搭建 HelloWorld 环境

咱们废话不多说,直接开干。我假设你已经装好了 JDK 1.8+ 和 Maven。如果你连 Maven 都没装,先去装一下,这相当于你的“工具箱”。

1. 创建 Maven 项目

打开你的 IDE(IntelliJ IDEA 是首选,Eclipse 也行),新建一个 Maven 项目。在 pom.xml 里,我们需要引入 Spring 5 的核心依赖。

<dependencies>
    <!-- Spring 核心容器 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.23</version>
    </dependency>
    
    <!-- 日志依赖,Spring 运行需要 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.36</version>
    </dependency>
</dependencies>

注意,版本我用了 5.3.23,这是 Spring 5 的晚期稳定版本,兼容性极好。

2. 编写第一个 Bean

在 Spring 眼里,你写的每一个 Java 类,只要交给它管,都叫 Bean

咱们先写一个简单的“问候服务”:

package com.spring.hello;

public class HelloService {
    
    private String message = "Hello, Spring 5!";

    // 这是一个简单的初始化方法,Spring 会在创建 Bean 后调用它
    public void init() {
        System.out.println("【初始化】HelloService 对象已被 Spring 创建");
    }
    
    public void sayHello() {
        System.out.println(message);
    }
}

注意看,这里没有任何 Spring 的注解,就是一个普普通通的 POJO(Plain Old Java Object)。这很重要!Spring 喜欢自然,它不强迫你的代码变得奇形怪状。

3. 配置 Spring 容器

接下来,我们需要告诉 Spring:“嘿,把这个 HelloService 管起来。”

src/main/resources 目录下,创建一个 applicationContext.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 把 HelloService 注册到容器中,id 是给它的代号 -->
    <bean id="helloService" class="com.spring.hello.HelloService" init-method="init"/>

</beans>

这里用了基于 XML 的配置方式。虽然现在很多新项目喜欢用注解,但我强烈建议初学者先看懂 XML 配置。为什么?因为 XML 配置能让你清晰地看到 Spring 容器的“全貌”,就像看一张地图,知道哪个路通哪个街。等搞懂了原理,再去学注解会轻松十倍。

4. 启动容器,运行 Hello World

最后,写一个测试类:

package com.spring.test;

import com.spring.hello.HelloService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainTest {
    public static void main(String[] args) {
        // 1. 指定配置文件的位置,启动 Spring 容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 2. 从容器中获取 Bean 实例,注意:这里不是 new,而是 get!
        HelloService helloService = context.getBean("helloService", HelloService.class);
        
        // 3. 调用方法
        helloService.sayHello();
    }
}

当你运行 main 方法时,你会看到控制台输出:

【初始化】HelloService 对象已被 Spring 创建
Hello, Spring 5!

恭喜!你刚刚亲手搭建了一个 Spring IOC 容器,并成功获取了 Bean。 这一刻,你已经成为一个 Spring 初学者了。

第三章:深入 IOC 容器——它到底怎么工作的?

很多人以为 new ClassPathXmlApplicationContext 只是读个文件,其实不然。这一步骤背后,Spring 干了一堆脏活累活。咱们来拆解一下,帮你建立直观的理解。

第一步:解析配置文件

Spring 读取 applicationContext.xml,把它解析成一个 BeanDefinition 对象。你可以把 BeanDefinition 想象成一张“简历”或“施工图纸”,里面记录了:

  • 这个 Bean 的类名(class)
  • 这个 Bean 的 ID(id)
  • 作用域(singleton 还是 prototype)
  • 初始化方法(init-method)

第二步:实例化 Bean

Spring 利用反射(Reflection),根据“图纸”调用构造函数,在堆内存中真正创建出对象。这时候,HelloService 这个对象才真正存在。

第三步:属性填充(DI)

如果这个 Bean 有依赖其他 Bean,或者需要注入属性,Spring 会在这里自动帮你完成。比如,如果 HelloService 里有个 private String message,Spring 会检查配置文件里有没有配置注入值,有的话就塞进去。

第四步:初始化回调

如果配置了 init-method,或者实现了 InitializingBean 接口,Spring 会在对象创建完成后,调用这些方法。这就是为什么我们在 HelloService 里定义了 init() 方法并能在控制台看到输出的原因。

第五步:注册到容器

最后,这个创建好的对象会被放入一个 Map(实际上是 DefaultSingletonBeanRegistry)中,以 ID 为键,对象为值。下次你再 getBean("helloService") 时,Spring 就直接从 Map 里取出来给你,不会再重新创建了(默认是单例模式)。

关键点总结:IOC 容器的核心就是一个大 Map,管理着所有对象的生命周期。

第四章:依赖注入(DI)的三种方式

既然叫“依赖注入”,那“注入”具体是怎么玩的?在 Spring 5 中,主要有三种方式。咱们用一个新的例子——UserService 依赖 UserDao,来演示这三种方式。

场景设定

假设我们有一个用户服务层,它需要操作数据库。

// 接口
public interface UserDao {
    void saveUser();
}

// 实现类
public class UserDaoImpl implements UserDao {
    public void saveUser() {
        System.out.println("数据已保存到数据库...");
    }
}

public class UserService {
    private UserDao userDao; // 依赖的接口

    // 后续通过 DI 注入 userDao
    public void addUser() {
        userDao.saveUser();
    }
}

方式一:Setter 注入(最常用,推荐)

这是最经典的方式。Spring 先调用无参构造函数创建对象,然后调用 setter 方法注入依赖。

在 XML 中:

<bean id="userDao" class="com.spring.di.UserDaoImpl"/>

<bean id="userService" class="com.spring.di.UserService">
    <!-- 通过 setter 方法注入,property 的 name 对应 setter 的方法名去掉 set 和首字母大写 -->
    <property name="userDao" ref="userDao"/>
</bean>

对应的 Java 代码需要有一个 setter:

public class UserService {
    private UserDao userDao;

    // Spring 会调用这个方法
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    
    public void addUser() {
        if (userDao != null) {
            userDao.saveUser();
        }
    }
}

优点:清晰、直观,支持可选依赖(不配置 setter 也能创建对象)。 缺点:代码稍多,需要写 setter。

方式二:构造器注入(最健壮)

在对象创建时就必须提供依赖,否则创建失败。

在 XML 中:

<bean id="userService" class="com.spring.di.UserService">
    <constructor-arg ref="userDao"/>
</bean>

对应的 Java 代码:

public class UserService {
    private UserDao userDao;

    // 构造器注入
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    
    public void addUser() {
        userDao.saveUser();
    }
}

优点:依赖不可变(final),对象创建后状态稳定,适合强制依赖。 缺点:依赖多时,构造函数参数列表会很长。

方式三:注解注入(现代开发主流)

这是目前企业开发中最常见的方式,配合 @Configuration<context:annotation-config/> 使用。

首先,在 XML 开启注解支持:

<context:component-scan base-package="com.spring.di"/>
<context:annotation-config/>

然后,在 Java 代码中:

@Component // 告诉 Spring 这是一个 Bean
public class UserService {
    
    @Autowired // 自动注入,按类型匹配
    private UserDao userDao;
    
    public void addUser() {
        userDao.saveUser();
    }
}

@Component // 同理
public class UserDaoImpl implements UserDao {
    public void saveUser() {
        System.out.println("数据已保存到数据库...");
    }
}

或者,你也可以用 @Resource(JDK 标准)代替 @Autowired,它按名称注入。

为什么推荐初学者理解 XML,但工作中多用注解? 因为 XML 展示了 Spring 的“骨架”,让你明白底层发生了什么;而注解是“语法糖”,让代码更简洁。不要只会被注解牵着走,要懂背后的原理。

第五章:Bean 的作用域——Singleton 还是 Prototype?

这是一个经常被忽视但极其重要的概念。你在配置 Bean 时,有没有想过:每次 getBean 得到的对象,是同一个吗?

Spring 默认是 Singleton(单例)。 这意味着,在整个应用中,无论你怎么 getBean,拿到的都是同一个对象实例。这就像公司的老板,全公司只有一个,大家都认他。

优点:节省内存,性能好。 缺点:如果这个 Bean 是有状态的(比如里面有成员变量会被修改),多个线程同时操作可能会出问题。

Prototype(原型/多例): 每次 getBean 都会创建一个新的实例。就像每个新员工入职,都会发一套全新的办公设备。

在 XML 中配置:

<bean id="helloService" class="com.spring.hello.HelloService" scope="prototype"/>

在注解中配置:

@Scope("prototype")
@Component
public class HelloService { ... }

实战建议:对于 Service、DAO 这类无状态或状态由外部管理的组件,务必使用 Singleton(默认即可),因为性能好且符合设计初衷。只有在极少数需要独立实例的场景(比如每个请求需要一个独立的上下文对象)才用 Prototype。

第六章:项目实战——搭建一个简单的图书管理系统

光说不练假把式。咱们来搞个完整的小项目,把今天学到的东西串起来。

项目结构

spring-tutorial/
├── pom.xml
└── src/main/java/com/spring/library/
    ├── model/Book.java
    ├── dao/BookDao.java
    ├── dao/impl/BookDaoImpl.java
    ├── service/BookService.java
    └── Main.java

1. 模型层

package com.spring.library.model;

public class Book {
    private String id;
    private String name;
    private double price;

    // 构造器、getter、setter 省略,建议用 Lombok 简化
    public Book(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    
    @Override
    public String toString() {
        return "Book{id='" + id + "', name='" + name + "', price=" + price + "}";
    }
}

2. 数据访问层 (DAO)

package com.spring.library.dao;

import com.spring.library.model.Book;
import java.util.*;

public interface BookDao {
    void addBook(Book book);
    List<Book> findAllBooks();
}

// 模拟数据库,用内存中的 List 代替
@Component
public class BookDaoImpl implements BookDao {
    
    // 模拟数据库数据
    private Map<String, Book> bookStore = new HashMap<>();

    @Override
    public void addBook(Book book) {
        bookStore.put(book.getId(), book);
        System.out.println("【DAO】图书已入库:" + book.getName());
    }

    @Override
    public List<Book> findAllBooks() {
        return new ArrayList<>(bookStore.values());
    }
}

3. 业务逻辑层 (Service)

package com.spring.library.service;

import com.spring.library.dao.BookDao;
import com.spring.library.model.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service // 标注为 Service Bean
public class BookService {
    
    @Autowired
    private BookDao bookDao; // 自动注入 DAO

    public void addBook(Book book) {
        bookDao.addBook(book);
    }

    public List<Book> getAllBooks() {
        return bookDao.findAllBooks();
    }
}

4. 配置文件

resources 目录下创建 applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 扫描组件,让 @Component 和 @Service 生效 -->
    <context:component-scan base-package="com.spring.library"/>

</beans>

5. 测试运行

package com.spring.library;

import com.spring.library.model.Book;
import com.spring.library.service.BookService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // 启动容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 获取 Service
        BookService bookService = context.getBean(BookService.class);
        
        // 添加图书
        bookService.addBook(new Book("001", "Spring 5 实战", 89.0));
        bookService.addBook(new Book("002", "Java 核心技术", 120.0));
        
        // 查询图书
        List<Book> books = bookService.getAllBooks();
        System.out.println("=== 馆藏图书列表 ===");
        for (Book book : books) {
            System.out.println(book);
        }
    }
}

运行结果:

【DAO】图书已入库:Spring 5 实战
【DAO】图书已入库:Java 核心技术
=== 馆藏图书列表 ===
Book{id='001', name='Spring 5 实战', price=89.0}
Book{id='002', name='Java 核心技术', price=120.0}

看,是不是很简单?没有一个人工 new 关键字,所有对象都由 Spring 帮你管好了。当你需要替换 BookDaoImpl 的实现(比如换成真正的 JDBC 实现)时,你只需要修改配置或注解,而