一.Spring Boot和Spring是什么关系,Spring Boot的出现意义何在?
Spring Boot 不是替代 Spring 的新框架,而是 Spring 的"懒人包"或"脚手架"。它的出现,对于小白而言,能够轻松上手,简化开发流程,跳过复杂的配置概念,能快速做出一个 Web 接口,获得成就感。对于高手而言,不用再写样板化配置,能专注于业务逻辑、架构设计,团队协作时项目结构统一。可以简单理解为:Spring Boot=内置tomCat+自动配置。下面举一个创建一个简单的 Web 接口 "Hello World"的例子,来对比一下Spring与Spring Boot的区别。
使用原生 Spring
-
配置 pom.xml:你要手动引入 Spring Core、Spring MVC、Servlet API、Jackson......还要自己确定版本。
-
配置 web.xml :这是一个 XML 文件,你需要在这里配置
DispatcherServlet。 -
配置 spring-servlet.xml:又一个 XML 文件,配置组件扫描、视图解析器等。
-
编写 Controller:
javapackage com.demo.controller; // ... 一堆 import @Controller public class HelloController { @RequestMapping("/hello") @ResponseBody public String sayHello() { return "Hello Spring!"; } } -
部署 :将项目打包成
war文件,放到外部安装好的 Tomcat 的webapps目录下,然后启动 Tomcat。整个过程非常冗长。
使用 Spring Boot
1.配置 pom.xml:只需继承一个父项目和引入一个 starter
XML
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.编写一个主类:这是你的启动入口。
java
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // 这个注解包含了所有关键配置
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
3.编写 Controller:
java
package com.demo.controller;
import org.springframework.web.bind.annotation.*;
@RestController // 一个组合注解,更方便
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello Spring Boot!";
}
}
4.运行 :在 IDE 里直接右键运行 main 方法。启动后,访问 http://localhost:8080/hello 即可。
看到了吗?Spring Boot 帮你省掉了所有的 XML 配置和外部 Web 服务器,代码量骤减。
**Spring Boot 完全依赖于 Spring,**你车里的发动机、轮胎依然是 Spring 提供的。Boot 只是"封装"了它,并没有重新发明轮子。
Spring Boot 是 Spring 的入口 :现在学习 Java 后端开发,几乎都是从 Spring Boot 开始的。你其实在使用 Spring Boot 的过程中,就已经在使用 Spring 的 IoC、AOP 等各种核心功能了。
自动配置是桥梁 :Boot 的
@SpringBootApplication注解背后是@EnableAutoConfiguration(开启自动配置)。它会根据你pom.xml里引入的依赖(比如spring-boot-starter-web),自动去配置 Tomcat、SpringMVC,把你之前需要手写的那些 XML 全部在后台帮你做好了。总的来说,Spring 是提供能力的"核心库",Spring Boot 是让你能"快速、轻松、零配置"地使用这些核心库的"启动器和工具集"。 你可以把 Spring Boot 看作是 Spring 官方向开发者推荐的最佳实践集合。
二者之间的对比如下图所示:

二.Spring Boot的AOP和IOC
首先来说说IOC
IOC 是将对象的创建、装配、生命周期等控制权交给 Spring 容器;依赖注入是实现这一机制最核心的方式。依赖注入的核心机制可以简化为下面的表达式:
依赖注入 = Spring容器(ConcurrentHashMap) + 反射(构造函数/字段赋值)
这里为什么要用反射呢?
因为在编写底层框架代码的时候,不能知道使用者会使用什么类型的对象和什么类,所以,要获取信息只能通过反射。比如 Spring 框架 和 JUnit 框架 ,它们编写时完全不知道你要创建
UserService类还是OrderService类,也不知道你要调用的是deleteOrder()方法 还是deleteOrder()方法。它们发只能通过反射,在你运行程序时:读取你给的配置(类名、方法名的字符串),动态加载你的类,动态创建对象,动态调用你的方法。
这里的ConcurrentHashMap是什么呢?
ConcurrentHashMap是 Java 提供的线程安全的HashMap,专门用于多线程环境。Spring 容器(可以想象成一个超级工厂)在启动时,会扫描所有加了@Component,@Service等注解的类,然后替你把对象 new 出来 。创建出来的对象不会"丢",而是被放在一个容器(一个大 Map) 里,也就是ConcurrentHashMap。默认情况下,它是单例的(一个类只有一个对象,并且这里是饿汉式单例模式,也就是在类编写好了之后,就会实例化一个对应的对象,而不是等到使用的时候才实例化)。当发现某个类需要依赖另一个类时(比如UserController需要一个UserService,如@Autowired注解时),Spring 会从它的大 Map 里找到 对应的对象,并通过反射把它赋值过去(完成赋值和初始化)。
这里提一下Bean(对象)的生命周期:
Bean 是受 Spring 容器管理的对象,有特殊的生命周期和作用域
实例化 → 属性赋值 (依赖注入)→ 初始化 → 使用→ 销毁
Bean的生命周期分为五个阶段:实例化(通过构造器反射创建空对象)、属性赋值(扫描@Autowired等注解完成依赖注入)把对象塞到使用注解的地方、初始化(依次执行@PostConstruct、afterPropertiesSet、init-method,可做检查和预热)、使用(正常调用)、销毁(容器关闭时依次执行@PreDestroy、destroy、destroy-method,释放资源)。注意销毁仅在正常关闭容器时触发。

