嘿,朋友!看到你点了这个标题,我猜你正坐在电脑前,面对满屏的 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 实现)时,你只需要修改配置或注解,而
