Ø 熟悉Spring,对IOC、AOP、Bean生命周期、循环依赖等有深入了解。
面试题整理
- 描述 Spring Context 初始化的流程
- 介绍 Bean 的生命周期及作用域
- Bean 的构造方法、 @PostConstruct注解、InitializingBean、init-method 的执行顺序?
- Spring 如何解决循环依赖?
- Spring 配置中的 placeholder 占位符是如何替换的?有什么办法实现自定义的配置替换?
- Spring AOP的实现原理
- JDK动态代理和CGLIB动态代理的区别?
- Spring 事务的实现机制
- Spring事务在什么情况下会失效?
- Spring中使用的设计模式有哪些
- 请解释Spring中的@Async注解的工作原理,并说明其异步执行机制。
1、描述 Spring Context 初始化的流程
Spring Context 初始化是 Spring 框架的核心过程,它负责加载配置、创建和管理 Bean 实例。以下是 Spring Context 初始化的主要流程:
① 容器创建
- 当使用
ApplicationContext
(Spring Context 的主要接口)启动一个 Spring 应用程序时,通常会通过ClassPathXmlApplicationContext
(用于从类路径加载 XML 配置文件)或AnnotationConfigApplicationContext
(用于基于 Java 配置类加载)等具体实现类来创建容器。 - 例如,使用
ClassPathXmlApplicationContext
加载配置文件的方式如下:
java
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
这里applicationContext.xml
是 Spring 的配置文件,它包含了关于 Bean 定义、依赖关系等信息。这个过程会触发 Spring Context 的初始化。
② 资源加载
- XML 配置文件加载(如果是基于 XML 的配置)
- Spring 会解析配置文件,通过
BeanDefinitionReader
(如XmlBeanDefinitionReader
)读取 XML 文件中的<bean>
标签等定义信息。这些定义信息包括 Bean 的类名、属性、构造函数参数等。 - 例如,以下是一个简单的 XML 配置文件中的 Bean 定义:
- Spring 会解析配置文件,通过
xml
<bean id="userService" class="com.example.UserService">
<property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.example.UserDao"/>
- Spring 会读取这些
<bean>
标签,将其转换为BeanDefinition
对象。BeanDefinition
是 Spring 内部用于描述 Bean 的元数据,它包含了 Bean 的类信息、属性设置、初始化方法、销毁方法等细节。 - Java 配置类加载(如果是基于 Java 配置)
- 对于基于 Java 配置的方式,
AnnotationConfigApplicationContext
会扫描配置类上的@Configuration
注解。 - 例如,有一个如下的 Java 配置类:
- 对于基于 Java 配置的方式,
java
@Configuration
public class AppConfig {
@Bean
public UserService userService(UserDao userDao) {
UserService userService = new UserService();
userService.setUserDao(userDao);
return userService;
}
@Bean
public UserDao userDao() {
return new UserDao();
}
}
- Spring 会解析这个配置类中的
@Bean
注解方法,将方法返回的对象也转换为BeanDefinition
对象,从而确定 Bean 的定义信息。
③ BeanDefinition 注册
- 无论是通过 XML 配置还是 Java 配置得到的
BeanDefinition
对象,都会被注册到BeanDefinitionRegistry
中。 - 这个注册表维护了所有 Bean 的定义信息,Spring 可以根据这个注册表在后续的流程中创建和管理 Bean。它就像是一个 Bean 定义的仓库,方便 Spring 在需要的时候查找和使用这些定义来实例化 Bean。
④ Bean 实例化前的准备
- Spring 会对需要创建的 Bean 进行排序。如果 Bean 之间存在依赖关系,Spring 会按照一定的顺序来处理它们。
- 例如,有 Bean A 依赖于 Bean B,Spring 会先确保 Bean B 在 Bean A 之前进行处理,这样才能正确地设置依赖关系。这是通过
BeanFactoryPostProcessor
接口来实现的。实现这个接口的类可以在 Bean 实例化之前修改BeanDefinition
,例如修改 Bean 的属性值等。 - 一个典型的
BeanFactoryPostProcessor
是PropertyPlaceholderConfigurer
,它可以用于将配置文件中的属性值(如数据库连接信息)替换到 Bean 的属性定义中。
⑤ Bean 实例化
- 根据
BeanDefinition
中的信息,Spring 通过反射机制来实例化 Bean。 - 如果 Bean 是通过构造函数注入依赖关系,Spring 会先解析构造函数的参数,找到对应的 Bean 来进行注入。
- 例如,对于前面 XML 配置中的
UserService
,如果它有一个带UserDao
参数的构造函数,Spring 会找到userDao
这个 Bean 并将其作为参数传入构造函数来实例化UserService
。 - 如果是通过属性注入,Spring 会在 Bean 实例化后,使用
setter
方法来注入依赖的 Bean。
⑥ Bean 属性填充和依赖注入
实例化 bean 后,容器会根据 bean 定义中的属性信息,将依赖的其他 bean 注入到当前 bean 中。如果 bean 有构造函数注入或属性注入的依赖,容器会先创建依赖的 bean 实例,然后通过反射将其注入到当前 bean 中。
⑦ Bean 初始化方法调用
- 如果在
BeanDefinition
中指定了初始化方法(如在 XML 配置中的init - method
属性或者在 Java 配置类中通过@PostConstruct
注解),Spring 会在 Bean 属性填充和依赖注入完成后调用这个初始化方法。 - 例如,在
UserService
类中有一个init
方法被标记为初始化方法,Spring 会在 Bean 初始化阶段调用这个方法,这可以用于一些资源的初始化,如数据库连接池的初始化等。
⑧ 完成初始化并发布事件(可选)
- Spring Context 完成所有 Bean 的初始化后,就可以正常使用了。
- 同时,它还可以发布一个
ContextRefreshedEvent
事件,其他组件可以监听这个事件来执行一些额外的初始化操作,例如一些需要在整个 Spring Context 初始化完成后才能进行的业务逻辑初始化。
Spring Context 初始化流程是一个复杂而有序的过程,通过这些步骤,Spring 容器能够创建和管理应用程序中的对象,协调它们之间的依赖关系,为应用程序的运行提供坚实的基础。整个流程体现了 Spring 框架的控制反转(IoC)和依赖注入(DI)特性,使得应用程序的组件解耦,易于维护和扩展。
2、介绍 Bean 的生命周期及作用域
2.1、Spring的生命周期
大致分为:实例化
-> 属性填充
-> 初始化bean
-> 使用
-> 销毁
几个核心阶段。我们先来简单了解一下这些阶段所做的事情:
① 实例化(Instantiation)
这是 Bean 生命周期的第一步。当IOC容器需要一个 Bean 实例时,它会通过反射调用 Bean 的构造函数来创建一个对象。例如,在 Spring 框架中,如果有一个简单的 Java 类UserService
,当 Spring 容器决定创建UserService
的 Bean 时,会使用new UserService()
(假设UserService
有默认构造函数)这样的方式来实例化对象。
② 属性赋值(Populate Properties)
在实例化之后,容器会将配置文件(如 Spring 的 XML 配置文件或基于注解的配置)中定义的属性值注入到 Bean 实例中。例如,如果UserService
有一个属性userRepository
,并且在配置文件中有对应的<property>
标签或者使用了@Autowired
注解,容器会找到对应的userRepository
Bean,并将其赋值给UserService
的userRepository
属性。
③ 初始化(Initialization)
初始化阶段可以让 Bean 在完全可用之前执行一些自定义的初始化操作。
- 执行各种通知;
- 执行初始化的前置工作;
- 进行初始化工作(使用注解
@PostConstruct
初始化 或者 使用(xml)init-method
初始化, 前者技术比后者技术先进~); - 执行初始化的后置工作;
④ 使用(In - Use)
此时 Bean 已经完全初始化,可以被应用程序使用了。例如,在一个 Web 应用中,UserService
的 Bean 可以被控制器(Controller)调用,来处理用户相关的业务逻辑,如注册、登录等操作。
⑤ 销毁(Destruction)
当容器关闭或者 Bean 不再需要时,会执行销毁操作。与初始化类似,在 Spring 中可以通过在 XML 配置文件中使用destroy - method
属性指定一个方法名来定义销毁方法,也可以通过实现DisposableBean
接口,实现destroy
方法。这个阶段可以用于释放资源,比如关闭数据库连接、释放文件句柄等。例如,如果UserService
在运行过程中打开了一些资源,在销毁阶段可以将这些资源关闭。
下图以买房、盖房、入住、卖房举例,方便理解~
详细版本
- 加载Bean定义:通过 loadBeanDefinitions 扫描所有xml配置、注解将Bean记录在beanDefinitionMap中。即IOC容器的初始化过程。
- Bean实例化:遍历 beanDefinitionMap 创建bean,最终会使用getBean中的doGetBean方法调用 createBean来创建Bean对象
- 构建对象:容器通过 createBeanInstance 进行对象构造
- 获取构造方法(大部分情况下只有一个构造方法)
- 如果只有一个构造方法,无论这个构造方法有没有入参,都用这个构造方法
- 有多个构造方法时
- 先拿带有@Autowired的构造方法,但是如果多个构造方法都有@Autowired就会报错
- 如果没有带有@Autowired的构造方法,那就找没有入参的;如果多个构造方法都是有入参的,那也会报错
- 准备参数
- 先根据类进行查找
- 如果这个类有多个实例,则再根据参数名匹配
- 如果没有找到则报错
- 构造对象:无参构造方法则直接实例化
- 获取构造方法(大部分情况下只有一个构造方法)
- 填充属性:通过populateBean方法为Bean内部所需的属性进行赋值,通常是 @Autowired 注解的变量;通过三级缓存机制进行填充,也就是依赖注入
- 初始化Bean对象:通过initializeBean对填充后的实例进行初始化
- 执行Aware:检查是否有实现者三个Aware:
BeanNameAware
,BeanClassLoaderAware
,BeanFactoryAware
;让实例化后的对象能够感知自己在Spring容器里的存在的位置信息,创建信息 - 初始化前:BeanPostProcessor,也就是拿出所有的后置处理器对bean进行处理,当有一个处理器返回null,将不再调用后面的处理器处理。
- 初始化:afterPropertiesSet,init- method;
- 实现了InitializingBean接口的类执行其afterPropertiesSet()方法
- 从BeanDefinition中获取initMethod方法
- 初始化后:BeanPostProcessor,;获取所有的bean的后置处理器去执行。AOP也是在这里做的
- 执行Aware:检查是否有实现者三个Aware:
- 注册销毁:通过reigsterDisposableBean处理实现了DisposableBean接口的Bean的注册
- Bean是否有注册为DisposableBean的资格:
- 是否有destroyMethod。
- 是否有执行销毁方法的后置处理器。
- DisposableBeanAdapter: 推断destoryMethod
- 完成注册
- Bean是否有注册为DisposableBean的资格:
- 构建对象:容器通过 createBeanInstance 进行对象构造
- 添加到单例池:通过 addSingleton 方法,将Bean 加入到单例池 singleObjects
- 销毁
- 销毁前:如果有@PreDestory 注解的方法就执行
- 如果有自定义的销毁后置处理器,通过 postProcessBeforeDestruction 方法调用destoryBean逐一销毁Bean
- 销毁时:如果实现了destroyMethod就执行 destory方法
- 执行客户自定义销毁:调用 invokeCustomDestoryMethod执行在Bean上自定义的destroyMethod方法
- 有这个自定义销毁就会执行
- 没有自定义destroyMethod方法就会去执行close方法
- 没有close方法就会去执行shutdown方法
- 都没有的话就都不执行,不影响
BeanFactory和FactoryBean的区别?
2.2、Bean的作用域
Spring框架提供了多种Bean作用域,以控制Bean实例的创建和销毁时机,以及Bean实例的可见性范围。主要的作用域包括:
- singleton(单例)
- 这是 Spring 默认的作用域。在这种作用域下,一个 Bean 在整个容器中只有一个实例。例如,对于
UserService
这个 Bean,如果它是单例作用域,那么无论在应用程序的多少个地方需要使用UserService
,容器都只会创建一个UserService
实例。所有对这个 Bean 的引用都指向同一个对象。这种作用域适合无状态的服务,比如工具类、数据访问层服务等。因为这些服务不需要维护每个请求的状态,一个实例可以满足所有请求的需求。
- 这是 Spring 默认的作用域。在这种作用域下,一个 Bean 在整个容器中只有一个实例。例如,对于
- prototype(原型)
- 每次从容器中获取这个 Bean 时,都会创建一个新的实例。比如有一个
Order
Bean,它可能包含了订单的各种状态信息。如果Order
是原型作用域,那么每次创建一个新订单时,从容器中获取Order
Bean 都会得到一个全新的Order
对象,这样可以确保每个订单的状态相互独立,不会互相干扰。
- 每次从容器中获取这个 Bean 时,都会创建一个新的实例。比如有一个
- request(请求)
- 这个作用域只在 Web 应用中有效。一个 Bean 在一个 HTTP 请求的生命周期内有效。例如,在一个 Web 应用中,有一个
HttpServletRequestWrapper
类型的 Bean,用于对请求进行一些包装和处理。这个 Bean 在每次 HTTP 请求时都会创建一个新的实例,并且在请求处理完成后销毁,保证每个请求都有自己独立的 Bean 来处理请求相关的操作。
- 这个作用域只在 Web 应用中有效。一个 Bean 在一个 HTTP 请求的生命周期内有效。例如,在一个 Web 应用中,有一个
- session(会话)
- 同样只在 Web 应用中有效。一个 Bean 在一个用户会话的生命周期内有效。例如,在一个 Web 应用的用户认证模块中,有一个
UserSession
Bean,用于存储用户登录后的会话信息,如用户 ID、权限等。这个 Bean 在用户登录时创建,在用户注销或者会话过期时销毁,在整个会话期间,这个 Bean 可以被多个请求共享,用于维护用户的会话状态。
- 同样只在 Web 应用中有效。一个 Bean 在一个用户会话的生命周期内有效。例如,在一个 Web 应用的用户认证模块中,有一个
- application(应用)
- 也用于 Web 应用。一个 Bean 在整个 Web 应用的生命周期内有效。例如,在一个 Web 应用中有一个
ApplicationConfig
Bean,用于存储整个应用的配置信息,如数据库连接配置、全局参数等。这个 Bean 在应用启动时创建,在应用关闭时销毁,整个应用中的所有请求和会话都可以访问这个 Bean 来获取应用的配置信息。
- 也用于 Web 应用。一个 Bean 在整个 Web 应用的生命周期内有效。例如,在一个 Web 应用中有一个
了解Bean的生命周期和作用域对于设计高效的应用程序至关重要,它们可以帮助开发者更好地管理Bean实例,提高应用程序的性能和可维护性。
3、Bean 的构造方法、 @PostConstruct注解、InitializingBean、init-method 的执行顺序?
如图所示:
- MyBean 构造函数调用
- MyBean @PostConstruct 方法调用
- MyBean InitializingBean.afterPropertiesSet() 调用
- MyBean customInitMethod() 调用
java
// 定义一个普通的 Spring Bean
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.InitializingBean;
public class MyBean implements InitializingBean {
private String myProperty;
// 构造器注入
public MyBean(String myProperty) {
System.out.println("MyBean 构造函数调用");
this.myProperty = myProperty;
}
@PostConstruct
public void postConstruct() {
System.out.println("MyBean @PostConstruct 方法调用");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("MyBean InitializingBean.afterPropertiesSet() 调用");
}
// 自定义 init-method
public void customInitMethod() {
System.out.println("MyBean customInitMethod() 调用");
}
}
// 配置类以注册上述 Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean(initMethod = "customInitMethod")
public MyBean myBean() {
return new MyBean("value from constructor");
}
}
// 启动应用程序上下文
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
MyBean myBean = context.getBean(MyBean.class);
// 这里可以进行其他操作
}
}
在 Spring 框架中,Bean 的生命周期涉及到多个阶段,每个阶段都有特定的回调接口或注解可以被实现或使用。以下是关于 Bean 构造方法、@PostConstruct
注解、InitializingBean
接口和 init-method
属性的执行顺序:
- 构造方法调用:
- 首先,Spring 容器会调用 Bean 的构造方法来实例化该 Bean。
- 依赖注入(DI):
- 在构造函数之后,Spring 会对所有属性进行依赖注入(如果有的话),包括通过构造器注入、设值注入(即 setter 方法)或者其他自定义注入方式。
- Aware 接口回调方法:
- 如果 Bean 实现了任何 Aware 接口(如
BeanNameAware
,BeanFactoryAware
,ApplicationContextAware
等),那么这些接口的方法将在依赖注入完成后被调用。这允许 Bean 获得对容器或其自身元数据的引用。
- 如果 Bean 实现了任何 Aware 接口(如
- BeanPostProcessor 的前置处理:
- 如果有
BeanPostProcessor
类型的 Bean 存在于容器中,它们的postProcessBeforeInitialization
方法会在初始化之前被调用。
- 如果有
- @PostConstruct 方法:
- 接下来,如果 Bean 中有使用
@PostConstruct
注解的方法,那么这些方法会被调用。@PostConstruct
标记的方法会在依赖注入完成后自动执行,并且只执行一次,用于执行需要在 Bean 初始化后立即运行的逻辑。
- 接下来,如果 Bean 中有使用
- InitializingBean 接口的 afterPropertiesSet() 方法:
- 如果 Bean 实现了
InitializingBean
接口,它的afterPropertiesSet()
方法将会在此时被调用。这是另一种定义初始化逻辑的方式。
- 如果 Bean 实现了
- 自定义 init-method:
- 如果在 Bean 定义中指定了
init-method
属性,那么对应的初始化方法将在上述所有步骤之后被调用。这是一种非侵入式的配置方式,因为它不需要修改 Bean 的代码。
- 如果在 Bean 定义中指定了
- BeanPostProcessor 的后置处理:
- 最后,如果有
BeanPostProcessor
类型的 Bean 存在于容器中,它们的postProcessAfterInitialization
方法会在初始化完成后被调用。
- 最后,如果有
请注意,@PostConstruct
和 InitializingBean
或 init-method
可以同时存在于同一个 Bean 中,但是按照上面的顺序,@PostConstruct
方法会先于 InitializingBean
的 afterPropertiesSet()
方法或者自定义的 init-method
方法被执行。此外,如果使用了 @Configuration
类并定义了 @Bean
方法,那么 @Bean
方法中的初始化逻辑可能会有所不同,因为 @Bean
方法本身也可以指定初始化方法。
4、Spring 如何解决循环依赖
首先,有两种Bean注入的方式:构造器注入和属性注入。
- 对于构造器注入的循环依赖,Spring处理不了,会直接抛出BeanCurrentlylnCreationException异常。
- 对于属性注入的循环依赖
- 单例模式下,是通过三级缓存处理来循环依赖的。
- 非单例对象的循环依赖,则无法处理。
4.1、什么是循环依赖
循环依赖是指两个或多个 Bean 相互依赖对方,例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,这就形成了一个循环。在 Spring 容器初始化这些 Bean 时,如果不加以处理,就会陷入死循环,导致程序无法正常启动。
Spring框架通过其三级缓存机制来解决循环依赖问题。这种机制仅适用于单例(singleton)作用域的Bean,对于原型(prototype)作用域的Bean,Spring容器不会尝试解决循环依赖,因为每次请求都会创建一个新的实例。
4.2、三级缓存解决循环依赖(针对单例 Bean)
java
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
Spring 通过三级缓存来解决单例 Bean 的循环依赖问题。这三级缓存分别是:
- 一级缓存(singletonObjects):存放已经完全初始化好的单例 Bean。这是最常用的缓存,当一个单例 Bean 完成了所有的生命周期步骤,包括构造函数、属性注入、初始化方法等,就会被放入这个缓存中,供其他 Bean 获取和使用。
- 二级缓存(earlySingletonObjects):存放早期暴露的单例 Bean。在 Bean 的实例化之后,但是还没有完成属性注入和初始化方法之前,如果发现存在循环依赖,就会将这个早期的 Bean 实例放入二级缓存中,以便其他 Bean 可以提前引用这个未完全初始化的 Bean。
- 三级缓存(singletonFactories):存放单例 Bean 工厂。这个工厂主要用于创建早期暴露的单例 Bean,它是一个
ObjectFactory
类型的工厂接口,定义了一个getObject
方法,用于生成早期暴露的 Bean。在 Bean 实例化之后,会将一个生成早期 Bean 的工厂放入三级缓存中。
4.3、解决过程详细步骤
假设存在两个相互依赖的单例 Bean A 和 Bean B。
- 当 Spring 开始创建 Bean A 时,首先会调用 Bean A 的构造函数进行实例化,此时 Bean A 还没有完成属性注入和初始化方法。然后,Spring 会将一个生成早期 Bean A 的工厂放入三级缓存(singletonFactories)中。这个工厂可以在需要的时候创建早期的 Bean A。
- 接着,Spring 发现 Bean A 依赖于 Bean B,于是开始创建 Bean B。同样,先调用 Bean B 的构造函数进行实例化,此时 Bean B 也没有完成属性注入和初始化方法。然后,Spring 也会将一个生成早期 Bean B 的工厂放入三级缓存中。
- 当为 Bean B 进行属性注入时,发现需要注入 Bean A。此时,Spring 会从三级缓存(singletonFactories)中获取生成早期 Bean A 的工厂,通过这个工厂的
getObject
方法获取早期的 Bean A,并将其放入二级缓存(earlySingletonObjects)中。这样,Bean B 就可以成功注入这个早期的 Bean A。 - 之后,Bean B 完成属性注入和初始化方法,成为一个完全初始化的单例 Bean,被放入一级缓存(singletonObjects)中。
- 当为 Bean A 进行属性注入时,由于 Bean B 已经在一级缓存中是完全初始化的状态,所以可以成功注入 Bean B。最后,Bean A 也完成属性注入和初始化方法,成为一个完全初始化的单例 Bean,也被放入一级缓存中。
简单图例:
详细图例:
4.4、其它循环依赖如何解决
这类循环依赖问题解决方法很多,主要有:
- 使用@Lazy注解,延迟加载
- 构造器循环依赖这类循环依赖问题可以通过使用@Lazy注解解决
- 使用@DependsOn注解,指定加载先后关系
- 使用@DependsOn产生的循环依赖:这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。
- 修改文件名称,改变循环依赖类的加载顺序
- 多例循环依赖这类循环依赖问题可以通过把bean改成单例的解决。
5、Spring 配置中的 placeholder 占位符是如何替换的?有什么办法实现自定义的配置替换?
5.1、占位符替换
在 Spring 框架中,placeholder
占位符通常用于配置文件(如 application.properties
或 application.yml
)中,允许你定义可变的配置值。这些占位符以 ${...}
的形式表示,例如 ${database.url}
。
Spring 使用 PropertyPlaceholderConfigurer
或其子类 PlaceholderConfigurerSupport
来解析这些占位符。在 Spring Boot 中,默认已经包含了对属性文件的支持,因此不需要显式地配置 PropertyPlaceholderConfigurer
。当你在配置文件中使用占位符时,Spring 会自动尝试从多种来源解析这些占位符:
- 操作系统环境变量。
- JVM 系统属性 (
System.getProperties()
)。 - 配置文件(如
application.properties
或application.yml
)。 - 命令行参数。
- 来自
@ConfigurationProperties
注解的类。 - 来自
RandomValuePropertySource
的随机数。 - 默认属性(通过
SpringApplication.setDefaultProperties
指定)。
例如,假设有一个 Spring 配置文件applicationContext.xml
,其中包含一个数据源(DataSource)的配置:
xml
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
同时,有一个jdbc.properties
文件,内容如下:
properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
jdbc.username=root
jdbc.password=123456
当 Spring 容器启动时,PropertyPlaceholderConfigurer
会读取jdbc.properties
文件,将applicationContext.xml
中的${jdbc.driverClassName}
替换为com.mysql.jdbc.Driver
,${jdbc.url}
替换为jdbc:mysql://localhost:3306/mydb
等,从而完成数据源的正确配置。
5.2、自定义的配置替换
为了实现自定义的配置替换,你可以采取以下几种方法:
5.2.1、自定义 PropertySourcesPlaceholderConfigurer
你可以定义一个自定义的 PropertySourcesPlaceholderConfigurer
Bean 来添加额外的属性源或修改属性解析逻辑:
java
@Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
// 可以在这里添加自定义属性源
return propertyPlaceholderConfigurer;
}
5.2.2、使用 Environment 和 @Value
你可以注入 Environment
对象或者使用 @Value
注解来获取属性值,并且可以在 @Value
注解中直接使用 SpEL(Spring Expression Language)表达式来进行更复杂的属性值计算。
5.2.3、实现自定义的 PropertySource
如果你需要从特定的地方(比如数据库、远程服务等)加载配置,可以创建一个实现了 PropertySource
接口的类,并将其添加到 Environment
中。
java
@Component
public class CustomPropertySource extends PropertySource<MyCustomProperties> {
public CustomPropertySource() {
super("customPropertySource");
}
@Override
public Object getProperty(String name) {
// 返回来自自定义位置的属性值
}
}
然后你需要将这个自定义的 PropertySource
添加到 Environment
中,这可以通过扩展 PropertySourcesPlaceholderConfigurer
或者实现 EnvironmentPostProcessor
来完成。
5.2.4、使用外部化配置
Spring Boot 支持外部化配置,这意味着你可以将配置文件放在应用程序之外,甚至可以从云配置服务器加载配置。Spring Cloud Config 是一个专门为此设计的项目,它允许你在分布式系统中管理外部配置。
5.2.5、使用 @ConfigurationProperties
对于分组的属性,可以使用 @ConfigurationProperties
注解来将一组相关的属性绑定到一个 Java 类上,这样可以更容易管理和验证这些属性。
以上是几种实现自定义配置替换的方法,根据你的具体需求选择合适的方式。
6、Spring AOP 的实现原理
AOP,也就是 Aspect-oriented Programming,译为面向切面编程,是计算机科学中的一个设计思想,旨在通过切面技术为业务主体增加额外的通知(Advice),从而对声明为"切点"(Pointcut)的代码块进行统一管理和装饰。
Spring
的AOP
实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
底层实现主要分两部分:创建AOP动态代理和调用代理
- 在启动Spring会创建AOP动态代理:
- 首先通过AspectJ解析切点表达式:在创建代理对象时,Spring AOP使用AspectJ来解析切点表达式。它会根据定义的条件匹配目标Bean的方法。如果Bean不符合切点的条件,将跳过,否则将会通动态代理包装Bean对象:具体会根据目标对象是否实现接口来选择使用JDK动态代理或CGLIB代理。这使得AOP可以适用于各种类型的目标对象。
- 在调用阶段:
- Spring AOP使用责任链模式来管理通知的执行顺序。通知拦截链包括前置通知、后置通知、异常通知、最终通知和环绕通知,它们按照配置的顺序形成链式结构。
- 通知的有序执行: 责任链确保通知按照预期顺序执行。前置通知在目标方法执行前执行,后置通知在目标方法成功执行后执行,异常通知在方法抛出异常时执行,最终通知无论如何都会执行,而环绕通知包裹目标方法,允许在方法执行前后添加额外的行为。
综上所述,Spring AOP在创建启动阶段使用AspectJ解析切点表达式如果匹配使用动态代理,而在调用阶段使用责任链模式确保通知的有序执行。这些机制共同构成了Spring AOP的底层实现。
7、JDK动态代理 和 CGLIB动态代理的区别?
Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。
7.1、JDK动态代理
如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是InvocationHandler
接口和Proxy
类。
缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。
7.2、CGLIB动态代理
通过继承实现。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB做动态代理的。
优点:目标类不需要实现特定的接口,更加灵活。
什么时候采用哪种动态代理?
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库
- 两者的区别:
- jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:
asm.jar
,它使用字节码增强技术来实现。 - 当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。
总结来说从四个方面:
- 代理对象创建方式:JDK动态代理通过Java的反射机制,在运行时动态生成一个实现了指定接口的新类。而CGLIB动态代理则是通过继承目标类,生成一个子类来实现代理。
- 代理范围:JDK动态代理只能代理实现了接口的类,而CGLIB动态代理可以代理任何类,包括没有实现接口的类。
- 性能开销:JDK动态代理使用反射机制,可能会导致一定的性能损失。CGLIB动态代理由于直接操作字节码,性能上可能更优,但在生成代理类的初始性能上稍慢。
- 适用场景:如果目标对象实现了接口,可以优先考虑JDK动态代理,因为它更为简洁,不需要额外引入库。如果目标对象没有实现接口,或者有特别需求需要继承特定类的情况,那么CGLIB是更好的选择。
8、Spring 事务的实现机制
8.1、Spring事务的基本原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
java
1. 获取连接
Connection con = DriverManager.getConnection();
2. 开启事务
con.setAutoCommit(true/false);
3. 执行CRUD
4. 提交事务/回滚事务
con.commit() / con.rollback();
5. 关闭连接 conn.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
-
配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
-
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
-
真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
8.2、Spring的事务机制
所有的数据访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或者在发生错误的时候回滚数据。
而 Spring 的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个 PlatformTransactionManager 接口,不同的数据访问技术的事务使用不同的接口实现,如表所示。
数据访问技术及实现
在程序中定义事务管理器的代码如下:
java
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
8.3、声名式事务
Spring支持声名式事务,即使用注解来选择需要使用事务的方法,将事务管理代码从业务方法中分离出来,通过aop进行封装。Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用 @Transactional
注解开启声明式事务。
java
@Transactional
public void saveSomething(Long id, String name) {
//数据库操作
}
在此处需要特别注意的是,此@Transactional注解来自org.springframework.transaction.annotation包,而不是javax.transaction。
@Transactional
相关属性如下:
属性 | 类型 | 描述 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |
AOP 代理的两种实现:
- jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到;
- cglib是子类,private的方法照样不会出现在子类里,也不能被拦截。
Java 动态代理
具体有如下四步骤:
-
通过实现 InvocationHandler 接口创建自己的调用处理器;
-
通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
-
通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
-
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
GCLIB代理
cglib(Code Generation Library)是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。
- cglib封装了asm,可以在运行期动态生成新的class(子类)。
- cglib用于AOP,jdk中的proxy必须基于接口,cglib却没有这个限制。
原理区别
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
-
如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
-
如果目标对象实现了接口,可以强制使用CGLIB实现AOP
-
如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如果是类内部方法直接不是走代理,这个时候可以通过维护一个自身实例的代理。
java
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
PersonRepository personRepository;
// 注入自身代理对象,在本类内部方法调用事务的传递性才会生效
@Autowired
PersonService selfProxyPersonService;
/**
* 测试事务的传递性
*
* @param person
* @return
*/
@Transactional
public Person save(Person person) {
Person p = personRepository.save(person);
try {
// 新开事务 独立回滚
selfProxyPersonService.delete();
} catch (Exception e) {
e.printStackTrace();
}
try {
// 使用当前事务 全部回滚
selfProxyPersonService.save2(person);
} catch (Exception e) {
e.printStackTrace();
}
personRepository.save(person);
return p;
}
@Transactional
public void save2(Person person) {
personRepository.save(person);
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void delete() {
personRepository.delete(1L);
throw new RuntimeException();
}
}
8.4、Spring 事务的传播属性
所谓spring事务的传播属性,就是定义在存在多个事务同时存在的时候,spring应该如何处理这些事务的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
8.5、数据库隔离级别
脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
总结
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如:MySQL InnoDB
8.6、Spring中的隔离级别
8.7、事务的嵌套
通过上面的理论知识的铺垫,我们大致知道了数据库事务和spring事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。
假设外层事务 Service A 的 Method A() 调用内层Service B 的 Method B()
PROPAGATION_REQUIRED(spring 默认)
如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,
那么执行 ServiceA.methodA() 的时候spring已经起了事务,
这时调用 ServiceB.methodB(),
ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,
就不再起新的事务。
假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在 ServiceA.methodA()
或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。
PROPAGATION_REQUIRES_NEW
比如我们设计 ServiceA.methodA() 的事务级别为
PROPAGATION_REQUIRED,ServiceB.methodB()
的事务级别为 PROPAGATION_REQUIRES_NEW。
那么当执行到 ServiceB.methodB() 的时候,
ServiceA.methodA() 所在的事务就会挂起,
ServiceB.methodB() 会起一个新的事务,
等待 ServiceB.methodB() 的事务完成以后,它才继续执行。
他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。
因为 ServiceB.methodB() 是新起一个事务,
那么就是存在两个不同的事务。
如果 ServiceB.methodB() 已经提交,
那么 ServiceA.methodA() 失败回滚,
ServiceB.methodB() 是不会回滚的。
如果 ServiceB.methodB() 失败回滚,
如果他抛出的异常被 ServiceA.methodA() 捕获,
ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
PROPAGATION_SUPPORTS
假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
PROPAGATION_NESTED
现在的情况就变得比较复杂了,
ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED,
此时两者之间又将如何协作呢?
ServiceB#methodB 如果 rollback,
那么内部事务(即 ServiceB#methodB)
将回滚到它执行前的 SavePoint 而外部事务
(即 ServiceA#methodA) 可以有以下两种处理方式:
a、捕获异常,执行异常分支逻辑
java
void methodA() {
try {
ServiceB.methodB();
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC();
}
}
这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而
PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
都没有办法做到这一点。
b、 外部事务回滚/提交 代码不做任何修改,
那么如果内部事务(ServiceB#methodB) rollback,
那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此),
外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback。
另外三种事务传播属性基本用不到,在此不做分析。
8.8、总结
对于项目中需要使用到事务的地方,建议开发者还是使用spring的TransactionCallback接口来实现事务,不要盲目使用spring事务注解,如果一定要使用注解,那么一定要对spring事务的传播机制和隔离级别有个详细的了解,否则很可能发生意想不到的效果。
8.9、只读事务(@Transactional(readOnly = true))的一些概念
概念:
从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。
@Transcational(readOnly=true) 这个注解一般会写在业务类上,或者其方法上,用来对其添加事务控制。当括号中添加readOnly=true, 则会告诉底层数据源,这个是一个只读事务,对于JDBC而言,只读事务会有一定的速度优化。而这样写的话,事务控制的其他配置则采用默认值,事务的隔离级别(isolation) 为DEFAULT,也就是跟随底层数据源的隔离级别,事务的传播行为(propagation)则是REQUIRED,所以还是会有事务存在,一代在代码中抛出RuntimeException,依然会导致事务回滚。
应用场合
-
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
-
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
注意:一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务。
9、Spring事务在什么情况下会失效?
9.1、事务不生效
9.1.1、访问权限问题
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:
java
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
我们可以看到add方法的访问权限被定义成了private
,这样会导致事务失效,spring要求被代理方法必须是public
的。
说白了,在AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute
返回null,即不支持事务。
java
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public
,而是private、default或protected的话,spring则不会提供事务功能。
9.1.2、方法用final修饰
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:
java
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
我们可以看到add方法被定义成了final
的,这样会导致事务失效。
为什么?
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。
9.1.3、方法内部调用
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
9.1.3.1、新加一个Service方法
这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:
java
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
9.1.3.2、在该Service类中注入自己
如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:
java
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?
答案:不会。
其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。
9.1.3.3、通过AopContent类
在该Service类中使用AopContext.currentProxy()获取代理对象
上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:
java
@Servcie
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA)AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
9.1.4、未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如:
java
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
从上面的例子,我们可以看到UserService类没有加@Service
注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。
9.1.5、多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。
这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
java
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
9.1.6、表不支持事务
周所周知,在mysql5之前,默认的数据库引擎是myisam
。
它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。
有些老项目中,可能还在用它。
在创建表的时候,只需要把ENGINE
参数设置成MyISAM
即可:
sql
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
myisam好用,但有个很致命的问题是:不支持事务
。
如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。
此外,myisam还不支持行锁和外键。
所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。
有时候我们在开发的过程中,发现某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务。
9.1.7、未开启事务
有时候,事务没有生效的根本原因是没有开启事务。
你看到这句话可能会觉得好笑。
开启事务不是一个项目中,最最最基本的功能吗?
为什么还会没有开启事务?
没错,如果项目已经搭建好了,事务功能肯定是有的。
但如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?
当然原因有很多,但没有开启事务,这个原因极其容易被忽略。
如果你使用的是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。
你所要做的事情很简单,只需要配置spring.datasource
相关参数即可。
但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
具体配置如下信息:
java
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
默默的说一句,如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。
9.2、事务不回滚
9.2.1、错误的传播特性
其实,我们在使用@Transactional
注解时,是可以指定propagation
参数的。
该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:
REQUIRED
如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。SUPPORTS
如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。MANDATORY
如果当前上下文中存在事务,否则抛出异常。REQUIRES_NEW
每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。NOT_SUPPORTED
如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。NEVER
如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。NESTED
如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:
java
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
9.2.2、自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。比如:
java
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。
如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。
9.2.3、手动抛了别的异常
即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。
java
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。
因为spring事务,默认情况下只会回滚RuntimeException
(运行时异常)和Error
(错误),对于普通的Exception(非运行时异常),它不会回滚。
4.自定义了回滚异常
在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor
参数,来完成这个功能。
但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:
java
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}
如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
这是为什么呢?
因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。
5.嵌套事务回滚多了
java
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。
why?
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
9.3、其他
9.3.1、大事务问题
在使用spring事务时,有个让人非常头疼的问题,就是大事务问题。
通常情况下,我们会在方法上@Transactional
注解,填加事务功能,比如:
java
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
但@Transactional
注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。
上面的这个例子中,在UserService类中,其实只有这两行才需要事务:
java
roleService.save(userModel);
update(userModel);
在RoleService类中,只有这一行需要事务:
java
saveData(userModel);
现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。
如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。
应对方法:
- 少用@Transactional注解
- 将查询(select)方法放到事务外
- 事务中避免远程调用
- 事务中避免一次性处理太多数据
- 非事务执行
- 异步处理
9.3.2、编程式事务
上面聊的这些内容都是基于@Transactional
注解的,主要说的是它的事务问题,我们把这种事务叫做:声明式事务
。
其实,spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务
。例如:
java
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
在spring中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的execute方法中,就实现了事务的功能。
相较于@Transactional
注解声明式事务,我更建议大家使用,基于TransactionTemplate
的编程式事务。主要原因如下:
- 避免由于spring aop问题,导致事务失效的问题。
- 能够更小粒度的控制事务的范围,更直观。
建议在项目中少使用@Transactional注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。
10、Spring中使用的设计模式有哪些
10.1、简单工厂模式
简单工厂模式的思想就是对外屏蔽创建对象的细节,将对象的获取统一内聚到一个工厂类中,BeanFactory
就是简单工厂模式的体现,根据传入一个唯一标识来获得 Bean 对象。
java
@Override
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
10.2、工厂方法模式
工厂方法模式适用于想让工厂专注创建一个对象的场景,相较于简单工厂模式,工厂方法模式思想是提供一个工厂的接口,开发者根据这个规范创建不同的工厂,然后按需使用不同的工厂创建不同的类即可。这种做法确保了工厂类也遵循开闭原则。
FactoryBean
就是典型的工厂方法模式。spring在使用getBean()
调用获得该bean时,会自动调用该bean的getObject()
方法。每个 Bean 都会对应一个 FactoryBean
,如 SqlSessionFactory
对应 SqlSessionFactoryBean
。
10.2、工厂方法模式相较于简单工厂模式的优缺点
工厂方法模式的优点:
- 符合开闭原则,相较于上面说到的简单工厂模式来说,我们无需因为增加一个类型而去修改工厂代码,我们完全可以通过实现一个新的工厂实现。
- 更符合单一职责的原则,对于单个类型创建的工厂逻辑更加易于维护。
缺点:
- 针对特定类型都需要创建特定工厂,创建的类会增加,导致项目变得臃肿。
- 因为工厂方法的模式结构,维护的成本相对于简单工厂模式会更高一些。
10.3、单例模式
一个类仅有一个实例,提供一个访问它的全局访问点。Spring 创建 Bean 实例默认是单例的。
Spring
中获取对象实例的方法即DefaultSingletonBeanRegistry
中的getSingleton
就是典型的Double-Checked Locking
单例模式代码:
java
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
//一级缓存没有需要的bean,进入该逻辑
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
//二级对象也没有,上锁进入创建逻辑
synchronized (this.singletonObjects) {
// 再次检查一级缓存也没有,避免重复创建问题
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//......
//创建对象存入二级缓存中
}
}
}
}
}
return singletonObject;
}
10.3.1、单例模式的在java中的使用优势
- 节省没必要的创建对象的时间,由于是单例的对象,所以创建一次后就可以一直使用了,所以我们无需为了一个重量级对象的创建而耗费大量的资源。
- 由于重量级对象的创建次数少了,所以我们就避免了没必要的GC。从而降低GC压力,避免没必要的
STW(Stop the World)
导致的GC停顿。
10.4、适配器模式
SpringMVC中的适配器HandlerAdatper
。由于应用会有多个Controller实现,如果需要直接调用Controller方法,那么需要先判断是由哪一个Controller处理请求,然后调用相应的方法。当增加新的 Controller,需要修改原来的逻辑,违反了开闭原则(对修改关闭,对扩展开放)。
为此,Spring提供了一个适配器接口,每一种 Controller 对应一种 HandlerAdapter
实现类,当请求过来,SpringMVC会调用getHandler()
获取相应的Controller,然后获取该Controller对应的 HandlerAdapter
,最后调用HandlerAdapter
的handle()
方法处理请求,实际上调用的是Controller的handleRequest()
。每次添加新的 Controller 时,只需要增加一个适配器类就可以,无需修改原有的逻辑。
常用的处理器适配器:SimpleControllerHandlerAdapter
,HttpRequestHandlerAdapter
,AnnotationMethodHandlerAdapter
。
java
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
public class HttpRequestHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {//handler是被适配的对象,这里使用的是对象的适配器模式
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
}
10.5、代理模式
代理模式解耦了调用者和被调用者的关系,同时通过对原生类型的代理进行增强易于拓展和维护,Spring AOP
就是通过代理模式实现增强切入,有两种方式JdkDynamicAopProxy
和Cglib2AopProxy
。我们就以JDK
代理为例查看Spring中的实现:
java
public Object getProxy(@Nullable ClassLoader classLoader) {
// 忽略代码
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
//通过被代理的类的接口以及增强逻辑创建一个增强的用户所需要的类
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
10.6、观察者模式
Spring 中 observer 模式常用的地方是 listener 的实现,如ApplicationListener
。
观察者模式是一种行为型模式。 它表示的是一种主题与订阅者之间具有依赖关系,当订阅者订阅的主题状态发生改变,会发送通知给响应订阅者,触发订阅者的响应操作。
Spring
事件驱动模型就是观察者模式很经典的一个应用。Spring
事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次发布一个通知就需要某个用户做出收到的响应,这个时候就可以利用观察者模式来解决这个问题。
事件机制的实现需要三个部分:事件源、事件、事件监听器
ApplicationEvent
抽象类[事件]
继承自jdk的EventObject
,所有的事件都需要继承ApplicationEvent
,并且通过构造器参数source得到事件源.
该类的实现类ApplicationContextEvent
表示ApplicaitonContext
的容器事件.
代码:
java
public abstract class ApplicationEvent extends EventObject {
private static final long serialVersionUID = 7099057708183571937L;
private final long timestamp;
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
public final long getTimestamp() {
return this.timestamp;
}
}
ApplicationListener
接口[事件监听器]
继承自jdk的EventListener
,所有的监听器都要实现这个接口。
这个接口只有一个onApplicationEvent()
方法,该方法接受一个ApplicationEvent
或其子类对象作为参数,在方法体中,可以通过不同对Event类的判断来进行相应的处理。
当事件触发时所有的监听器都会收到消息。
代码:
java
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E event);
}
ApplicationContext
接口[事件源]
ApplicationContext
是spring中的全局容器,翻译过来是"应用上下文"。
实现了ApplicationEventPublisher
接口。
10.7、模板方法模式
模板方法模式即固定一个算法骨架,抽象出某些可变的方法交给子类实现,Spring
的AbstractApplicationContext
的refresh
方法就是典型模板方法模式,Spring 中 jdbcTemplate
、hibernateTemplate
等,也使用到了模板模式。
java
@Override
public void refresh() throws BeansException, IllegalStateException {
// 给容器refresh加锁,避免容器处在refresh阶段时,容器进行了初始化或者销毁的操作
synchronized (this.startupShutdownMonitor) {
// .........
try {
// .........
//定义了相关接口给用户实现,该方法会通过回调的方式调用这些方法,已经实现好的细节
invokeBeanFactoryPostProcessors(beanFactory);
// 注册拦截bean创建过程的BeanPostProcessor,已经实现好的细节
registerBeanPostProcessors(beanFactory);
//模板方法的体现,用户可自定义重写该方法
onRefresh();
//.......
}
// .......
}
}
10.7.1、模板方法模式的优劣势
优势很明显:
- 算法骨架由父类定义,封装不变,扩展可变。
- 子类只需按需实现抽象类即可,易于扩展维护。
- 提取了公共代码,避免编写重复代码。
缺点:
- 可读性下降。
- 可能会导致子类泛滥问题。
10.8、装饰器模式
Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含Decorator。动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator
模式相比生成子类更为灵活。
装饰器模式即通过组合的方式对原有类的行为进行一些扩展操作即在开闭原则下的一种结构型设计模式,就以Spring
中的TransactionAwareCacheDecorator
为例,它就是实现缓存支持事务的功能,继承缓存接口,并将目标缓存类组合进来,保证原有类不被修改的情况下实现功能的扩展:
java
//继承Cache 类
public class TransactionAwareCacheDecorator implements Cache {
//行为需要扩展的目标类
private final Cache targetCache;
// 从接口那里获得的put方法,通过对targetCache的put进行进一步封装实现功能的包装
@Override
public void put(final Object key, @Nullable final Object value) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
TransactionAwareCacheDecorator.this.targetCache.put(key, value);
}
});
}
else {
this.targetCache.put(key, value);
}
}
}
10.9、责任链模式
- 责任链模式基本概念
- 责任链模式是一种行为设计模式,它将请求的发送者和接收者解耦。在这个模式中,多个对象(称为处理器或节点)被连接成一条链,每个节点都有机会处理请求。当一个请求进入责任链时,它会从链的头部开始传递,每个节点都可以选择处理该请求或者将其传递给下一个节点。
- 例如,在一个员工请假审批系统中,员工的请假申请可能需要经过直接主管、部门经理、人力资源经理等多个审批者的审批。每个审批者就是责任链中的一个节点,他们依次检查和处理请假申请。
- Spring 中责任链模式的体现:Servlet 过滤器(Filter)
- 工作原理
- 在 Java Web 应用中,Servlet 过滤器是责任链模式的一个典型例子。当一个 HTTP 请求到达 Web 应用时,它会首先经过一系列的过滤器。每个过滤器都可以对请求进行预处理,如检查用户权限、记录请求日志等。如果过滤器决定将请求传递下去,那么请求会到达下一个过滤器或者最终的 Servlet。同样,在响应阶段,过滤器也可以对响应进行后处理。
- 代码示例
- 以下是一个简单的 Servlet 过滤器示例,用于记录请求的 URL 和访问时间:
- 工作原理
java
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.util.Date;
@WebFilter("/*")
public class RequestLoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 过滤器初始化方法
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Date startTime = new Date();
System.out.println("请求URL: " + servletRequest.getRequestURL() + ",开始时间: " + startTime);
filterChain.doFilter(servletRequest, servletResponse);
Date endTime = new Date();
System.out.println("请求URL: " + servletRequest.getRequestURL() + ",结束时间: " + endTime);
}
@Override
public void destroy() {
// 过滤器销毁方法
}
}
* 在这个例子中,`doFilter`方法是核心。它首先记录了请求的开始时间和 URL,然后通过`filterChain.doFilter(servletRequest, servletResponse)`将请求传递给下一个过滤器或者 Servlet。在请求返回时,又记录了结束时间。
- Spring 拦截器(Interceptor)也是责任链模式的体现
- 工作原理
- Spring 拦截器用于在请求处理前后进行一些操作,比如权限验证、性能监控等。它类似于 Servlet 过滤器,但它是基于 Spring MVC 框架的。当一个请求到达 Spring MVC 的处理器(Handler)之前,拦截器可以进行预处理,如检查用户是否登录。在处理器处理完请求后,拦截器还可以进行后处理,如添加一些响应头信息。
- 代码示例
- 以下是一个简单的 Spring 拦截器示例,用于检查用户是否登录:
- 工作原理
java
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean intercept(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getSession().getAttribute("user") == null) {
response.sendRedirect("/login");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 处理器处理请求后执行的操作
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求完成后的清理操作等
}
}
- 在`intercept`方法中,如果用户没有登录(通过检查`session`中的`user`属性),就将用户重定向到登录页面,并且返回`false`,阻止请求继续传递;如果用户已经登录,则返回`true`,允许请求继续传递到下一个拦截器或者处理器。
- 责任链模式的优势
- 解耦请求发送者和接收者:请求的发送者不需要知道具体哪个对象会处理请求,它只需要将请求发送到责任链的头部即可。这样可以降低系统的耦合度,方便系统的维护和扩展。例如,在上述的请假审批系统中,如果需要添加一个新的审批级别,只需要在责任链中插入一个新的审批者节点,而不需要修改员工提交请假申请的方式。
- 动态组合处理逻辑:可以根据具体的需求灵活地组合和调整责任链中的节点顺序和内容。比如在 Web 应用中,可以根据不同的安全级别要求,动态地添加或删除过滤器来改变请求的处理流程。
- 增强系统的灵活性和可维护性:每个节点只负责自己特定的功能,如权限检查、日志记录等。这样使得代码的职责更加清晰,易于理解和维护。当需要修改某个功能时,只需要找到对应的节点进行修改,而不会影响到其他部分的代码。
11、请解释Spring中的@Async注解的工作原理,并说明其异步执行机制。
@Async
注解是Spring框架提供的一个用于声明异步方法的注解。当在方法上使用@Async
注解时,Spring会在运行时将该方法调用放入一个单独的线程中执行,从而实现异步执行。- 要使用
@Async
注解,首先需要在配置类中启用异步支持,通常是通过在配置类上添加@EnableAsync
注解来实现的。 - 当调用带有
@Async
注解的方法时,Spring会创建一个新的线程来执行该方法。这个新线程是由Spring的TaskExecutor
(任务执行器)来管理的。TaskExecutor
是一个接口,它定义了提交任务和执行任务的方法。在Spring中,可以使用不同的TaskExecutor
实现来配置异步任务的执行方式。 - 通过
@Async
注解和TaskExecutor
的配合使用,Spring提供了一种简单而有效的异步执行机制,使得开发人员可以轻松地实现异步任务的处理和响应。
参考
Java面试 32个核心必考点完全解析(下)_32个java面试必考点 总结-CSDN博客