这里还有一个问题,为什么是先实例化,依赖注入过以后才初始呢?这里要涉及到循环依赖问题,后面再说。
这里通过一段伪代码来感受一下Spring Boot的IOC是怎么实现的
java
// 1. 模拟 Spring 的容器 (就是一个 Map)
public class SimpleSpringContext {
private static Map<Class<?>, Object> container = new ConcurrentHashMap<>();
// 2. 启动时扫描和初始化 (只有 Controller 和 Service)
public static void start() throws Exception {
// 假设扫描到了 Service
container.put(UserService.class, new UserService());
// 假设扫描到了 Controller,并发现它需要 UserService
Class<?> clazz = UserController.class;//在堆区创建一个唯一的 Class 对象(由类加载器完成),这个对象里包含了 UserService 这个类的全部元数据,包括:类名,包名,字段,方法,依赖...
Object controller = clazz.getDeclaredConstructor().newInstance(); // 🌟 反射创建
// 获取 controller 里的所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 发现这个字段需要注入 (模拟 @Autowired)
if (field.getName().equals("userService")) {
// 从容器 Map 里拿到依赖的对象
Object dependency = container.get(UserService.class);
field.setAccessible(true); // 🌟 突破 private,临时跳过权限控制,可以访问private修饰的数据
field.set(controller, dependency); // 🌟 反射赋值 (这就是注入!)
break;
}
}
// 把装配好的 controller 也放进容器
container.put(UserController.class, controller);
}
}
fields数组包含了 controller 中声明的所有字段信息。在循环中找到了名为userService的字段后,通过UserService.class作为 key,从容器 Map 中取出已经创建好的 UserService 实例 。然后通过field.set(controller, dependency)把这个实例注入到 controller 中。最后,把装配好的 controller 实例也放入容器,供其他地方使用。
userService.class 获取的不是"名字字符串",也不是"所有信息的集合",而是 Class 类型的对象,这个对象里面封装了该类的完整结构信息。它作为一个"元数据句柄",指向 JVM 内部该类的完整描述。容器的 Map<Class<?>, Object> 是用"这个句柄"当 key,快速拿到真实的 Bean 实例。

java
// 第一步:容器里已经有一个 UserService 对象了(之前 put 进去的)
container.put(UserService.class, new UserService());
// ↑ ↑
// 钥匙(Class对象) 值(Service实例)
// 第二步:需要注入时
Object dependency = container.get(UserService.class);
// ↑ ↑
// 拿到的就是那个Service实例 用相同的钥匙去取
这里的dependency在spring 里面是通过getType()来获取类型的
java
Class<?> fieldType = field.getType(); // 得到 UserService.class
Object dependency = container.get(fieldType); // 效果一样
从上面可以看出,Spring 默认在容器启动时就把所有单例 Bean 通过反射创建好了,放在 ConcurrentHashMap 里。当你需要时,直接从 Map 里拿,不用再 new。
这里还有一个问题,为什么要设计成默认单例模式?
因为 UserService 通常没有自己的状态 (无成员变量或只有只读依赖),所以一个实例足够所有地方使用,没必要创建多个。
理解了上面所说的,你可能还有以下误区

