SpringBoot八股文

SpringBoot的项目结构是怎么样的?

image-20251120165638699

image-20251120165714585

image-20251120165746866


说一说Spring框架核心特性有哪些? / 好处有哪些?

  1. IOC容器:Spring通过控制反转(DI)实现了对象的创建和对象间的依赖关系,开发者只需要定义好Bean及其依赖关系,Spring容器就会负责创建和组装这些对象

  2. AOP:面向切面编程,允许开发者横切关注点,例如事务管理、安全控制、日志记录等,可以提高代码的可维护度和可重用性

  3. 事务管理:Spring提供了一致的事务管理接口。开发者可以轻松地进行事务管理,而无需关心具体的事务API

image-20251120165939412

  1. MVC框架:Spring MVC是一种Web框架,采用了模型-视图-控制器(MVC)框架,它支持灵活的URL到页面控制器的映射

  2. 非侵入式设计:它可以使应用程序对框架的依赖最小化


介绍一下IOC

简介:IOC即控制反转的意思,它是一种创建和获取对象的技术思想依赖注入(DI)是实现这种技术的一种方式。在传统开发过程中,我们需要通过new关键字来创建对象。使用IOC思想开发方式的话,就可以不用new关键字来创建对象,而是通过IOC容器来帮我们完成实例化对象。通过IOC的方式可以大大降低对象之间的耦合度

IOC控制就是对象的创建、初始化、销毁

  • 创建对象:原来是new一个,现在由Spring容器创建;
  • 初始化对象:原来是对象自己通过构造器或者setter方法给依赖的对象赋值,现在是由Spring容器自动注入;
  • 销毁对象:原来是直接给对象赋值null或做一些销毁操作,现在是Spring容器管理生命周期负责销毁对象

IOC的实现机制

  • 反射:Spring IOC容器利用Java的反射机制动态的加载类信息、创建对象实例以及调用对象方法,反射允许在运行时检查类、方法、属性等信息,有利于灵活的实现对象实例化和管理

  • 依赖注入:IOC的核心概念是依赖注入,也就是容器负责应用程序组件之间的依赖关系。

  • 设计模式-工厂模式:Spring IOC容器通常使用工厂模式来管理对象的创建和生命周期;容器作为工厂负责实例化Bean并管理它们的生命周期,将Bean的实例化过程交给容器来管理

  • 容器实现:Spring IOC容器是实现IOC的核心,通常使用BeanFactory或ApplicationContext来管理Bean。BeanFactory是IOC容器的基本形式,提供基本的IOC功能;ApplicationContext是BeanFactory的扩展,并提供更多企业级功能


什么是Bean?

  • Bean是对象,一个或者多个

  • Bean托管在Spring中一个叫IOC的容器中

  • 我们的程序是由一个个Bean构成的


什么是FactoryBean?

  • FactoryBean是Spring所提供的一种比较灵活的创建Bean的方式,可以通过实现FactoryBean接口中的getObject()方法来返回一个对象,这个对象就是最终的Bean对象;

  • 如果一个对象实现了这个接口,那么它就成为一种特殊的Bean,被注册到IOC容器后,如果调用这个对象的 getBean 获得的是 FactoryBean

image-20251120170331542

  • 为什么要有FactoryBean呢? FactoryBean机制被广泛的应用在Spring内部和Spring与第三方框架或组件的整合过程中;假设依赖一个第三方类A,创建这个类的对象比较复杂,同时我们想设置这个类中的某些属性,但是这个类没有提供设置的方法,就可以用一个Bean来封装它

image-20251120170357123

image-20251120170414747


说一说BeanFactory和ApplicationContext

  1. BeanFactory是一个接口,里面定义了getBean()这一个方法,通过实现这个方法可以用于管理Bean;而ApplicationContext它实现了BeanFactory的实现接口,也属于一个BeanFactory,在ApplicationContext中扩展了很多其他功能,AOP、国际化、资源管理、事件、注解等

  2. 普通实现BeanFactory接口类,如DefaultListableBeanFactory的优缺点

    image-20251205085118564

    • 优点:应用启动时占用资源很少

      • 缺点:运行速度相对来说慢一点,有可能出现空指针异常的错误
  3. ApplicationContext的优缺点

    image-20251120170521136

    image-20251120170537279

    image-20251205084627017

    • 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快

      • 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,会导致内存占用大

介绍一下DI

DI 是实现 IOC 的方式,通过构造器注入、Setter 注入或字段注入,由容器把依赖对象主动注入进去,让对象不再自己创建依赖,从而实现控制反转。让Spring帮我们管理对象之间的依赖关系

