专栏简介
八股战神篇专栏是基于各平台共上千篇面经,上万道面试题,进行综合排序提炼出排序前百的高频面试题,并对这些高频八股进行关联分析,将每个高频面试题可能进行延伸的问题进行分析排序选出高频延伸八股题。面试官都是以点破面从一个面试题不断深入,目的是测试你的理解程度。本专栏将解决你的痛点,助你从容面对。本专栏已更新Java基础高频面试题、Java集合高频面试题、MySQL高频面试题、JUC 并发高频面试题、JVM高频面试题、Spring高频面试题,Redis高频面试题、操作系统高频面试题,后续会继续更新计算机网络、设计模式、场景题、RocketMQ等,计划在七月前更新完毕,点此链接订阅专栏“八股战神篇”。
Bean 的生命周期了解么?
Bean 的生命周期是 Spring 容器管理 Bean 从创建到销毁的整个过程,主要包括实例化、属性赋值、初始化和销毁这四个阶段,下面我将分阶段进行讲述。
首先,创建 Bean 实例。Spring 容器会根据配置文件或注解信息,使用 Java 反射机制实例化 Bean。
然后,属性赋值和依赖注入。Spring 会解析 Bean 的属性,例如 @Autowired、@Value 或 @Resource 等注解,并通过构造方法或 setter 方法将依赖注入到 Bean 中。
接着,初始化 Bean。在这个阶段,Spring 会进行一些额外的处理:
如果 Bean 实现了 BeanNameAware、BeanClassLoaderAware 或 BeanFactoryAware 接口,Spring 会调用相应的 set 方法,将 Bean 名称、类加载器和 BeanFactory 传递给 Bean。
如果有 BeanPostProcessor 处理器,Spring 会在初始化前调用 postProcessBeforeInitialization() 方法。
如果 Bean 实现了 InitializingBean 接口,则会执行 afterPropertiesSet() 方法。
如果在配置中指定了 init-method,Spring 会调用该方法执行自定义初始化逻辑。
初始化后,Spring 还会调用 BeanPostProcessor 的 postProcessAfterInitialization() 方法。
最后,销毁 Bean。当 Spring 容器关闭时,它会销毁 Bean:
如果 Bean 实现了 DisposableBean 接口,则会执行 destroy() 方法。
如果在配置中定义了 destroy-method,则会调用指定的销毁方法。
如果使用 @PreDestroy 注解标记了销毁前的方法,Spring 也会执行该方法释放资源。
延伸
1.Spring Bean 的生命周期图
2.Bean 是线程安全的吗?
Spring 框架中的 Bean 是否具备线程安全性,主要取决于它的作用域以及是否包含可变状态。
Bean 线程安全性的影响因素
Spring 默认的 Bean 作用域是 singleton,即在 IoC 容器中只会创建一个实例,并被多个线程共享。如果这个 Bean 维护了可变的成员变量,就可能在并发访问时引发数据不一致的问题,从而导致线程安全风险。
而 prototype 作用域 下,每次获取 Bean 都会创建新的实例,因此不会发生资源竞争,自然也就没有线程安全问题。
单例 Bean 是否一定不安全?
不一定!
无状态 Bean 是线程安全的:例如常见的 Service 或 Dao 层 Bean,它们通常不存储可变数据,仅执行业务逻辑,因此不会受到并发影响。
有状态 Bean 可能会引发线程安全问题:如果 Bean 存储了可变成员变量,比如用户会话信息、计数器等,可能会因多个线程同时访问导致数据不一致。
解决有状态 Bean 的线程安全问题
如果一个单例 Bean 需要维护状态,可通过以下方式确保线程安全:
设计为无状态 Bean:尽量避免定义可变成员变量,或在方法内部使用局部变量。
使用 ThreadLocal:让每个线程拥有独立的变量副本,防止数据共享导致冲突。
同步控制:在访问共享资源时,使用 synchronized 或 ReentrantLock 进行加锁,确保线程互斥访问。
3.将一个类声明为 Bean 的注解有哪些?
注解 | 用途 | 示例场景 |
@Component | 通用注解,标记任意类为 Bean | 通用组件 |
@Service | 标记服务层组件 | 业务逻辑层 |
@Repository | 标记数据访问层组件 | 数据库操作 |
@Controller | 标记表现层组件 | Web 控制器 |
@Configuration | 配置类,配合 @Bean 显式声明 Bean | 自定义 Bean 配置 |
@Conditional | 根据条件动态注册 Bean | 环境或逻辑条件 |
@Profile | 根据激活的环境注册 Bean | 不同环境下的配置 |
@Scope | 定义 Bean 的作用域 | 单例、原型等 |
@RestController | RESTful Web 服务控制器 | API 接口开发 |
@EnableScheduling | 配合 @Scheduled 声明定时任务 Bean | 定时任务 |
谈谈自己对于 Spring IoC 的了解
Spring IoC(Inversion of Control,控制反转)是 Spring 框架的核心机制之一,负责管理对象的创建、依赖关系和生命周期,从而实现组件解耦,提升代码的可维护性和扩展性。接下来我会详细讲述 Spring IoC 的核心概念、实现方式、工作流程以及它解决的问题。
首先,IoC 的核心思想 是将对象的管理权从应用程序代码中转移到 Spring 容器。传统方式下,类 A 依赖于类 B,A 需要自己创建 B 的实例,而在 IoC 模式下,Spring 负责实例化和注入 B,A 只需要声明依赖即可。
其次,Spring IoC 主要通过依赖注入(DI)来实现。Spring 通过 XML 配置、Java 注解(@Autowired、@Resource)或 Java 代码(@Bean)定义 Bean 及其依赖关系,容器会在运行时自动解析并注入相应的对象。
接着,Spring IoC 的工作流程 可以分为三个阶段:
第一个阶段是IOC 容器初始化,
Spring 解析 XML 配置或注解,获取所有 Bean 的定义信息,生成 BeanDefinition。
BeanDefinition 存储了 Bean 的基本信息(类名、作用域、依赖等),并注册到 IOC 容器的 BeanDefinitionMap 中。
这个阶段完成了 IoC 容器的初始化,但还未实例化 Bean。
第二个阶段是Bean 实例化及依赖注入
Spring 通过反射实例化那些 未设置 lazy-init 且是单例模式 的 Bean。
依赖注入(DI)发生在这个阶段,Spring 根据 BeanDefinition 解析 Bean 之间的依赖关系,并通过构造方法、setter 方法或字段注入(@Autowired)完成对象的注入。
第三个阶段是Bean 的使用
业务代码可以通过 @Autowired 或 BeanFactory.getBean() 获取 Bean。
对于 设置了 lazy-init 的 Bean 或非单例 Bean,它们的实例化不会在 IoC 容器初始化时完成,而是在 第一次调用 getBean() 时 进行创建和初始化,且 Spring 不会长期管理它们。
最后,Spring IoC 主要解决三个问题,
第一个是降低耦合,组件之间通过接口和依赖注入解耦,增强了代码的灵活性。
第二个是简化对象管理,开发者无需手动创建对象,Spring 统一管理 Bean 生命周期。
第三个是提升维护性,当需要修改依赖关系时,只需调整配置,而无需修改业务代码。
延伸
1.传统应用程序图
2.IoC控制反转图
3.IoC 和 DI 的区别?
IoC(Inversion of Control,控制反转)和 DI(Dependency Injection,依赖注入)是 Spring 框架中非常重要的两个概念。虽然它们密切相关,但它们的含义和作用有所不同。以下是它们的区别及联系:
(1)定义与核心思想
IoC(控制反转)
定义:控制反转是一种设计原则,指的是将对象的创建、依赖管理和生命周期的控制权从应用程序代码转移到框架或容器中。
核心思想:传统开发中,对象需要自己负责创建依赖的对象(即“正向控制”)。而在 IoC 中,对象不再负责创建依赖,而是由容器来管理这些依赖关系。
DI(依赖注入)
定义:依赖注入是 IoC 的一种实现方式,指的是容器通过构造方法、setter 方法或字段注入的方式,将对象的依赖自动传递给它。
核心思想:对象只需要声明它需要的依赖,而不需要关心如何获取这些依赖。容器会负责将依赖注入到对象中。
(2)区别对比
维度 | IoC(控制反转) | DI(依赖注入) |
定义 | 一种设计原则,强调控制权的转移 | 一种具体实现方式,用于实现 IoC |
关注点 | 对象的创建、依赖管理和生命周期的控制权 | 如何将依赖传递给对象 |
实现方式 | 通过容器(如 Spring 容器)管理对象 | 通过构造方法、setter 方法或字段注入依赖 |
范围 | 更广泛,包含 DI 和其他实现方式 | 是 IoC 的一个子集 |
示例 | Spring 容器接管了对象的创建和管理 | Spring 容器通过 |
(3)联系
DI 是 IoC 的实现方式:依赖注入是控制反转的一种具体实现形式。IoC 是一种更广泛的设计原则,而 DI 是 IoC 的一种技术手段。
共同目标:两者都旨在降低代码的耦合性,提升代码的可维护性和扩展性。
(4)示例说明
传统方式(无 IoC 和 DI)
public class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository(); // 自己创建依赖
}
public void addUser(String name) {
userRepository.save(name);
}
}
问题:UserService 直接依赖 UserRepository,耦合度高。如果需要更换 UserRepository 的实现,必须修改 UserService 的代码。
使用 IoC 和 DI
@Component
public class UserService {
@Autowired
private UserRepository userRepository; // 声明依赖
public void addUser(String name) {
userRepository.save(name);
}
}
改进:UserService 不再负责创建 UserRepository,而是通过 Spring 容器注入依赖。如果需要更换 UserRepository 的实现,只需在配置中调整,无需修改 UserService 的代码。
什么是动态代理?
动态代理是一种在运行时动态生成代理对象,并在代理对象中增强目标对象方法的技术。它被广泛用于 AOP(面向切面编程)、权限控制、日志记录等场景,使得程序更加灵活、可维护。动态代理可以通过 JDK 原生的 Proxy 机制或 CGLIB 方式实现。接下来我会讲述动态代理的实现方式和执行流程。
首先,JDK 动态代理基于接口,适用于代理实现了接口的对象,当使用 JDK 动态代理时,主要分为四步,
第一步是定义接口,由于动态代理是基于接口进行代理的,因此目标对象必须实现接口。
第二步是创建并实现 InvocationHandler 接口,并在 invoke 方法中定义增强逻辑。
第三步是生成代理对象,使用 Proxy.newProxyInstance 创建代理对象,代理对象内部会调用 invoke 方法。
第四步是调用代理方法,当调用代理对象的方法时,invoke 方法会被触发,执行增强逻辑,并最终调用目标方法。
其次,CGLIB 通过子类继承目标类,适用于没有实现接口的类,当使用 CGLIB 动态代理时,主要分为四步,
第一步是通过 Enhancer 创建代理对象。
第二步是设置父类,CGLIB 代理基于子类继承,因此代理对象是目标类的子类。
第三步是定义并实现 MethodInterceptor 接口,在 intercept 方法中增强目标方法。
第四步是调用代理方法,当调用代理对象的方法时,intercept 方法会被触发,执行增强逻辑,并最终调用目标方法。
延伸
1.动态代理有哪些应用场景?
Spring AOP(面向切面编程):如事务管理、日志记录、权限控制等。
MyBatis Mapper 代理:使用 JDK 动态代理创建 Mapper 接口的代理对象。
RPC 远程调用:动态代理封装远程服务调用(如 Dubbo)。
拦截器机制:如 Servlet 过滤器、拦截器等。
Mock 测试:动态代理可用于创建测试桩对象。
动态代理和静态代理的区别
动态代理和静态代理都属于代理模式,它们都用于在不修改目标对象代码的情况下增强其功能。接下来我会详细讲述动态代理和静态代理的五点区别。
第一点是实现方式的不同,静态代理需要手动编写代理类,而动态代理在运行时动态生成代理类。
第二点是灵活性的不同,静态代理不够灵活,代理类与目标类一一对应;而动态代理更加灵活,适用于多种目标类。
第三点是维护成本的不同,静态代理的维护成本较高,因为每个目标类都需要一个代理类;而动态代理的维护成本较低,因为代理逻辑是通用的。
第四点是技术依赖的不同,静态代理基于普通 Java 类实现,而动态代理依赖于反射机制(JDK 动态代理)或字节码技术(CGLIB)。
第五点是适用场景的不同,静态代理则适用于简单的、目标类较少的场景;而动态代理适合需要为多个目标类添加相同逻辑的场景。
延伸
1.如何使用静态代理?
首先,静态代理是指在编译时就已经确定了代理类和目标类的关系。开发者需要手动编写代理类,并在代理类中调用目标类的方法,同时添加额外的逻辑。
当使用静态代理时,
首先,开发者需要为目标类创建一个代理类,代理类通常会实现与目标类相同的接口。
其次,在代理类中,开发者需要手动编写方法调用逻辑,例如在调用目标方法前后添加自定义逻辑(如日志记录或权限校验)。
然后,通过代理类的对象调用方法,实际执行的是代理类中的逻辑,而不是直接调用目标类的方法。
最后,由于代理类是手动编写的,因此每个目标类都需要一个对应的代理类,这会导致代码冗余,尤其是在目标类较多时维护成本较高。
Spring AOP的执行流程
Spring AOP(Aspect-Oriented Programming,面向切面编程)是一种通过代理机制实现方法增强的技术,它允许在不修改原始代码的情况下,对方法的执行过程进行扩展,如日志记录、事务管理、权限控制等。Spring AOP 主要依赖动态代理来实现,接下来我会讲述 Spring AOP 的执行流程,主要分为六步。
当 Spring AOP 拦截一个方法调用时:
第一步是要定义切面(Aspect),可以使用 @Aspect 标注类,并在其中定义切点(Pointcut)和通知(Advice),如 @Before、@After、@Around 等。
第二步是要解析切点,Spring 会解析 @Pointcut 表达式,确定需要增强的方法。
第三步是要创建代理对象,如果目标类实现了接口,Spring 使用 JDK 动态代理,通过 Proxy.newProxyInstance 生成代理对象;如果目标类没有实现接口,Spring 使用 CGLIB 动态代理,通过创建目标类的子类来生成代理对象。
第四步是要方法调用拦截,如果是JDK 动态代理,代理对象会拦截方法调用,并调用 InvocationHandler#invoke,执行增强逻辑后,再调用目标方法;如果是CGLIB 动态代理,代理对象则通过 MethodInterceptor#intercept 代理方法调用,执行增强逻辑后,再调用目标方法。
第五步是要执行增强逻辑,根据通知类型,在方法执行前后或异常时,执行对应的 AOP 逻辑,如日志记录、事务提交等。
第六步是要执行目标方法,最终调用目标对象的方法,完成实际业务逻辑。
延伸
1. AOP 关键术语
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来。以下是 AOP 的关键术语:
(1) Aspect(切面)
定义:切面是横切关注点的模块化表示,通常包含一组相关的通知(Advice)和切点(Pointcut)。
作用:将通用的功能(如日志、事务管理)封装到一个独立的模块中。
示例:
@Aspect
public class LoggingAspect {
// 切面内容
}
(2) Join Point(连接点)
定义:程序执行过程中的某个特定点,例如方法调用、方法执行、异常抛出等。
作用:Spring AOP 中的连接点通常是方法执行。
示例:UserService.addUser() 方法的执行是一个连接点。
(3) Pointcut(切点)
定义:切点是一组连接点的集合,用于定义哪些连接点需要被增强。
作用:通过表达式匹配特定的方法或类。
示例:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
(4) Advice(通知)
定义:通知是切面在特定连接点上执行的动作,定义了“何时”以及“如何”增强。
作用:在方法执行前、后或异常时执行额外的逻辑。
类型:见下文详细说明。
(5) Target Object(目标对象)
定义:被代理的对象,也就是需要增强的业务逻辑对象。
作用:目标对象的方法会被切面拦截并增强。
(6) Proxy(代理对象)
定义:由 AOP 框架生成的代理对象,用于拦截目标对象的方法调用并执行增强逻辑。
作用:实现方法拦截和增强。
(7) Weaving(织入)
定义:将切面应用到目标对象并创建代理对象的过程。
作用:织入可以在编译时、类加载时或运行时完成。
2. AOP 常见的通知类型
通知(Advice)定义了切面在连接点上的行为,Spring AOP 提供了以下五种常见的通知类型:
(1) Before Advice(前置通知)
定义:在目标方法执行之前执行的通知。
注解:@Before
示例:
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
(2) After Returning Advice(返回后通知)
定义:在目标方法成功执行并返回结果后执行的通知。
注解:@AfterReturning
示例:
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("Method returned: " + result);
}
(3) After Throwing Advice(异常后通知)
定义:在目标方法抛出异常后执行的通知。
注解:@AfterThrowing
示例:
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logAfterThrowing(Exception ex) {
System.out.println("Exception thrown: " + ex.getMessage());
}
(4) After (Finally) Advice(最终通知)
定义:无论目标方法是否成功执行,都会在方法结束后执行的通知。
注解:@After
示例:
@After("execution(* com.example.service.*.*(..))")
public void logAfter() {
System.out.println("After method execution");
}
(5) Around Advice(环绕通知)
定义:在目标方法执行前后都执行的通知,可以控制目标方法的执行。
注解:@Around
示例:
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("After method: " + joinPoint.getSignature().getName());
return result;
}
总结表
通知类型 | 注解 | 执行时机 | 用途 |
Before Advice | @Before | 在目标方法执行之前 | 日志记录、权限检查 |
After Returning | @AfterReturning | 在目标方法成功返回后 | 记录返回值、清理资源 |
After Throwing | @AfterThrowing | 在目标方法抛出异常后 | 异常处理、日志记录 |
After (Finally) | @After | 在目标方法结束时(无论成功还是失败) | 资源释放、日志记录 |
Around Advice | @Around | 在目标方法执行前后,可控制目标方法的执行 | 性能监控、事务管理、日志记录 |
3. AOP 的应用场景有哪些?
日志记录:在方法执行前后自动记录日志。
事务管理:自动开启、提交或回滚事务(如 @Transactional)。
权限控制:在方法执行前进行权限校验,决定是否继续执行。
性能监控:统计方法执行时间,进行性能分析。
异常处理:统一捕获和处理异常,避免在业务代码中大量 try-catch。
缓存机制:拦截方法调用,判断是否需要从缓存中获取数据,而不是重复查询数据库。
分布式追踪:在微服务架构下,拦截请求并添加分布式跟踪信息,如 Sleuth + Zipkin。
4.AOP 为什么叫做切面编程?
AOP 之所以被称为 切面编程,是因为它的核心思想是 "切面"(Aspect),即 把系统中的横切关注点抽取出来,通过切面进行统一管理。
为什么叫“切面”:
横切关注点(Cross-cutting Concerns):日志、事务、权限等功能本身不属于某个具体业务,而是多个模块都需要的功能,称为“横切关注点”。
“切片”思想:AOP 允许在不修改原有业务逻辑的情况下,给多个业务方法“切入”额外的功能。
解耦业务代码:通过“切面”技术,AOP 让这些横切关注点从业务代码中分离出来,集中管理,避免代码重复。
形象比喻:
如果把业务代码比作“面包”,AOP 就像“切刀”,可以在方法执行的不同阶段(前、后、异常)“切入”额外功能,而不影响面包的主要结构。
Spring的事务什么情况下会失效?
Spring 的事务管理是 Spring 框架中非常重要的功能,它通过声明式事务(如 @Transactional 注解)或编程式事务简化了事务的管理。然而,在某些情况下,Spring 的事务可能会失效,导致事务无法正常回滚或提交。接下来我会详细讲述 Spring 事务失效的常见场景及其原因,失效的常见场景主要有六个。
第一个场景是方法未被代理对象调用,当使用 @Transactional 注解时,Spring 会为目标类生成一个代理对象,并通过代理对象拦截方法调用以管理事务。如果目标方法是通过类内部调用(即 this.method())而不是通过代理对象调用,则事务会失效。这是因为代理对象无法拦截类内部的方法调用,导致事务逻辑未被执行。
第二个场景是异常未被捕获或未触发回滚规则,当事务方法抛出异常时,Spring 默认只会在遇到 RuntimeException 或 Error 时回滚事务,而不会对受检异常(Checked Exception)进行回滚。如果开发者捕获了异常但未重新抛出,或者未正确配置回滚规则(如通过 @Transactional(rollbackFor = Exception.class)),事务也会失效。
第三个场景是事务传播行为配置不当,当多个事务方法相互调用时,Spring 提供了多种事务传播行为(如 REQUIRED、REQUIRES_NEW 等)。如果传播行为配置不当,可能导致事务未按预期工作。例如,如果外部方法的事务传播行为为 NOT_SUPPORTED 或 NEVER,则内部方法的事务可能被挂起或完全不生效。
第四个场景是数据库引擎不支持事务,当使用不支持事务的数据库引擎时,例如 MySQL 的 MyISAM 引擎不支持事务,即使代码中配置了事务管理,也无法生效。因此,确保使用的数据库引擎(如 InnoDB)支持事务是非常重要的。
第五个场景是代理模式配置错误,当使用 @Transactional 注解时,Spring 默认使用 JDK 动态代理或 CGLIB 动态代理来管理事务。如果目标类没有实现接口且未启用 CGLIB 代理(如未设置 proxyTargetClass=true),事务可能会失效。此外,如果目标类被标记为 final 或方法被标记为 private,CGLIB 代理也无法生成,导致事务失效。
第六个场景是事务管理器配置错误,当 Spring 容器中存在多个事务管理器时,如果未明确指定事务管理器(如通过 @Transactional("transactionManagerName")),可能导致事务管理器选择错误。这会导致事务无法正常工作,尤其是在多数据源场景下。
延伸
1.事务的特性(ACID)
(1)A(Atomicity,原子性)定义: 事务是一个 不可分割的最小执行单位,要么全部执行,要么全部回滚,不会有部分成功、部分失败的情况。示例: 转账操作 A → B,如果 A 账户扣款成功,但 B 账户未到账,系统必须回滚,保证 A 账户的钱不会凭空减少。
(2)C(Consistency,一致性)定义: 事务执行前后,数据库要始终保持 一致性状态,不会破坏数据的完整性和约束条件。示例: 银行总账恒定:A 给 B 转账 100 元,无论事务成功还是失败,A + B 的总金额不变。
(3)I(Isolation,隔离性)定义: 并发事务之间 互不影响,一个事务的操作对其他事务不可见,直到它提交。隔离级别(由低到高):
Read Uncommitted(读未提交):可能出现脏读(读取未提交数据)。
Read Committed(读已提交):防止脏读,但可能出现不可重复读(同一查询两次结果不同)。
Repeatable Read(可重复读)(MySQL 默认):防止脏读、不可重复读,但可能有幻读。
Serializable(可串行化):最高级别,事务串行执行,性能最低。
(4)D(Durability,持久性)定义: 事务提交后,对数据库的更改是 永久性的,即使系统崩溃也不会丢失数据。示例: 订单支付成功后,系统崩溃,重启后订单状态依然保持“已支付”。