总结
当你运行Spring Boot时,先启动内嵌的tomcat,扫描@SpringBootApplication,接着,创建Spring容器,执行Bean生命周期,注册DispatcherServlet,监听端口,最后等待请求。Spring Boot靠
spring-boot-starter-web、spring-boot-starter-data-jpa这类 starter 包 + 大量的@Conditional条件注解,帮你做了"绝大多数情况下的默认选择"。通过启动时自动读取这些外部配置(application.yml / application.properties)来覆盖默认行为。
接下来,来谈一谈AOP
aop是面向切面编程,AOP 可以在不修改源码的情况下,给方法增加额外行为(日志、事务、权限等)。Spring AOP 底层用的是动态代理, AOP 会帮你生成一个代理对象,在调用目标方法前后插入额外逻辑。
为什么要懂AOP?
因为Spring 里的@Transactional(事务),@preAuthorize(权限),@Async(异步),@Cacheable(缓存)这些功能都基于 AOP。不懂 AOP,就不懂 Spring 一半的便利从哪来。
一个简单的使用场景
java
@Service
public class UserService {
public void saveUser() {
// 核心业务:保存用户
System.out.println("保存用户");
}
}
// 你想在 saveUser 执行前后加日志,但不改 UserService 源码
用AOP实现
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.demo.UserService.saveUser(..))")
public void beforeSave() {
System.out.println("保存前:记录日志");
}
@After("execution(* com.demo.UserService.saveUser(..))")
public void afterSave() {
System.out.println("保存后:记录日志");
}
}
用法:
使用 @Before 和 @After 注解时通过execution(*com.demo.UserService.saveUser(..))定义切入点(Pointcut),指定该方法被增强的位置与范围。被 @Aspect 注解的类称为切面(Aspect),它封装了多个通知(Advice,如 @Before/@After),用于将重复的逻辑(如日志、事务)抽取为可复用的公共代码块。
切点表达式语法
execution(修饰符 返回类型 包名.类名.方法名(参数))
常见的通配符

常用表达式
java
// 1. 匹配 UserService 中的所有方法
@Pointcut("execution(* com.demo.UserService.*(..))")
// 2. 匹配 service 包下所有类的所有方法
@Pointcut("execution(* com.demo.service.*.*(..))")
// 3. 匹配 service 包及子包下所有类的所有方法
@Pointcut("execution(* com.demo.service..*.*(..))")
// 4. 匹配带 @GetMapping 注解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
// 5. 匹配 save 开头的方法
@Pointcut("execution(* *..*.save*(..))")
AOP 最底层的原理:动态代理 + 方法拦截
下面写一个简单的AOP动态代理伪代码来感知一下它的原理
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class SimpleAopDemo {
// 目标对象:要被增强的业务类
static class UserService {
public void saveUser(String name) {
System.out.println("【核心业务】保存用户:" + name);
}
}
// 代理工厂:给目标对象生成一个代理对象
public static Object createProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// @Before 逻辑
System.out.println("【代理】方法执行前,参数:" + args[0]);
// 执行目标方法
Object result = method.invoke(target, args);
// @After 逻辑
System.out.println("【代理】方法执行后");
return result;
}
}
);
}
public static void main(String[] args) {
UserService target = new UserService();
// ⚠️ 注意:UserService 没有实现接口,这里需要改为 CGLIB,上面只演示思路
// 实际运行会报错,这只是展示 AOP 的核心思想
UserService proxy = (UserService) createProxy(target);
proxy.saveUser("小明");
}
}
如果像上面那样给UserService的saveUser方法使用AOP添加before和after,当我们调用saveUser方法时,spring会自动生成代理对象,执行代理方法before和after,把saveUser方法放在中间。专业表述为:Spring AOP 会为目标对象(UserService)动态生成代理对象,并将切面中的通知(Advice,如 @Before、@After)织入到代理逻辑中。当调用 saveUser 方法时,实际执行的是代理对象的方法,该代理会按照通知顺序,先执行 @Before 逻辑,再调用目标方法(即原始的 saveUser 业务),最后执行 @After 逻辑。
1️⃣ 扫描切面↓
找到 @Aspect 注解的类,解析里面的 @Before、@After、@Around
↓
2️⃣ 匹配切点
↓
检查每个 Bean 的方法是否匹配切点表达式(如 execution(* com.demo.service.*.*(..)))
↓
3️⃣ 创建代理
↓
匹配上的 → 创建代理对象
没匹配上 → 返回原始对象
三.一些常见面试问题
1.@Autowired 和 @Resource 的区别
@Autowired 是 Spring 的;@Resource 是 Java 标准的。前者按类型注入,后者按名称注入。
核心区别