几种注入的方式

  • 构造器注入(推荐使用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Service
    public class OrderService {

    private final UserService userService;

    public OrderService(UserService userService) {
    this.userService = userService;
    }
    }

优点:

  1. 使用final代表注入以后永远不会变
  2. 不可能出现“忘记注入”的情况,因为如果忘记了,编译阶段会报错,缺少构造函数的参数;
  3. 不会出现空指针异常
  4. 方便单元测试
1
2
3
4
5
6
7
8
// 如果不加构造函数,就不能修改userService的值

@Service
public class OrderService {

@Autowired
private UserService userService;
}
1
2
3
4
5
6
// 通过构造注入
OrderService os = new OrderService(new FakeUserService());
// 还可以插入:
// mock
// fake
// 测试想测试的依赖
  1. 更符合依赖倒置原则(DIP)
  2. 会在spring启动阶段暴露循环依赖的问题

  • Set方法注入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class OrderService {

    private UserService userService;

    @Autowired
    // 这个注解:告诉 Spring,请用这个方法,把依赖注入到这个 Bean 里。
    // Spring会找到 IOC 容器里名字是 UserService 的 Bean
    // 将它作为参数传给setUserService()
    public void setUserService(UserService userService) {
    this.userService = userService;
    }
    }

    缺点:

    1. 对象初始化不完整
    2. 可能导致空指针异常

  • 注解注入

    1
    2
    3
    4
    5
    6
    @Service
    public class OrderService {

    @Autowired
    private UserService userService;
    }

    缺点:

    1. 无法测试(不能new以后手动注入)
    2. 会导致循环依赖难以排查

没有 DI 的话,IOC 为什么无法落地?

因为 IOC 的核心是让对象不再自己创建依赖,而 DI 正是把依赖“注入”进去的机制。如果没有 DI,容器就没办法把依赖交给对象,那对象还是得自己 new,控制权就无法反转,所以 IOC 根本落不了地。


介绍一下AOP

  1. 简介:AOP是面向切面编程,在不改动代码的情况下,给某段逻辑“加功能”

  2. 在面向切面编程的思想里面,把功能分为两种,一种是核心业务,比如登陆、注册、增、删、改、查都叫核心业务;另一种是周边功能,比如日志、事务管理这些次要的就是周边业务;在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的,通过AOP可以将核心业务和周边功能“编织”在一起,这样可以减少系统的重复代码,降低模块之间的耦合度,有利于未来的可拓展性和可维护性

  3. AOP实现机制

    • Spring AOP的实现依赖于动态代理技术。动态代理是在运行时动态生成代理对象。它允许开发者在运行时指定要代理的接口和行为,从而实现在不修改源码的情况下增强方法的功能。

image-20251120170740930


动态代理

动态代理是什么?

一种在运行时动态创建代理对象的机制,主要用于在不修改原始类的情况下对方法调用进行拦截和增强

  • 基于接口的代理(JDK动态代理):这种类型的代理要求目标对象必须实现至少一个接口
1
2
3
class UserServiceImpl implements UserService {}  // OK

class UserServiceImpl {} // ❌ JDK Proxy 无法代理

它只能代理这个接口里声明的方法

1
2
3
4
// 接口类
public interface UserService {
void save();
}
1
2
3
4
5
// UserServiceImpl类
class UserServiceImpl implements UserService {
public void save() {} // 可以被代理
public void internalLogic() {} // 不能被代理
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 切面类,完成AOP通知类型
@Aspect
@Component
public class LogAspect {

@Before("execution(* com.xxx.UserService.save(..))")
public void before() {
System.out.println("before");
}

@After("execution(* com.xxx.UserService.save(..))")
public void after() {
System.out.println("after");
}
}

1
2
3
4
5
6
7
8
9
10
// JDK Proxy自动生成代理类的结构就是
class $Proxy11 implements UserService {
InvocationHandler h;

@Override
public void save() {
// before / after
h.invoke(...)
}
}

说明】:当从 Spring 容器中获取到 UserService Bean 并调用 save() 方法时,实际拿到的是一个 JDK 动态代理对象 $Proxy11,而不是UserServiceImpl 本体。

当调用代理对象中的save()方法的时候,代理对象内部再去调 目标对象 UserServiceImpl.save(),在save()方法中又会根据自定义切面类LogAspect中的AOP切面通知处理相应的逻辑

  • 基于类的代理(CGLIB动态代理):当目标对象没有实现接口的时候,会创建一个继承这个类的子类,在这个子类中可以代理目标类的所有非 final 方法

动态代理和静态代理的区别?

代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法

  • 静态代理:代理类是程序员自己写的,在代码编译时就确定了被代理的类是一个静态代理。静态代理通常只代理一个类;

  • 动态代理:一种在运行时动态创建代理对象的机制,主要用于在不修改原始类的情况下对方法调用进行拦截和增强


能用静态代理的方式实现AOP吗?

可行的,但是在实际生产中基本没有人使用,因为有三大硬伤

  • 第一是代码爆炸:如果有100个Service类需要加事务,就需要写100个重复的try-catch提交回滚代码,维护起来很困难

  • 第二是僵化:一旦给业务接口改了个方法名,所有相关的代理类都得跟着改方法名

  • 第三是无法动态筛选:比如你想只给带 @Transactional 注解的方法加事务,静态代理只能写死逻辑,而 Spring AOP 可以在运行时通过切点表达式精准匹配需要增强的方法

例子

1
2
3
4
5
6
7
8
9
public interface UserService {
void save();
}

public class UserServiceImpl implements UserService {
public void save() {
System.out.println("save user");
}
}

自己手写的代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserServiceProxy implements UserService {

private UserService target;

public UserServiceProxy(UserService target) {
this.target = target;
}

@Override
public void save() {
System.out.println("before...");
target.save();
System.out.println("after...");
}
}

使用方法

1
2
UserService userService = new UserServiceProxy(new UserServiceImpl());
userService.save();

反射

  1. 反射指的是在程序运行状态下,对于任意一个类,都可以知道这个类的所有属性和方法,都能调用它的所有属性和方法,Java的反射机制允许在运行时获取类的信息并动态操作对象,即使在编译时不知道具体的类也能实现

  2. 反射有以下几个特性

    • 运行时类信息方法:反射允许程序在运行时获取类的所有信息,属性、方法

    • 动态对象创建:可以使用反射API,Class类的newInstance()方法,动态的创建对象实例,即使在编译时不知道具体的类名

    • 动态方法调用:通过Method类的invoke()方法可以在运行时动态调用对象的方法,包括私有方法

    • 访问和修改字段值:通过Field类的get()和set()方法可以在程序运行时访问和修改对象的字段值,即使是私有的

  3. Spring框架的依赖注入(DI)和控制反转(IOC)

    • 依赖注入(DI):在Spring中,程序员可以通过XML配置文件或者基于注解的方式声明组件之间的依赖关系,当程序运行启动时,Spring容器会扫描这些配置或注解,然后利用反射来实例化Bean(也就是Java对象),并根据配置自动状态它们的依赖

    • 控制反转(IOC):当一个Service类需要依赖另一个DAO类,程序员可以在Service类中使用@Autowired注解,无需自己编写DAO实例的代码,Spring容器在运行解析这个注解,通过反射找到对应的DAO类,实例化它并管理它,将其注入到Service类中。降低了组件之间的耦合度,增强可维护性

  4. 动态代理中反射的应用

Spring 想要在所有方法调用前后统一加上日志,所以它不会让你在每个业务方法里手动写日志逻辑。它会在运行时用 JDK 动态代理或 CGLIB 生成一个“代理对象”。

真正的方法调用不会直接找原对象,而是先进入代理对象。代理对象会通过 反射 调用目标方法,并在调用前后插入日志逻辑。

整个过程都是在运行时动态完成的,不需要你修改任何业务代码。

  1. Class对象

Class对象不是一个类的实例,而是JVM 里记录该类全部结构信息的数据结构。

一个类被加载(loading过程)的是通过加载字节码 → JVM 创建 Class 对象 → 放入方法区,反射发生在类加载之后、运行阶段(runtime)

通过这个Class对象,我们可以知道里面所有的内容,包括方法,属性

类加载负责把字节码读进 JVM,并创建唯一的 Class 对象;

反射就是利用这个 Class 对象里保存的类结构(方法、字段、构造器)在运行时做动态操作,比如调用方法、访问属性。

1
2
3
4
5
6
7
8
9
10
11
12
     JVM 内存
┌─────────────────┐
│ 方法区/元空间 │
│ ┌─────────────┐ │
│ │ Class对象 │ │ ← 唯一的
│ └─────────────┘ │
└─────────────────┘

堆区对象:
new UserService() → 对应同一个 Class
new UserService() → 对应同一个 Class
new UserService() → 对应同一个 Class

依赖倒置,依赖注入,控制反转分别是什么?

  1. 控制反转(IOC):“控制”指的是对程序的执行流程控制,而“反转”指的是在没有使用框架之前,程序员自己控制对象的生命周期。在使用框架后,对象的创建控制权交给了Spring

  2. 依赖注入(DI):不通过new的方式在类内部创建依赖类的对象,Spring会将已经创建好的对象给我们

  3. 依赖倒置:高层模块(高管)不依赖底层模块(员工),它们共同依赖同一个抽象类,可以类似于一种设计模式的设计指南,目的都是为了“解耦”

image-20251120171735051


Spring时如何解决循环依赖的?

什么是循环依赖?

循环依赖指的是两个类中的属性相互依赖对方:例如A类中有B属性,B类中有A属性,从而形成了一个依赖闭环,如下图

  • 相互依赖

image-20251120203655462

  • 三者之间及其以上的依赖

image-20251120203711754

  • 自我依赖

image-20251120203726168


循环依赖问题在Spring中的三种主要情况

  • 第一种:通过构造方法进行依赖注入时产生的循环依赖问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Bean A,构造方法依赖 B
@Component
class A {

private B b;

public A(B b) {
this.b = b;
}
}

// Bean B,构造方法依赖 A
@Component
class B {

private A a;

public B(A a) {
this.a = a;
}
}
  • 第二种:通过setter方法(使用了@Scope注解)进行依赖注入且是在多例(原型)模式下产生的循环依赖问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 原型 Bean C,Setter 依赖 D
@Component
// 把这个 Bean 的作用域改成 prototype(原型模式):
// 每次注入、每次 getBean(),都会创建一个新的实例。
@Scope("prototype")
class C {

private D d;

@Autowired
public void setD(D d) {
this.d = d;
}
}

// 原型 Bean D,Setter 依赖 C
@Component
@Scope("prototype")
class D {

private C c;

@Autowired
public void setC(C c) {
this.c = c;
}
}
  • 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 原型 Bean E,Setter 依赖 F
@Component
class E {

private F f;

@Autowired
public void setF(F f) {
this.f = f;
}

public void print() {
System.out.println("E 依赖的 F: " + f);
}
}

// 单例 Bean F,Setter 依赖 E
@Component
class F {

private E e;

@Autowired
public void setE(E e) {
this.e = e;
}

public void print() {
System.out.println("F 依赖的 E: " + e);
}
}

只有【第三种方式】的循环依赖问题被Spring解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常


三级缓存

Spring在DefaultSingletonBeanRegistry类中维护了三个重要的缓存(Map),称为“三级缓存”,"三级缓存"是按 “Bean 在创建流程中的暴露方式” 来划分的

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

// 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

// 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// ...
}
  • singletonObjects(一级缓存):存放的是初始化好的,可以使用的Bean,getBean() 方法返回的也是这里面的Bean,此时Bean已经初始化、实例化、AOP代理(如果需要的话)也已经生成

交付最终完整的Bean

  • earlySingletonObjects(二级缓存):当一个Bean还在创建的过程中(已经实例化了,但尚未完成属性填充和初始化),但它的引用需要被注入到另一个Bean中,就暂时存放到这里

提前曝光过的 Bean(半成品的真实对象)

  • singletonFactories(三级缓存):存放的是Bean的ObjectFactory工厂对象,这是解决循环依赖的关键,当一个Bean被实例化后(刚调完构造函数),Spring会创建一个ObjectFactory并将其放入三级缓存。这个工厂的getObject()方法负责返回该Bean的早期引用

ObjectFactory,用来创建 Bean(可能被AOP代理)


Spring解决循环依赖的流程

  • 第一步:创建BeanA的实例并提前暴露工厂

image-20251120210844768

Spring首先调用BeanA的构造函数进行实例化,此时得到一个原始对象(没有填充属性),紧接着,Spring会将一个特殊的ObjectFactory工厂对象存入三级缓存(singletonFactories)。这个工厂的作用是,当其他Bean需要引用BeanA时,它能动态返回当前这个半成品BeanA,此时BeanA的状态是“已实例化但未初始化”

  • 第二步:填充BeanA的属性时触发BeanB的创建

image-20251120210918060

Spring开始为BeanA注入属性,发现它依赖BeanB。于是容器转向创建BeanB,同样先调用其构造函数实例化,并将BeanB对应的ObjectFactory存入三级缓存。至此,三级缓存中同时存在BeanA和BeanB的工厂,都是未完成品

  • 第三步:BeanB属性注入时发现循环依赖
  1. 定位BeanA在哪

image-20251120211011627

  1. BeanA的引用存入二级缓存,清理三级缓存

image-20251120211030396

当Spring试图填充BeanB的属性时,检测到它需要注入BeanA,此时容器开始依赖查找:在一级缓存中未找到BeanA,在二级缓存中也未命中,最终在三级缓存中定位到BeanA的工厂;Spring立即调用BeanA的getObject() 方法,这个方法如果需要AOP代理,会动态生成代理对象;如果不需要,直接返回原始对象。得到的BeanA早期引用会被放入二级缓存(earlySingletonObjects),同时从三级缓存清理BeanA的工厂。

  • 第四步:完成BeanB的生命周期

image-20251120211052089

BeanB获得所有依赖后,Spring执行其初始化方法,将其转化为可以使用的Bean,随后BeanB被提升至一级缓存,二级缓存、三级缓存由于BeanB的临时数据被清除

  • 第五步:回溯完成BeanA的构建

image-20251120211107968

随着BeanB创建完毕,流程回溯到最初中断BeanA属性注入环节。Spring已经具备能将BeanB实例注入BeanA,接着执行BeanA的初始化方法,最终完成初始化的BeanA会进入一级缓存中


Spring为什么用3级缓存解决循环依赖问题?用2级缓存不行吗?

延迟创建 A 的代理对象(若需要),并把这个代理对象作为“提前暴露的 Bean”提供给别的 Bean 使用。

如果没有三级缓存,Spring 没法动态创建代理,也无法处理“带 AOP 的循环依赖”。


Spring提供了哪些配置方式?

  1. 基于xml配置

bean所需要的依赖项和服务在XML格式的配置文件中定义,这些配置文件通常包含需要bean定义的配置选项

image-20251120211150873

  1. 基于注解配置

可以通过在相关的类,方法或字段声明上使用注解,将bean配置为组件类本身,而不是使用XML来描述bean装配。需要在Spring配置文件中启用它

image-20251120211207534

启动之后,可以基于 Java API 配置,Spring的Java配置是通过使用@Bean和@Configuration来实现

  • @Bean注解扮演与 <bean /> 元素相同的角色

  • @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义Bean间依赖关系

image-20251120211402963

@Bean和@Configuration与**@Component和@Autowired**这两组有什么区别?

1
2
3
4
5
6
7
8
9
10
┌────────────────────────────────────┐
│ 第一组:声明 Bean(创建 Bean) │
│ @Configuration + @Bean │ ← 手动注册 Bean
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 第二组:扫描 Bean(发现 Bean) │
│ @Component + @Autowired │ ← Spring自动扫描 + 自动注入
└────────────────────────────────────┘


将一个类声明为Spring的Bean的注解有哪些?

  1. 我们一般使用 @Autowired 注解自动装配bean,想要把类表示成可用于 @Autowired 注解自动装配的 bean 的类,可以采用以下注解实现

  2. @Component:通用的注解,可以标注任意类为Spring组件,如果一个Bean不知道属于哪个层,可以使用@Component注解标注

  3. @Repository:对应持久层即Dao层,主要用于数据库相关操作

  4. @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层

  5. @Controller:对应Spring MVC控制层,主要用于接收用户请求并调用Service层返回数据给前端页面


Bean一共有几种作用域?

  1. singleton:默认是单例,一个 IOC 容器内部仅此一个

  2. prototype:原型,多实例

  3. request:每个请求都会新建一个属于自己的 Bean 实例,这种作用域仅存在 Spring Web 应用中

  4. session:一个 http session 中有一个 bean 的实例,这种作用域仅存在 Spring Web 应用中

  5. application:整个 ServletContext 生命周期里,只有一个 bean,这种作用域仅存在 Spring Web 应用中

  6. websocket:一个 WebSocket 生命周期内一个 bean 实例,这种作用域仅存在 Spring Web 应用中


Spring中的单例Bean的线程安全问题?

当多个用户同时发送一个请求,容器会给每一个请求分配一个线程,如果这些线程都涉及到对Bean对象的值进行修改,就需要考虑线程同步问题

无状态Bean和有状态Bean

  1. 无状态就是没有可以修改的成员变量,不能保存数据,是线程安全的

  2. 有状态是数据存储功能,可以保存数据,线程不安全

在Spring中无状态的Bean适合用不变模式,使用单例模式策略,这样可以共享实例提高性能;

有状态的Bean,在多线程环境下不安全,我们可以使用线程安全类(ConcurrentHashMap、CopyOnWriteArrayList),也可以使用ThreadLocal


Spring中的Bean生命周期?

image-20251120211827244

具体一点

image-20251120211837461

1. 先通过构造器创建 Bean 实例;

2. 根据属性,注入需要的 Bean;

3. 如果实现了 一些 aware 接口,就执行 aware 注入;

4. 把 bean 实例传递 bean 前置处理器的方法postProcessBeforeInitialization;

5. 调用 bean 的初始化的方法;

6. 再把 bean 实例传递 bean 后置处理器的方法 postProcessAfterInitialization;(AOP代理在这里生成)

7. 这样 bean 就可以使用了;

8. 当容器关闭时候,调用 bean 的销毁的方法


什么是Spring的内部Bean?

只有将 Bean 作用另一个 Bean 的属性时,才能将 Bean 声明为内部 Bean。例如,假设有一个 Student 类,其中引用了 Person 类。代码和配置如下

image-20251120212024751

image-20251120212037048


什么是 Spring 装配?

“装配” 是指 Spring 容器通过依赖注入,将多个 Bean(对象)组合绑定,形成完整业务逻辑单元的过程

举个例子:有 UserService依赖于UserDao,Spring容器通过依赖注入,将UserDao注入到UserService的属性 / 构造器中,让两个Bean形成依赖,这个组合绑定的过程就是Bean装配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserDao {
public void save() {
System.out.println("User saved.");
}
}

public class UserService {
private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void createUser() {
userDao.save();
}
}

UserService 依赖 UserDao,用 XML 配置自动装配它的不同模式

  • no:这是默认设置,表示没有自动装配

image-20251120212117492

  • byName:它根据bean的名称注入对象依赖项

image-20251120212132469

  • byType:它根据类型注入对象依赖项

  • constructor构造函数:它通过调用类的构造函数来注入依赖项

image-20251120212149308

  • autodetect:首先容器会尝试使用构造函数 autowire 装配,如果不行,则尝试通过 byType 自动装配

需要手动扫描才能使用(了解)

1️⃣ 把 XML 放在 resources 目录下(例如:src/main/resources/spring/beans.xml

文件位置你可以自由决定,比如:

1
2
src/main/resources/beans.xml
src/main/resources/spring/beans.xml

这都可以,但重点在下面👇


2️⃣ 再用 @ImportResource 显式告诉 Spring Boot 去加载它

放进去并不会自动生效,你必须在启动类或者配置类中加上:

1
2
3
4
@Configuration
@ImportResource("classpath:beans.xml") // 或 classpath:spring/beans.xml
public class AppConfig {
}

这样 Spring Boot 才会去加载 XML 里的 Bean。


Spring / SpringBoot框架中用到了哪些设计模式?

  • 工厂设计模式:Spring使用工厂模式通过BeanFactory、ApplicationContext创建Bean对象

  • 代理设计模式:Spring AOP功能的设计

  • 单例设计模式:Spring中的Bean默认都是单例的

  • 模板方法模式:Spring中 jdbcTemplate、hibernateTemplate对数据库操作的类,使用了模板模式

  • 包装器设计模式:项目需要连接多个数据库,不同用户在每次访问中根据需求回去访问不同的数据库,这种模式可以让我们根据用户的需求能够动态切换不同的数据源

  • 观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用

  • 适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式


Spring事务有几个隔离级别?

数据库自定义的隔离级别、读未提交、读已提交、可重复度、序列化


spring事务失效的几种场景

  • @Transactional 放在方法上会失效

Spring AOP 默认基于代理(JDK Proxy 或 CGLIB),真正起作用的是代理类,而不是接口本身。

1
2
3
4
5
6
7
8
9
10
11
public interface UserService {
@Transactional // ❌ 不会生效
void save();
}

@Service
public class UserServiceImpl implements UserService {

@Transactional // ✔ 事务生效
public void save() { ... }
}

  • 调用内部方法导致事务失效

你调用 createOrder() → 调用到了代理对象 → 事务生效

createOrder() 内部调用 this.reduceStock() → 没有经过代理 → 事务失效

所以 reduceStock() 并不是在“事务代理”中执行的。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderService {

@Transactional
public void createOrder() {
this.reduceStock(); // 内部调用 ❌ 事务失效
}

public void reduceStock() {
// 扣库存
}
}

  • 异常没有抛出去
1
2
3
4
5
6
try {
insertUser();
throw new Exception();
} catch (Exception e) {
// 异常被吃掉,没有往外抛
}

异常被你 catch + 吞掉 了,导致:

  • 代理类 没有接收到异常,事务无法触发 rollback

所以事务不会回滚。


  • 访问修饰符导致失效

private 方法上的 @Transactional 不会生效,Spring 事务依赖 AOP 代理,而代理只能拦截 public 方法。

1
2
3
4
5
6
7
8
9
@Service
public class OrderService {

@Transactional
public void createOrder() {} // 生效

@Transactional
private void innerMethod() {} // 不生效
}

  • 多线程下事务不生效

Spring 的事务绑定在线程上,新线程不会继承当前线程的事务上下文。

1
2
3
4
5
6
7
8
9
10
@Transactional
public void createOrder() {
new Thread(() -> {
doSomething(); // ❌ 这里没有事务
}).start();

public void doSomething() {
// 执行 SQL
}
}

  • 数据库本身不支持事务(如 MyISAM)

Spring 事务只是帮你“开启事务、提交、回滚”,但真正的事务执行者是数据库。
数据库不支持,Spring 也无能为力。


Spring有哪几种事务传播行为?

  • REQUIRED:(默认) 如果当前存在事务,则用当前事务,如果没有事务则新起一个事务

  • supports: 如果当前存在事务,则用当前事务,如果不存在,则以非事务方式执行

  • mandatory:如果当前存在事务,则用当前事务,如果不存在,则抛出异常

  • requires_new: 总是会创建一个新的事物,如果外层有事务,就挂起外层事务

  • not_supported: 无论外层有没有事务,当前方法都不会在事务里运行,如果外层有事务,就挂起外层事务

  • never: 无论外层有没有事务,当前方法都不会在事务里运行,如果外层有事务,就抛出异常

  • nested: 如果当前事务存在,则在嵌套事务中执行,内层事务依赖外层事务,如果外层失败,整个一起回滚(包括内层),内层失败不影响外层。


Spring MVC工作原理是什么?

image-20251120212315460

  1. 客户端发送请求到 DispatcherServlet,整个MVC的调度中心

  2. DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler

  3. 解析到对应的Handler(也就是常说的Controller层),开始由HandlerAdapter适配器处理

  4. HandlerAdapter会根据Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑

  5. 处理完请求后,会返回一个ModelAndView对象,Model是返回的数据对象,View是这个逻辑上视图名

  6. ViewResolver会根据逻辑View查找实际的View

  7. DispaterServlet会把返回的Model传给View(视图渲染)

  8. 最后,将View返回给客户端


@Component和@Autowired的区别 ?

  1. @Component 让 Spring 创建对象并放进容器。

  2. @Autowired 让 Spring 把容器中的对象引用给我们。

@Autowired等于Spring到容器里面查找有没有这个类型的Bean,如果有就注入一个,如果没有就报错


@Resource和@Autowired的区别?

  1. 注入规则不同

@Autowired是按照类型(byType)注入,先在IOC容器中找到匹配的Bean,如果有多个,再根据字段名按名称匹配

@Resource是根据字段名找同名的Bean,如果找不到再根据类型匹配

@Autowired 先按类型找,@Resource 先按名字找。

  1. 来源不同

@Autowired来自Spring

@Resource来自JDK

  1. Required属性(只有Autowired有)

@Autowired(required = false)表示注入失败也不会报错。

@Resource 没有这个功能。


@Controller注解有什么用?

@Controller 注解标记一个类为 Spring Web MVC 控制器 Controller。Spring MVC 会将扫描到该注解的类,然后扫描这个类下面带有 @RequestMapping 注解的方法,根据注解信息,为这个方法生成一个对应的处理器对象。


@RequestMapping 注解有什么用?

@RequestMapping 注解,配置处理器的 HTTP 请求方法,URI等信息,这样才能将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这个控制器的 URI 前缀


@RestController 和 @Controller 有什么区别?

@RestController 注解,在 @Controller 基础上,增加了 @ResponseBody 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。当然,返回什么样的数据格式,根据客户端的 ACCEPT 请求头来决定。

  • @Controller

image-20251120212448703

  • @RestController

image-20251120212507302


@RequestMapping 和 @GetMapping 注解的不同之处在哪里?

  • @RequestMapping:可注解在类和方法上;@GetMapping 仅可注册在方法上

  • @RequestMapping:可进行 GET、POST、PUT、DELETE 等请求方法;

  • @GetMapping 是 @RequestMapping 的 GET 请求方法的特例,目的是为了提高清晰度。


@RequestParam 和 @PathVariable 两个注解的区别

两个注解都用于方法参数,获取参数值的方式不同,@RequestParam 注解的参数从请求携带的参数中获取,而 @PathVariable 注解从请求的 URI 中获取


返回 JSON 格式使用什么注解?

可以使用 @ResponseBody 注解,或者使用包含 @ResponseBody 注解的 @RestController 注解。

当然,还是需要配合相应的支持 JSON 格式化的 HttpMessageConverter 实现类。例如,Spring MVC 默认使用 MappingJackson2HttpMessageConverter。


Spring Boot

Spring Boot自动装配?

按照各种 @Conditional 条件,自动把你需要的 Bean 装进去,不需要你写 XML 或 @Configuration

1
2
3
4
@SpringBootApplication 
= @SpringBootConfiguration
+ @EnableAutoConfiguration ← SpringBoot装配真正入口
+ @ComponentScan

@EnableAutoConfiguration 会从 spring.factories 中读取所有自带的自动装配类,然后根据 @Conditional 条件决定要不要加载它们。

比如说Tomcat相关的Bean、RedisTemplate相关的Bean


Spring Boot如何使用定时任务?

Spring Boot 定时任务三步走:
1️⃣ 在启动类上开启定时任务:@EnableScheduling
2️⃣ 写一个普通的 @Component
3️⃣ 在方法上加 @Scheduled(...) 注解,写好执行频率

一、开启定时任务功能

在你的 Spring Boot 启动类上加一个注解:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

@EnableScheduling 就是让 Spring Boot 打开“定时任务调度器”。

二、写一个定时任务类

随便建一个类,加上 @Component,让它被 Spring 管理:

1
2
3
4
5
6
7
8
9
@Component
public class MyTask {

// 每 5 秒执行一次
@Scheduled(fixedRate = 5000)
public void printLog() {
System.out.println("定时任务执行中..." + LocalDateTime.now());
}
}

✔ 关键点:

  • 方法一般是 public void不需要参数
  • 类要能被 Spring 扫描到(有 @Component@Service 都行)

为什么使用springboot?/ 比spring好在哪里?

  1. 简化开发:SpringBoot通过提供一系列的开箱即用的组件和自动配置,简化了项目的配置和开发过程,开发人员可以更专注于业务逻辑实现,而不需要花费过多时间在繁琐的配置上

  2. 快速启动:Spring Boot提供了快速的应用程序启动方式,可通过内嵌的Tomcat、Jetty或Undertow等容器快速启动应用程序,无需额外的部署步骤,方便快捷。

  3. 自动化配置:Spring Boot通过自动配置功能,根据项目中的依赖关系和约定俗成的规则来配置应用程序,减少了配置的复杂性,使开发者更容易实现应用的最佳实践。


怎么理解SpringBoot中的约定大于配置?

约定大于配置是SpringBoot的核心设计理念,它通过预期合理的默认行为和项目规范,大幅减少开发者需要手动配置的步骤,从而提升开发效率和项目标准化程度

理解约定大于配置,可以从以下几个方面来解释

  • 自动化配置:Spring Boot 提供了大量的自动化配置,通过分析项目的依赖和环境,自动配置应用程序的行为。开发者无需配置每个细节,大部分常用的配置都已经预设好了。例如,引入spring-boot-starter-web后,Spring Boot会自动配置内嵌Tomcat和Spring MVC,无需手动编写XML。

  • 默认配置:Spring Boot 为诸多方面提供大量默认配置,如连接数据库、设置 Web 服务器、处理日志等。开发人员无需手动配置这些常见内容,框架已做好决策。例如,默认的日志配置可让应用程序快速输出日志信息,无需开发者额外繁琐配置日志级别、输出格式与位置等。

  • 约定的项目结构:Spring Boot 提倡特定项目结构,通常主应用程序类(含 main 方法)置于根包,控制器类、服务类、数据访问类等分别放在相应子包,如com.example.demo.controller 放控制器类,com.example.demo.service 放服务类等。此约定使团队成员更易理解项目结构与组织,新成员加入项目时能快速定位各功能代码位置,提升协作效率。


Spring Boot中如何实现对不同环境的属性配置文件的支持?

Spring Boot支持不同环境的属性配置文件切换,通过创建application-{profile}.properties文件,其中{profile}是具体的环境标识名称。

例如:application-dev.properties用于开发环境,application-test.properties用于测试环境。。如果要想使用application-dev.properties文件,则在application.properties文件中添加spring.profiles.active=dev。


Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,它也是Spring Boot的核心注解,主要包含了以下3个注解:

  1. @SpringBootConfiguration:继承自@Configuration,二者功能也一致,标注当前类是配置类。可以理解为一个Configuration就是对应的一个Spring的xml版的容器;一个被@Configuration标注的类,相当于一个ApplicationContext.xml文件。

  2. @EnableAutoConfiguration:即把指定的类构造成对象,并放入Spring容器中,使其成为bean对象,作用类似@Bean注解。

  3. @ComponentScan:主要作用是定义包扫描的规则,然后根据定义的规则找出哪些需类需要自动装配到Spring的bean容器中,然后交由Spring进行统一管理。标注了@Controller、@Service、@Repository、@Component 的类都可以别spring扫描到。


你如何理解Spring Boot中的Starter?

Starter可以理解为启动器,它的主要作用如下:

  1. Starter可以维护对应的jar包的版本依赖,使得开发者不需要去关心版本冲突这种容易出错的细节,Starter组件会把对应功能的所有jar包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦

  2. Starter内部集成了自动装配的机制,也就是说在程序中依赖对应的starter组件以后,这个组件会自动集成到Spring生态下,并且对于相关Bean的管理,也是基于自动装配机制来完成

  3. 依赖Starter组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到SpringBoot中,只需要在Application.properties文件里面进行维护就行了,比如Redis这个Starter,只需要在Application.properties文件里面添加Redis的连接信息就可以直接使用了


Spring Boot Starter的工作原理是什么?

在SpringBoot启动的时候,按照约定去读取SpringBoot Starter的配置信息,再根据配置信息对资源进行初始化,并注入到Spring容器中。这样SpringBoot启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应Bean资源即可。