说实话,刚开始接触Spring的时候,我也被它那种“黑魔法”般的配置搞懵过。为什么加了几个注解,对象就能自己冒出来?为什么我不new对象,它还能自动注入到我需要的地方?别急,今天咱们不背说明书,我就当个老学长,带你一步步把这块硬骨头啃下来。从最基础的HelloWorld开始,一路杀到REST接口,中间再把你可能会踩的依赖注入坑填平,最后给你把IOC和AOP这两个大Boss的核心原理扒得干干净净。准备好咖啡了吗?咱们开始。
一、 那个“Hello World”背后,到底发生了什么?
很多人学Spring,第一反应就是去官网复制一堆XML配置,或者一上来就搞什么Spring Boot自动配置,结果连最基本的依赖注入(DI)是怎么回事都没搞懂。咱们先回归原点,用最纯粹的方式,看看Spring到底怎么帮你管理对象。
想象一下,你写了一个简单的Java类,想输出一句“Hello, Spring!”。在没有Spring的时候,你会怎么做?
public class HelloWorld {
public static void main(String[] args) {
// 传统的做法:自己new对象
HelloWorld hw = new HelloWorld();
hw.sayHello();
}
public void sayHello() {
System.out.println("Hello, Spring!");
}
}
这没问题,对吧?但问题来了:如果HelloWorld类依赖了一个很复杂的数据库连接对象DatabaseService,而DatabaseService又依赖了配置加载器ConfigLoader……你是不是得一层一层地new下去?这就像你买手机,得先给手机厂打钱,手机厂再给零件厂打钱,零件厂再给原材料厂打钱……你想想都头大,而且一旦某个环节出问题,整个链条都得崩。
Spring的出现,就是为了帮你当这个“大管家”。它提供了一个容器(Container),专门帮你管理这些对象的生命周期和依赖关系。你只需要告诉Spring:“嘿,我有一个HelloWorld,它还缺一个DatabaseService,你给我搞定!”然后,Spring就会帮你把所有依赖都准备好,直接塞到你面前。
1.1 最小化的Spring HelloWorld(XML配置时代)
虽然现在是注解的天下,但我还是建议你了解一下XML配置,因为很多老项目还在用,而且理解了XML,你才能真正明白Spring IOC容器的本质——它就是一个巨大的BeanFactory。
首先,你需要在pom.xml里引入Spring Core的依赖:
<dependencies>
<!-- Spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version> <!-- 请使用最新稳定版 -->
</dependency>
</dependencies>
然后,创建一个Java类,别加任何奇怪的注解,就保持它是个普通的POJO(Plain Old Java Object):
package com.example.demo;
public class HelloWorld {
private String message;
// Setter方法,Spring会通过它来注入值
public void setMessage(String message) {
this.message = message;
}
public void sayHello() {
System.out.println("Hello, Spring! " + message);
}
}
注意看,这个类没有任何Spring相关的导入,它很干净。这就是Spring推崇的“低耦合”。
接下来,写一个Spring的配置文件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">
<!-- 告诉Spring,我们要管理一个名为helloWorld的Bean -->
<bean id="helloWorld" class="com.example.demo.HelloWorld">
<!-- 通过setter注入属性 -->
<property name="message" value="我是通过XML配置的!"/>
</bean>
</beans>
最后,写一个Main类来启动它:
package com.example.demo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// 1. 创建Spring容器,加载配置文件
// 这个过程会实例化所有Bean,并解决它们之间的依赖
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 2. 从容器中获取Bean,而不是自己new
HelloWorld hw = (HelloWorld) context.getBean("helloWorld");
// 3. 调用方法
hw.sayHello();
}
}
运行这段代码,你会看到输出:Hello, Spring! 我是通过XML配置的!
关键点来了: 注意看Main类里的context.getBean("helloWorld")。你没有new HelloWorld(),是Spring帮你创建的。而且,如果HelloWorld依赖于其他复杂对象,Spring会在你第一次getBean的时候,自动递归地去创建那些依赖对象,并把它们注入进来。这个过程叫做依赖注入(Dependency Injection, DI)。
1.2 现代做法:注解驱动(Annotation-Based Configuration)
XML配置太繁琐了,对吧?于是Spring推出了注解方式。我们来把上面的例子改造成注解版本。
首先,给HelloWorld类加上@Component注解,告诉Spring:“我是个Bean,请管着我。”
package com.example.demo;
import org.springframework.stereotype.Component;
@Component // 这是一个组件,Spring会自动扫描并创建它
public class HelloWorld {
private String message;
// 如果你想注入一个String类型的值,可以用@Value
// 但通常我们更希望注入另一个Bean
@Value("我是通过注解配置的!")
public void setMessage(String message) {
this.message = message;
}
public void sayHello() {
System.out.println("Hello, Spring! " + message);
}
}
注意,@Value注解默认是作用在Setter方法上的,这样Spring调用Setter时就会把值塞进去。你也可以直接加在字段上,但为了可测试性,建议加在Setter或构造器上。
接下来,创建一个配置类,代替之前的applicationContext.xml:
package com.example.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 表明这是一个配置类
@ComponentScan("com.example.demo") // 告诉Spring去哪里扫描@Component等注解
public class AppConfig {
// 这里可以放置更多的Bean定义,但大部分时候,组件扫描就够了
}
最后,修改Main类:
package com.example.demo;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.demo.config.AppConfig;
public class Main {
public static void main(String[] args) {
// 使用注解配置类创建容器
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 根据类型获取Bean,而不是ID(更推荐,因为ID变了不影响代码)
HelloWorld hw = context.getBean(HelloWorld.class);
hw.sayHello();
}
}
运行结果和之前一样。看,是不是简单多了?@ComponentScan让Spring自动帮你找到所有加了@Component的类,并创建它们。这就是现代Spring开发的基础。
这里我要特别强调一点: 很多初学者会在Main类里直接new HelloWorld(),然后就困惑为什么依赖没注入进来。记住,只有通过Spring容器获取的对象,才是受Spring管理的Bean,才会享受到依赖注入等特性。 你自己new出来的,就是个普通的Java对象,Spring管不了它。
二、 依赖注入(DI)的三种方式,你选哪种?
刚才我们看到了两种注入方式:字段/Setter注入和构造器注入(其实@Value也可以用在构造器参数上)。在Spring中,DI主要有三种方式,我一一给你拆解,并告诉你为什么我强烈推荐最后一种。
2.1 构造器注入(Constructor Injection)—— 我的首选
这是Spring官方强烈推荐的方式,尤其是在Spring 4.3之后,如果一个Bean只有一个构造器,你甚至不需要显式写@Autowired注解。
为什么首选构造器注入?
- 不可变性: 你可以在构造器里把依赖赋给
final字段,保证这个依赖一旦注入就不能被修改,更安全。 - 强制依赖: 如果构造器参数缺失,对象根本无法创建,这避免了“半初始化”状态,让代码更健壮。
- 易于测试: 在单元测试中,你可以直接
new出你的类,并手动传入模拟对象(Mock),而不需要启动整个Spring容器。
来看一个例子。假设HelloWorld需要一个GreetingService:
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service // 表明这是一个服务组件,也是Bean
public class GreetingService {
public String getGreeting() {
return "Hi there!";
}
}
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HelloWorld {
private final GreetingService greetingService; // 用final修饰
// 构造器注入
@Autowired // 如果只有一个构造器,这个注解可以省略
public HelloWorld(GreetingService greetingService) {
this.greetingService = greetingService;
}
public void sayHello() {
System.out.println(greetingService.getGreeting() + " " + "Hello, Spring!");
}
}
@Autowired注解可以加在构造器、Setter方法或字段上。当加在构造器上时,Spring会确保在调用构造器之前,所有依赖都已经被创建并注入。
注意: 如果你有多个构造器,必须用@Autowired明确指定哪个是注入点,否则Spring会报错。
2.2 Setter注入(Setter Injection)
这种方式比较古老,现在用得少一些,但在某些依赖可选的情况下,它还有用武之地。
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HelloWorld {
private GreetingService greetingService;
// Setter注入
@Autowired
public void setGreetingService(GreetingService greetingService) {
this.greetingService = greetingService;
}
public void sayHello() {
System.out.println(greetingService.getGreeting() + " " + "Hello, Spring!");
}
}
缺点: 依赖可以在对象创建后被修改,不够安全。而且,如果忘记调用Setter,对象可能处于无效状态。
2.3 字段注入(Field Injection)—— 不推荐!
这是很多教程和老项目里常见的写法,看起来最简洁:
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HelloWorld {
// 字段注入
@Autowired
private GreetingService greetingService;
public void sayHello() {
System.out.println(greetingService.getGreeting() + " " + "Hello, Spring!");
}
}
为什么我不推荐?
- 隐藏依赖: 你看不出
HelloWorld依赖了谁,除非你深入看代码。构造器注入则一目了然。 - 难以测试: 如果要单元测试
HelloWorld,你必须启动Spring容器,或者用反射来注入Mock对象,非常麻烦。 - 循环依赖风险: 字段注入更容易导致循环依赖问题(A依赖B,B依赖A),虽然Spring能解决一部分,但这始终是代码坏味道。
- 违反单一职责: 对象的创建和依赖注入混在一起,不够纯粹。
总结一下: 除非有非常特殊的理由(比如依赖是可选的,且需要动态更改),否则请一律使用构造器注入。这是写高质量、可维护Spring代码的第一步。
三、 实战:构建一个RESTful API
学会了DI,咱们来点实际的。现在企业开发,几乎都离不开REST接口。Spring MVC(现在通常和Spring Boot一起用)让构建REST API变得异常简单。
3.1 项目结构搭建
假设我们要做一个简单的“用户管理”接口。首先,确保你的pom.xml里有Spring Web的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 其他依赖... -->
</dependencies>
推荐使用Spring Boot,因为它帮你屏蔽了大部分复杂的配置,让你能专注于业务逻辑。我们的项目结构大概是这样的:
src/main/java/com/example/userapi/
├── UserApiApplication.java // 启动类
├── controller/
│ └── UserController.java // 控制器
├── service/
│ └── UserService.java // 服务层
├── repository/
│ └── UserRepository.java // 数据访问层(这里用模拟数据)
├── model/
│ └── User.java // 实体类
└── dto/
└── UserDto.java // 数据传输对象
3.2 定义实体和数据传输对象(DTO)
先定义一个User实体,和一个UserDto。为什么要分两个?因为实体通常包含数据库相关字段(如ID、创建时间),而DTO只包含前端需要的数据,这样更安全,也更具灵活性。
// model/User.java
package com.example.userapi.model;
public class User {
private Long id;
private String name;
private String email;
// 构造函数、Getter、Setter省略...
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// ...
}
// dto/UserDto.java
package com.example.userapi.dto;
public class UserDto {
private Long id;
private String name;
private String email;
// 构造函数、Getter、Setter省略...
public UserDto(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// ...
}
3.3 构建Service层
Service层负责处理业务逻辑。这里我们用一个简单的List来模拟数据库。
// service/UserService.java
package com.example.userapi.service;
import com.example.userapi.dto.UserDto;
import com.example.userapi.model.User;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
@Service // 告诉Spring这是一个服务组件
public class UserService {
// 模拟数据库
private final List<User> users = new ArrayList<>();
private final AtomicLong idGenerator = new AtomicLong(1);
// 获取所有用户
public List<UserDto> getAllUsers() {
return users.stream()
.map(user -> new UserDto(user.getId(), user.getName(), user.getEmail()))
.toList();
}
// 根据ID获取用户
public Optional<UserDto> getUserById(Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.map(user -> new UserDto(user.getId(), user.getName(), user.getEmail()))
.findFirst();
}
// 创建用户
public UserDto createUser(UserDto userDto) {
Long newId = idGenerator.getAndIncrement();
User newUser = new User(newId, userDto.getName(), userDto.getEmail());
users.add(newUser);
return new UserDto(newUser.getId(), newUser.getName(), newUser.getEmail());
}
// 更新用户
public Optional<UserDto> updateUser(Long id, UserDto userDto) {
Optional<User> existingUserOpt = users.stream()
.filter(user -> user.getId().equals(id))
.findFirst();
if (existingUserOpt.isPresent()) {
User existingUser = existingUserOpt.get();
existingUser.setName(userDto.getName());
existingUser.setEmail(userDto.getEmail());
return Optional.of(new UserDto(existingUser.getId(), existingUser.getName(), existingUser.getEmail()));
}
return Optional.empty();
}
// 删除用户
public boolean deleteUser(Long id) {
return users.removeIf(user -> user.getId().equals(id));
}
}
3.4 构建Controller层
Controller层负责接收HTTP请求,调用Service,并返回响应。
”`java // controller/UserController.java package com.example.userapi.controller;
import com.example.userapi.dto.UserDto; import com.example.userapi.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.Optional;
@RestController // 表明这是一个控制器,且所有方法的返回值都会直接序列化为JSON @RequestMapping(“/api/users”) // 所有方法的路径前缀 public class UserController {
private final UserService userService;
// 构造器注入Service
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
// GET /api/users - 获取所有用户
@GetMapping
public List<UserDto> getAllUsers() {
return userService.getAllUsers();
}
// GET /api/users/{id} - 根据ID获取用户
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
Optional<UserDto> user = userService.getUserById(id);
if (user.isPresent()) {
return ResponseEntity.ok(user.get());
} else {
return ResponseEntity.notFound().build();
}
}
// POST /api/users - 创建用户
@PostMapping
public ResponseEntity<UserDto> createUser(@
