说实话,刚开始接触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注解。

为什么首选构造器注入?

  1. 不可变性: 你可以在构造器里把依赖赋给final字段,保证这个依赖一旦注入就不能被修改,更安全。
  2. 强制依赖: 如果构造器参数缺失,对象根本无法创建,这避免了“半初始化”状态,让代码更健壮。
  3. 易于测试: 在单元测试中,你可以直接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!");
    }
}

为什么我不推荐?

  1. 隐藏依赖: 你看不出HelloWorld依赖了谁,除非你深入看代码。构造器注入则一目了然。
  2. 难以测试: 如果要单元测试HelloWorld,你必须启动Spring容器,或者用反射来注入Mock对象,非常麻烦。
  3. 循环依赖风险: 字段注入更容易导致循环依赖问题(A依赖B,B依赖A),虽然Spring能解决一部分,但这始终是代码坏味道。
  4. 违反单一职责: 对象的创建和依赖注入混在一起,不够纯粹。

总结一下: 除非有非常特殊的理由(比如依赖是可选的,且需要动态更改),否则请一律使用构造器注入。这是写高质量、可维护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(@