决战圣地玛丽乔亚Day41 Spring其余部分
扩展点:
BeanPostProcessor:在 Bean 初始化前后进行一些处理,常用于 AOP 功能的实现。
postProcessBeforeInitialization(初始化bean前)
postProcessAfterInitialization(初始化bean后)
BeanDefinitionRegistryPostProcessor:
可以向 BeanFactory 动态注册 BeanDefinition,也可以修改已经注册的 BeanDefinition。这个在BeanFactoryPostProcessor之前进行,是在refresh的invokeBeanFactoryPostProcessors中进行处理。
执行顺序:
BeanDefinitionRegistryPostProcessor > BeanFactoryPostProcessor > BeanPostProcessor
使用的方法:
1.实现 BeanDefinitionRegistryPostProcessor 接口,重写其 postProcessBeanDefinitionRegistry 方法
允许在BeanFactoryPostProcessor执行之前调用,对BeanDefinition对象进行增删改查操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 在这里可以添加新的 BeanDefinition 或修改已有的 BeanDefinition } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } } //将该类注册为 Spring 的 Bean,在 Spring 容器启动时会自动调用其 postProcessBeanDefinitionRegistry 方法。 @Configuration public class AppConfig { @Bean public static MyBeanDefinitionRegistryPostProcessor myBeanDefinitionRegistryPostProcessor() { return new MyBeanDefinitionRegistryPostProcessor(); } } |
tips:需要注意的是,如果我们在 postProcessBeanDefinitionRegistry 方法中添加了新的 BeanDefinition,那么这些 BeanDefinition 不会立即被实例化,而是需要等到下一次的 BeanFactoryPostProcessor 处理完成后才会被实例化。因此,如果我们需要在应用中使用这些新的 Bean,需要确保这些 Bean 已经被实例化。
需要注意一个问题:
BeanDefinitionRegistryPostProcessor 接口是继承的BeanFactoryPostProcessor
BeanFactoryPostProcessor:
postProcessBeanFactory方法,在bean的实例化之前,对beanFactory进行一些后置操作。
InstantiationAwareBeanPostProcessor:在 Bean 实例化之前和之后进行一些处理,可以对 Bean 进行定制化的实例化。
postProcessBeforeInstantiation:在Bean实例化之前,允许用户返回一个代理对象或者原始Bean实例,以覆盖Spring默认的实例化过程。(Spring不会再实例化、初始化这个bean,返回的bean作为最终的bean,并且Spring容器不会管理这个bean慎用)
应用场景:单例bean的手动管理,动态代理,自定义bean实例化逻辑。
postProcessPropertyValues:在Bean的属性注入完成之后,允许用户对属性进行自定义的修改或操作。
postProcessAfterInstantiation:在Bean实例化之后,允许用户对实例进行自定义的修改或操作。
由于继承BeanPostProcessor,所以BeanPostProcessor的扩展点也包括在内:
postProcessBeforeInitialization
postProcessAfterInitialization
ApplicationListener:用于监听 Spring 事件,当事件发生时可以进行一些处理。
ContextRefreshedEvent:当Spring容器初始化或刷新时触发该事件
ContextStartedEvent:当Spring容器启动时触发该事件
ContextStoppedEvent:当Spring容器停止时触发该事件
ContextClosedEvent:当Spring容器关闭时触发该事件
RequestHandledEvent:当一个HTTP请求处理完成时触发该事件
使用的过程就是:首先我们需要实现ApplicationListener接口,并重写onApplicationEvent方法。并把我们自定义的扩展点注入到Spring容器即可。
InitializingBean :
afterPropertiesSet():实例化完成后,属性注入之后执行。
DisposableBean:容器关闭调用destroy()。
@PostConstruct 和 @PreDestroy:分别在 Bean 初始化和销毁时被调用,通过注解方式来实现。
@Autowired 和 @Resource:用于实现自动装配。
具体的顺序流大概如下所示:
如何控制事务?
声明/编程式:编程式是手写逻辑,声明式是写注解或XML。 推荐声明式的@Transactional
通过AOP的方式进行事务的管理,根据是否有接口可分为JDK动态代理和CGLIB的动态代理。
当我们使用@Transactional
注解标注一个方法时,Spring会使用TransactionInterceptor
来创建一个事务代理对象,该代理对象会继承目标对象实现的接口,并在目标对象的方法执行前后,执行事务管理相关的代码。具体来说,代理对象会在方法执行前开启事务,在方法执行后提交/回滚事务,从而实现事务管理的功能。
@Transactional失效的几种情况:
1.放在了私有方法/类上:
@Transactional是通过AOP实现,AOP是基于代理对象的,如果是通过类名调用方法的情况,不会生成代理对象调用类的方法,而是通过类名直接调用绕过了代理对象导致注解失效。
2.异常
未被捕获的异常,且异常类型不是RuntimeException. Spring只会对RuntimeException类型的异常进行回滚。
但是如果我用try-catch捕获了这个未被捕获的异常,也不会回滚,因为Spring认为异常已经被捕获,不需要回滚。
3.自调用问题
如果在 @Transactional
注解的方法中,方法内部调用该类中其他 @Transactional
注解的方法,则事务不会生效。这是因为 Spring 的事务是通过代理对象实现的,自调用会直接绕过代理对象,从而导致事务不生效
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Service @Transactional public class UserService { public void addUser() { // ... updateUser(); // 调用了同一个类中的另一个方法 // ... } @Transactional public void updateUser() { // ... } } |
当我们在其他类中通过 UserService 的实例对象调用 addUser() 方法时,Spring 会为该实例对象创建一个代理对象,从而可以触发 AOP 通知,实现事务管理。但是,当 addUser() 方法内部直接调用 updateUser() 方法时,由于编译器只知道调用的是目标方法 updateUser(),而不知道调用的是代理对象的方法,因此会直接调用 updateUser() 方法本身,而不是代理对象的方法,从而绕过了代理对象,AOP 通知不会被触发,导致事务管理失效。
如果addUser方法区调用updateUser方法,由于updateUser是同一个类的方法,是通过类调用方法本身,而不是通过代理对象去调用
的方法,绕开了代理对象。可以通过this.updateUser来避免这个问题,直接使用当前代理对象进行调用。
事务的传播机制
REQUIRED(默认值):如果当前已经存在事务,则加入该事务中,否则新开一个事务。这是最常用的传播行为。
SUPPORTS:如果当前已经存在事务,则加入该事务中,否则不开启事务。
MANDATORY:必须在一个已存在的事务中执行,否则会抛出异常。
REQUIRES_NEW:不管当前是否存在事务,都会开启一个新的事务。如果已经存在事务,则暂停当前事务,执行完新事务后再恢复原事务。
NOT_SUPPORTED:不应该在事务中执行。如果当前存在事务,则挂起该事务并执行方法,执行完方法后再恢复原事务。
NEVER:不应该在事务中执行。如果当前存在事务,则抛出异常。
NESTED:在当前事务中嵌套一个事务,如果当前不存在事务,则等价于REQUIRED。嵌套事务是外部事务的一部分,如果嵌套事务失败,则只会回滚嵌套事务本身,而不会回滚外部事务。
事务传播机制可以保证事务之间不进行干扰。
多线程事务的处理
使用线程池:多线程环境下创建大量的线程会消耗系统资源,可能导致系统性能下降,因此应该使用线程池来管理线程。
使用数据库连接池:多线程环境下频繁地打开和关闭数据库连接会增加数据库的负载,降低系统性能,因此应该使用数据库连接池来管理数据库连接。
选择合适的事务隔离级别:在多线程事务处理中,不同的事务隔离级别会影响事务的一致性和隔离性。一般建议使用Serializable(序列化)事务隔离级别,可以避免脏读、不可重复读和幻读等问题,但是会对系统性能造成一定影响。
使用分布式事务:在分布式环境下,多个应用程序之间可能需要协同完成一个事务。此时,可以使用分布式事务来保证事务的一致性。一般情况下,使用XA协议或者基于消息队列的分布式事务都可以满足要求。
使用乐观锁:在多线程环境下,使用悲观锁会降低系统性能,因此建议使用乐观锁来控制并发操作。乐观锁的实现方式有多种,比如基于版本号或者时间戳等机制。
IOC和AOP
IOC:控制反转,是指将对象的创建和依赖关系的管理交给容器来完成,而不是由程序员手动管理。IOC实现了程序的解耦,使得程序更加灵活和可扩展。Spring中的IOC容器负责创建和管理对象,并将这些对象注入到应用程序中。开发人员只需要定义对象和依赖关系的配置信息,由IOC容器来完成对象的创建和依赖关系的管理。
AOP:面向切面编程,是指通过在程序中定义切面,将横跨多个对象的通用功能封装成一个切面,从而提高程序的模块化和复用性。在Spring中,AOP通过将横切关注点(如日志记录、安全检查、事务管理等)从业务逻辑中分离出来,实现了对业务逻辑的解耦。Spring AOP实现了基于代理的AOP,在运行时生成代理对象,从而实现对切面的织入。
循环依赖的问题是怎么产生的,如何解决?
- 循环依赖的产生原因:
循环依赖产生的原因主要是因为对象之间的依赖关系相互交织,形成了一个闭环。比如,对象A依赖于对象B,对象B又依赖于对象A。在创建对象A和对象B时,容器需要注入依赖关系,但是由于对象之间相互依赖,容器无法确定先创建哪个对象,这就导致了循环依赖的问题。
- 循环依赖的解决方法:
在Spring框架中,循环依赖的解决方法主要有三种:
(1)构造器注入:使用构造器注入可以避免循环依赖的问题。因为在对象创建时,容器需要调用构造器来创建对象,所以可以在构造器中注入对象依赖,从而避免循环依赖的问题。不过,这种方式要求必须有一个对象是通过构造器注入的,不适用于使用setter方法注入的情况。
(2)setter注入:使用setter注入时,可以先创建对象A并注入依赖,然后再创建对象B并注入依赖,这样就可以避免循环依赖的问题。但是,如果A和B之间存在循环依赖,则仍然无法解决问题。
(3)使用@Lazy注解:使用@Lazy注解可以延迟对象的创建,从而避免循环依赖的问题。在Spring容器中,只有当需要使用某个Bean时,才会创建该Bean的实例。因此,可以使用@Lazy注解将循环依赖的Bean标记为延迟加载,从而避免循环依赖的问题。
懒加载和饿汉加载的区别和应用
- 懒加载
懒加载是指在需要使用对象时才进行加载,也称为延迟加载。懒加载可以提高程序的性能和内存利用率,因为只有在需要使用对象时才进行加载,避免了不必要的开销。
懒加载在Java中有多种实现方式,例如Hibernate框架中的延迟加载机制,Spring框架中的@Lazy注解等。使用懒加载时需要注意,如果对象的初始化比较耗时,那么在第一次使用时可能会出现延迟现象,降低程序的响应速度。
- 饿汉加载
饿汉加载是指在程序启动时就进行对象的加载,也称为立即加载。饿汉加载可以保证对象在使用时已经准备好,避免了延迟现象,但也会造成资源浪费,因为有些对象在启动时可能并没有被用到。
饿汉加载在Java中的应用很广泛,例如单例模式中就采用了饿汉加载的方式。单例模式是一种创建对象的方式,它保证在整个程序中只有一个实例。使用饿汉加载时,在程序启动时就会创建该实例,以保证在整个程序中只有一个对象被使用。
- 区别和应用
懒加载和饿汉加载的区别在于对象的创建时间。懒加载会在需要使用对象时才进行加载,而饿汉加载会在程序启动时就进行加载。因此,懒加载可以避免资源浪费,提高程序的性能和内存利用率,但可能会出现延迟现象;饿汉加载可以保证对象在使用时已经准备好,避免了延迟现象,但可能会造成资源浪费。
懒加载和饿汉加载的应用主要取决于对象的使用情况和程序的性能需求。如果对象不需要在程序启动时就被加载,可以使用懒加载来提高程序的性能和内存利用率;如果对象需要在程序启动时就被加载,并且对性能要求较高,可以使用饿汉加载来避免延迟现象。
Spring中的单例/多例模式的实现
- 单例模式
Spring框架默认使用单例模式创建对象,即在应用上下文(ApplicationContext)中,每个bean只会被创建一次。如果在配置文件中没有特别指定,所有bean都将默认为单例模式。
在Spring中使用单例模式可以节省系统资源,因为只有一个实例被创建并重复使用。同时,由于Spring框架对单例模式进行了优化,所以在多线程环境下也能保证单例对象的线程安全。
如何保证?
-
延迟加载:Spring框架默认使用延迟加载机制来创建单例对象。即只有在第一次使用时才会创建实例,而不是在容器启动时就创建实例。这样可以减少系统开销,避免不必要的对象创建和资源浪费。
-
线程安全:Spring框架对单例模式进行了线程安全的处理,确保多线程环境下单例对象的安全性。具体来说,Spring使用synchronized关键字对实例化过程进行同步,保证同一时间只有一个线程可以访问单例对象的创建过程。同时,Spring还提供了双重检查锁定的机制,避免了每次获取单例对象时都需要进行同步的开销。
-
对象池:Spring框架还提供了对象池的机制,可以重复使用单例对象,避免频繁创建和销毁实例的开销,提高系统性能。
-
序列化:Spring框架支持对单例对象的序列化和反序列化操作,确保单例对象在分布式环境下的安全性和可用性。
关于双重检查锁定,在之前的volatile中提到过
https://www.cnblogs.com/dwj-ngu/p/17130745.html,
大概的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class Singleton { private volatile static Singleton instance; private Singleton() { // 私有构造方法 } public static Singleton getInstance() { if (instance == null ) { // 第一次检查 synchronized (Singleton. class ) { // 同步代码块 if (instance == null ) { // 第二次检查 instance = new Singleton(); // 创建单例对象 } } } return instance; } } |
- 多例模式
在Spring中,如果需要创建多个相同的bean实例,可以将其配置为多例模式。在配置文件中,可以使用"scope"属性指定为"prototype"来实现多例模式
在应用中,单例模式适用于那些只需要一个实例的情况,如配置信息类、日志类等;而多例模式适用于那些需要频繁创建和销毁的对象,如连接池、线程池等。
需要注意的是,在使用多例模式时要确保正确地管理对象的生命周期,避免因为对象过多而导致内存溢出等问题。同时,也需要考虑线程安全等方面的问题,以保证程序的正确性和稳定性。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
2021-03-26 Java-Core
2021-03-26 时隔多年那些想说的话。