具体场景
java
有两个Service类继承同一个service
@Service
public class UserServiceA implements UserService {}
@Service
public class UserServiceB implements UserService {}
//使用 @Autowired(按类型)
@RestController
public class UserController {
@Autowired
// ❌ 报错:因为有两个 UserService 类型的 Bean,Spring 不知道选哪个
private UserService userService;
}
//解决办法1:用 @Qualifier 指定名字
@RestController
public class UserController {
@Autowired
@Qualifier("userServiceA")
private UserService userService;
}
/*@Qualifier()里面的是Bean的名称
默认情况下,Bean 名称是类名首字母小写(如 userServiceImpl → "userServiceImpl")
*/
/*解决办法2:使用@Resource
使用 @Resource(按名称)*/
@RestController
public class UserController {
@Resource(name = "userServiceB")
private UserService userService;
}
//如果不写 name,默认按字段名去查找:userService → 找叫 userService 的 Bean
/*@Resource 找不到会不会按类型找?
不会。它会降级尝试按类型找吗?不会直接降级。
行为更准确是:先 byName → 找不到且 name 是自动生成的 → 再 byType(容易混乱),所以不要依赖它的降级行为,明确写 name*/
扩展
方法3:使用@Primary设置默认首选
@Parimary
@Service
public class UserServiceA implements UserService {}
@RestController
public class UserController {
@Autowired
private UserService userService;//默认注入UserServiceA
}
//注意@Primary的优先级比@Qualifier低

2.@Bean 和 @Component 的区别
@Component 加在类上,@Bean 加在方法上。前者 Spring 帮你创建对象,后者你告诉 Spring 如何创建对象。自己的类用 @Component,第三方的类用 @Bean。
使用场景
java
// 方式1:@Component(Spring 自动实例化)
@Component
public class UserService {
// Spring 会调用无参构造器创建对象
}
// 方式2:@Bean(你手动 new)
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
// 你亲自创建对象,还可以做额外配置
RestTemplate rt = new RestTemplate();
rt.setConnectTimeout(5000);
return rt;
}
}
/*这里的RestTemplate 是 Spring 提供的类,我们不想要spring自动构建对象,想要自己创建,设置一些字段的值,就需要用到@Bean加在方法前面。*/
3.什么时候自己new对象,什么时候用依赖注入。

java
1)应该new的场景
// 1. 简单的数据对象(DTO、VO、Entity)
UserDTO userDTO = new UserDTO();
userDTO.setName("张三");
// 2. 工具类(无状态,不需要 Spring 管理)
MathUtil mathUtil = new MathUtil();
int sum = mathUtil.add(1, 2);
// 3. 方法内部的临时变量
public void doSomething() {
List<String> list = new ArrayList<>(); // 局部变量,用完就丢
list.add("a");
}
// 4. 多例场景(每次都需要新对象,且不需要 AOP)
Order order = new Order();
order.setOrderNo("ORD123");
2)不应该new的场景
// 1. 需要事务的方法
// ❌ 错误:自己 new 的 Service,@Transactional 不会生效
UserService userService = new UserService();
userService.saveUser(); // 事务不生效!
// ✅ 正确:从容器中获取
@Autowired
private UserService userService;
// 2. 需要 AOP 增强(日志、权限、缓存等)
// 自己 new 的对象,切面不会生效
// 3. 需要依赖其他 Bean 的对象
// 自己 new 的话,里面的 @Autowired 字段全是 null
4.如何解决循环依赖
什么是循环依赖?
java
@Component
public class A {
@Autowired
private B b; // A 依赖 B
}
@Component
public class B {
@Autowired
private A a; // B 依赖 A
}
Spring 怎么解决的?
三级缓存 + 提前暴露半成品对象
XML
1. 开始创建 A → 实例化 A(半成品,属性为 null)
2. 把半成品的 A 提前暴露到缓存
3. 开始注入 A 的属性 b → 发现需要 B
4. 开始创建 B → 实例化 B
5. B 需要注入 a → 从缓存拿到半成品的 A
6. B 完成注入 → B 成为成品
7. 回到 A,把 B 注入进去 → A 成为成品
Spring 解决循环依赖的关键,正是把实例化和依赖注入这两个步骤拆开了 ,而不是混合在一起做。这也就是为什么要先实例化,依赖注入后,才初始化。而不是先实例化,初始化了之后才依赖注入的原因。
四.写在最后
本文为笔者初次探究 Spring Boot 原理的学习总结,内容源自交流梳理,存在不足之处恳请各位读者不吝赐教。相关实践示例项目链接如下:
