深入浅出Spring Boot

一.Spring Boot和Spring是什么关系,Spring Boot的出现意义何在?

Spring Boot 不是替代 Spring 的新框架,而是 Spring 的"懒人包"或"脚手架"。它的出现,对于小白而言,能够轻松上手,简化开发流程,跳过复杂的配置概念,能快速做出一个 Web 接口,获得成就感。对于高手而言,不用再写样板化配置,能专注于业务逻辑、架构设计,团队协作时项目结构统一。可以简单理解为:Spring Boot=内置tomCat+自动配置。下面举一个创建一个简单的 Web 接口 "Hello World"的例子,来对比一下Spring与Spring Boot的区别。

使用原生 Spring
  1. 配置 pom.xml:你要手动引入 Spring Core、Spring MVC、Servlet API、Jackson......还要自己确定版本。

  2. 配置 web.xml :这是一个 XML 文件,你需要在这里配置 DispatcherServlet

  3. 配置 spring-servlet.xml:又一个 XML 文件,配置组件扫描、视图解析器等。

  4. 编写 Controller

    java 复制代码
    package com.demo.controller;
    // ... 一堆 import
    @Controller
    public class HelloController {
        @RequestMapping("/hello")
        @ResponseBody
        public String sayHello() {
            return "Hello Spring!";
        }
    }
  5. 部署 :将项目打包成 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-webspring-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 原理的学习总结,内容源自交流梳理,存在不足之处恳请各位读者不吝赐教。相关实践示例项目链接如下:

AOP Bean生命周期

相关推荐
Zella折耳根1 小时前
复习篇-继承和接口
java·开发语言·python
程序员二叉2 小时前
【JVM】OOM详解+JVM参数+FullGC排查+CPU飙高+死锁+内存泄漏+命令大全
java·开发语言·jvm·面试
云烟成雨TD2 小时前
Spring AI 1.x 系列【47】 MCP Annotations 模块
java·人工智能·spring
不知名的老吴2 小时前
线程的生命周期之线程同步
java·开发语言·jvm
协享科技2 小时前
Spring Boot 与 Go 双服务架构实践:从单体拆分到通信设计
java·人工智能·spring boot·后端·架构·golang·ai编程
码语智行3 小时前
地图上图、空间拓扑查询示例
java·arcgis
程序员黑豆3 小时前
AI全栈开发 - Java:变量
java·前端·ai编程
我是一颗柠檬3 小时前
【Java项目技术亮点】分库分表+数据路由策略:单表5000万后的架构升级方案
java·开发语言·分布式·架构
布朗克1683 小时前
25 IO流高级操作——序列化、NIO与Files工具类
java·数据库·io·nio