Java高级开发高频面试题(八)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: Java高级开发高频面试题

🌟 4.深入理解消息中间件

解决过各种消息通讯场景的疑难问题,消息中间件(Kafka、RabbitMQ、RocketMQ)出现的消息可靠投递、消息丢失、消息顺序性、消息延迟、过期失效、消息队列满了、消息高可用等问题都有着不错的实战解决方案。有消息中间件调优经验,如CPU、内存、磁盘、网络、操作系统、MQ本身配置优化等。

🍊 三种mq对比

使用消息队列有解耦,扩展性,削峰,异步等功能,市面上主流的几款mq,rabbitmq,rocketmq,kafka有各自的应用场景。kafka,有出色的吞吐量,比较强悍的性能,而且集群可以实现高可用,就是会丢数据,所以一般被用于日志分析和大数据采集。rabbitmq,消息可靠性比较高,支持六种工作模式,功能比较全面,但是由于吞吐量比较低,消息累积还会影响性能,加上erlang语言不好定制,所以一般使用于小规模的场景,大多数是中小企业用的比较多。rocketmq,高可用,高性能,高吞吐量,支持多种消息类型,比如同步,异步,顺序,广播,延迟,批量,过滤,事务等等消息,功能比较全面,只不过开源版本比不上商业版本的,加上开发这个中间件的大佬写的文档不多,文档不太全,这也是它的一个缺点,不过这个中间件可以作用于几乎全场景。

🍊 消息丢失

消息丢失,生产者往消息队列发送消息,消息队列往消费者发送消息,会有丢消息的可能,消息队列也有可能丢消息,通常MQ存盘时都会先写入操作系统的缓存页中,然后再由操作系统异步的将消息写入硬盘,这个中间有个时间差,就可能会造成消息丢失,如果服务挂了,缓存中还没有来得及写入硬盘的消息就会发生消息丢失。

不同的消息中间件对于消息丢失也有不同的解决方案,先说说最容易丢失消息的kafka吧。生产者发消息给Kafka Broker:消息写入Leader后,Follower是主动与Leader进行同步,然后发ack告诉生产者收到消息了,这个过程kafka提供了一个参数,request.required.acks属性来确认消息的生产,0表示不进行消息接收是否成功的确认,发生网络抖动消息丢了,生产者不校验ACK自然就不知道丢了。1表示当Leader接收成功时确认,只要Leader存活就可以保证不丢失,保证了吞吐量,但是如果leader挂了,恰好选了一个没有ACK的follower,那也丢了。-1或者all表示Leader和Follower都接收成功时确认,可以最大限度保证消息不丢失,但是吞吐量低,降低了kafka的性能。一般在不涉及金额的情况下,均衡考虑可以使用1,保证消息的发送和性能的一个平衡。Kafka Broker 消息同步和持久化:Kafka通过多分区多副本机制,可以最大限度保证数据不会丢失,如果数据已经写入系统缓存中,但是还没来得及刷入磁盘,这个时候机器宕机,或者没电了,那就丢消息了,当然这种情况很极端。Kafka Broker 将消息传递给消费者:如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交offset了,但是此时消费者直接宕机了,未处理完的数据丢失了,下次也消费不到了。所以为了避免这种情况,需要将配置改为,先消费处理数据,然后手动提交,这样消息处理失败,也不会提交成功,没有丢消息。

rabbitmq整个消息投递的路径是producer—>rabbitmq broker—>exchange—>queue—>consumer。

生产者将消息投递到Broker时产生confirm状态,会出现二种情况,ack:表示已经被Broker签收。nack:表示表示已经被Broker拒收,原因可能有队列满了,限流,IO异常等。生产者将消息投递到Broker,被Broker签收,但是没有对应的队列进行投递,将消息回退给生产者会产生return状态。这二种状态是rabbitmq提供的消息可靠投递机制,生产者开启确认模式和退回模式。使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。消费者在rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认。none自动确认模式很危险,当生产者发送多条消息,消费者接收到一条信息时,会自动认为当前发送的消息已经签收了,这个时候消费者进行业务处理时出现了异常情况,也会认为消息已经正常签收处理了,而队列里面显示都被消费掉了。所以真实开发都会改为手动签收,可以防止消息丢失。消费者如果在消费端没有出现异常,则调用channel.basicAck方法确认签收消息。消费者如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。通过一系列的操作,可以保证消息的可靠投递以及防止消息丢失的情况。

然后说一下rocketmq,生产者使用事务消息机制保证消息零丢失,第一步就是确保Producer发送消息到了Broker这个过程不会丢消息。发送half消息给rocketmq,这个half消息是在生产者操作前发送的,对下游服务的消费者是不可见的。这个消息主要是确认RocketMQ的服务是否正常,通知RocketMQ,马上要发一个消息了,做好准备。half消息如果写入失败就认为MQ的服务是有问题的,这个时候就不能通知下游服务了,给生产者的操作加上一个状态标记,然后等待MQ服务正常后再进行补偿操作,等MQ服务正常后重新下单通知下游服务。然后执行本地事务,比如说下了个订单,把下单数据写入到mysql,返回本地事务状态给rocketmq,在这个过程中,如果写入数据库失败,可能是数据库崩了,需要等一段时间才能恢复,这个时候把订单一直标记为"新下单"的状态,订单的消息先缓存起来,比如Redis、文本或者其他方式,然后给RocketMQ返回一个未知状态,未知状态的事务状态回查是由RocketMQ的Broker主动发起的,RocketMQ过一段时间来回查事务状态,在回查事务状态的时候,再尝试把数据写入数据库,如果数据库这时候已经恢复了,继续后面的业务。而且即便这个时候half消息写入成功后RocketMQ挂了,只要存储的消息没有丢失,等RocketMQ恢复后,RocketMQ就会再次继续状态回查的流程。第二步就是确保Broker接收到的消息不会丢失,因为RocketMQ为了减少磁盘的IO,会先将消息写入到os缓存中,不是直接写入到磁盘里面,消费者从os缓存中获取消息,类似于从内存中获取消息,速度更快,过一段时间会由os线程异步的将消息刷入磁盘中,此时才算真正完成了消息的持久化。在这个过程中,如果消息还没有完成异步刷盘,RocketMQ中的Broker宕机的话,就会导致消息丢失。所以第二步,消息支持持久化到Commitlog里面,即使宕机后重启,未消费的消息也是可以加载出来的。把RocketMQ的刷盘方式 flushDiskType配置成同步刷盘,一旦同步刷盘返回成功,可以保证接收到的消息一定存储在本地的内存中。采用主从机构,集群部署,Leader中的数据在多个Follower中都存有备份,防止单点故障,同步复制可以保证即使Master 磁盘崩溃,消息仍然不会丢失。但是这里还会有一个问题,主从结构是只做数据备份,没有容灾功能的。也就是说当一个master节点挂了后,slave节点是无法切换成master节点继续提供服务的。所以在RocketMQ4.5以后的版本支持Dledge,DLedger是基于Raft协议选举Leader Broker的,当master节点挂了后,Dledger会接管Broker的CommitLog消息存储 ,在Raft协议中进行多台机器的Leader选举,发起一轮一轮的投票,通过多台机器互相投票选出来一个Leader,完成master节点往slave节点的消息同步。数据同步会通过两个阶段,一个是uncommitted阶段,一个是commited阶段。Leader Broker上的Dledger收到一条数据后,会标记为uncommitted状态,然后他通过自己的DledgerServer组件把这个uncommitted数据发给Follower Broker的DledgerServer组件。接着Follower Broker的DledgerServer收到uncommitted消息之后,必须返回一个ack给Leader Broker的Dledger。然后如果Leader Broker收到超过半数的Follower Broker返回的ack之后,就会把消息标记为committed状态。再接下来, Leader Broker上的DledgerServer就会发送committed消息给Follower Broker上的DledgerServer,让他们把消息也标记为committed状态。这样,就基于Raft协议完成了两阶段的数据同步。第三步,Cunmser确保拉取到的消息被成功消费,就需要消费者不要使用异步消费,有可能造成消息状态返回后消费者本地业务逻辑处理失败造成消息丢失的可能。用同步消费方式,消费者端先处理本地事务,然后再给MQ一个ACK响应,这时MQ就会修改Offset,将消息标记为已消费,不再往其他消费者推送消息,在Broker的这种重新推送机制下,消息是不会在传输过程中丢失的。

🍊 消息重复消费

消息重复消费的问题

第一种情况是发送时消息重复,当一条消息已被成功发送到服务端并完成持久化,此时出现了网络抖动或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

第二种情况是投递时消息重复,消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,tMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

第三种情况是负载均衡时消息重复,比如网络抖动、Broker 重启以及订阅方应用重启,当MQ的Broker或客户端重启、扩容或缩容时,会触发Rebalance,此时消费者可能会收到重复消息。

那么怎么解决消息重复消费的问题呢?就是对消息进行幂等性处理。

在MQ中,是无法保证每个消息只被投递一次的,因为网络抖动或者客户端宕机等其他因素,基本都会配置重试机制,所以要在消费者端的业务上做消费幂等处理,MQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,业务上可以用这个MessageId加上业务的唯一标识来作为判断幂等的关键依据,例如订单ID。而这个业务标识可以使用Message的Key来进行传递。消费者获取到消息后先根据id去查询redis/db是否存在该消息,如果不存在,则正常消费,消费完后写入redis/db。如果存在,则证明消息被消费过,直接丢弃。

🍊 消息顺序

消息顺序的问题,如果发送端配置了重试机制,mq不会等之前那条消息完全发送成功,才去发送下一条消息,这样可能会出现发送了1,2,3条消息,但是第1条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2,3,1了。RocketMQ消息有序要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才行。在发送者端:在默认情况下,消息发送者会采取Round Robin轮询方式把消息发送到不同的分区队列,而消费者消费的时候也从多个MessageQueue上拉取消息,这种情况下消息是不能保证顺序的。而只有当一组有序的消息发送到同一个MessageQueue上时,才能利用MessageQueue先进先出的特性保证这一组消息有序。而Broker中一个队列内的消息是可以保证有序的。在消费者端:消费者会从多个消息队列上去拿消息。这时虽然每个消息队列上的消息是有序的,但是多个队列之间的消息仍然是乱序的。消费者端要保证消息有序,就需要按队列一个一个来取消息,即取完一个队列的消息后,再去取下一个队列的消息。而给consumer注入的MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队列来取的。MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个Message中取一批数据,默认不超过32条。因此也无法保证消息有序。RocketMQ 在默认情况下不保证顺序,要保证全局顺序,需要把 Topic 的读写队列数设置为 1,然后生产者和消费者的并发设置也是 1,不能使用多线程。所以这样的话高并发,高吞吐量的功能完全用不上。全局有序就是无论发的是不是同一个分区,我都可以按照你生产的顺序来消费。分区有序就只针对发到同一个分区的消息可以顺序消费。kafka保证全链路消息顺序消费,需要从发送端开始,将所有有序消息发送到同一个分区,然后用一个消费者去消费,但是这种性能比较低,可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列(可以搞多个),一个内存队列开启一个线程顺序处理消息。RabbitMq没有属性设置消息的顺序性,不过我们可以通过拆分为多个queue,每个queue由一个consumer消费。或者一个queue对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理,保证消息的顺序性。

🍊 消息积压

线上有时因为发送方发送消息速度过快,或者消费方处理消息过慢,可能会导致broker积压大量未消费消息。消息数据格式变动或消费者程序有bug,导致消费者一直消费不成功,也可能导致broker积压大量未消费消息。解决方案可以修改消费端程序,让其将收到的消息快速转发到其他主题,可以设置很多分区,然后再启动多个消费者同时消费新主题的不同分区。可以将这些消费不成功的消息转发到其它队列里去,类似死信队列,后面再慢慢分析死信队列里的消息处理问题。另外在RocketMQ官网中,还分析了一个特殊情况,如果RocketMQ原本是采用的普通方式搭建主从架构,而现在想要中途改为使用Dledger高可用集群,这时候如果不想历史消息丢失,就需要先将消息进行对齐,也就是要消费者把所有的消息都消费完,再来切换主从架构。因为Dledger集群会接管RocketMQ原有的CommitLog日志,所以切换主从架构时,如果有消息没有消费完,这些消息是存在旧的CommitLog中的,就无法再进行消费了。这个场景下也是需要尽快的处理掉积压的消息。

🍊 延迟队列

消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费者才能获取这个消息进行消费。例如10分钟,内完成订单支付,支付完成后才会通知下游服务进行进一步的营销补偿。往MQ发一个延迟1分钟的消息,消费到这个消息后去检查订单的支付状态,如果订单已经支付,就往下游发送下单的通知。而如果没有支付,就再发一个延迟1分钟的消息。最终在第10个消息时把订单回收,就不用对全部的订单表进行扫描,而只需要每次处理一个单独的订单消息。这个就是延迟对列的应用场景。rabbittmq,rocketmq都可以通过设置ttl来设置延迟时间,kafka则是可以在发送延时消息的时候,先把消息按照不同的延迟时间段发送到指定的队列中,比如topic_1s,topic_5s,topic_10s,topic_2h,然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了。

mq设置过期时间,就会有消息失效的情况,如果消息在队列里积压超过指定的过期时间,就会被mq给清理掉,这个时候数据就没了。解决方案也有手动写程序,将丢失的那批数据,一点点地查出来,然后重新插入到 mq 里面去。

🍊 消息队列高可用

对于RocketMQ来说可以使用Dledger主从架构来保证消息队列的高可用,这个在上面也有提到过。然后在说说rabbitmq,它提供了一种叫镜像集群模式,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,可以在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。只不过消息需要同步到所有机器上,导致网络带宽压力和消耗很重。最后再说说kafka,它是天然的分布式消息队列,在Kafka 0.8 以后,提供了副本机制,一个 topic要求指定partition数量,每个 partition的数据都会同步到其它机器上,形成自己的多个 replica 副本,所有 replica 会选举一个 leader 出来,其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去。如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来。

🌟 5.深入理解开源框架

熟悉Spring中Bean的生命周期与线程安全、单例模式的单例Bean、Spring AOP底层实现原理、Spring循环依赖、Spring容器启动流程、Spring事务及传播机制底层原理、Spring IOC容器加载过程与依赖注入、Spring的自动装配、Spring6.0核心新特性;Spring Boot自动装配、Spring Boot启动过程、Spring Framework的SPI机制;SpringMVC执行流程;Dubbo服务发现与调用、Dubbo容错机制、Dubbo负载均衡、Dubbo序列化协议、动态感知服务下线;ZooKeeper选举、脑裂与假死、Zab协议、Quorum机制、ACL访问控制列表。深入理解@Configuration、@Autowired、@Resource、@ComponentScan、@Conditional、@Lazy、@Primary、@Import、@SpringBootApplication等注解的底层实现。

🍊 Spring Bean的生命周期

Spring Bean的生命周期决定了一个Bean的整个生命周期,它分为四个阶段:实例化、属性赋值、初始化和销毁。

实例化通过构造器实例化和工厂方法实例化两种方式实现;构造器实例化是指通过Java类的构造函数实例化Bean。在Spring中,构造函数可以是无参构造函数,也可以是有参构造函数。Spring通过利用Java反射机制,调用bean对应类的构造方法进行实例化。在XML文件中,可以使用标签的class属性指定要实例化的Bean类。当容器启动时,容器会根据class属性的全限定类名使用反射机制实例化Bean。在注解方式中,@Component、@Service、@Controller等注解都是用来标注Bean类,容器会根据这些注解的信息进行实例化。工厂方法实例化是指通过Java类的静态方法实例化Bean,实现方式类似于单例模式的实现方式。在Spring中,通过配置工厂方法的返回值和参数,实现Bean的实例化。

实例化前后的InstantiationAwareBeanPostProcessor接口是Spring框架中的一个扩展点,例如SmartInstantiationAwareBeanPostProcessor接口主要作用是在Bean实例化之前,提供对Bean实例化的更细粒度的控制,并提供给实现者对AOP代理和"热插拔"类等功能的支持,通过该接口,我们可以在Bean实例化之前完成对Bean实例化的"拦截",并加入自己的处理逻辑。在Bean实例化之前,这些接口可以用来修改Bean的实例化过程,或者进行Bean类型转换,AOP代理等相关操作;在Bean实例化后,这些接口可以用来修改Bean的属性值、进行Bean的修饰、或者完成其他需要在Bean实例化后执行的操作。它提供了在Bean实例化过程中的多个扩展点,在实际应用中,InstantiationAwareBeanPostProcessor接口可以用于实现很多的扩展功能。例如:AOP技术,可以通过实现该接口来在Bean实例化和属性设置过程中进行代理和增强操作。数据库连接池技术,可以通过实现该接口来在Bean实例化过程中进行数据库连接和释放操作。外部配置文件加载,可以通过实现该接口来在Bean实例化过程中加载外部配置文件,并将其设置到Bean实例中。

属性赋值是Spring Bean生命周期的第二个阶段,它是通过BeanPostProcessor接口实现的。BeanPostProcessor在实例化Bean后,对Bean进行属性赋值。属性赋值可以通过XML文件或注解方式进行配置,在XML文件中,可以使用标签或标签进行属性赋值。Spring容器在实例化Bean后,会遍历所有的BeanPostProcessor实现类,调用它们的postProcessBeforeInitialization()方法,进行属性赋值操作。这个方法的返回值是一个对象,可以修改或替换原始的Bean实例。在注解方式中,可以使用@Autowired或@Value注解进行属性赋值。这些注解的实现原理也是基于BeanPostProcessor接口实现的。

属性注入之后,开始执行Aware,Spring框架提供了Aware相关接口,如BeanNameAware、BeanFactoryAware、ApplicationContextAware等,通过实现 BeanNameAware接口,Bean可以获取到Spring容器中创建的Bean的名称,可以在Bean中直接调用该名称对应的Bean。通过实现BeanFactoryAware接口,Bean可以获取到Spring容器中的BeanFactory,可以通过BeanFactory获取其他Bean的实例。通过实现ApplicationContextAware接口,Bean可以获取到Spring容器的ApplicationContext,可以通过ApplicationContext获取其他Bean的实例和容器中的其他资源。

初始化是Spring Bean生命周期的第三个阶段,它包括两个过程:初始化前和初始化后。Spring提供了一个扩展点BeanPostProcessor,BeanPostProcessor是在Bean的创建过程中,在执行初始化方法之前和之后的扩展点,它定义了两个方法:postProcessBeforeInitialization()和postProcessAfterInitialization()。

postProcessBeforeInitialization()方法在执行Bean的初始化方法之前被调用,可以对Bean进行自定义的初始化操作。例如,可以修改Bean的属性值、增加一些代理逻辑等等。此时,Bean还没有执行初始化方法,也就是说Bean还没有完全初始化。这个方法常常用于注册一些事件监听器、给Bean进行数据校验等。

实现InitializingBean接口是一种在Spring框架中初始化Bean的方式,这种方式要求Bean实现InitializingBean接口,并且实现afterPropertiesSet()方法,在该方法中执行Bean的初始化操作。Spring容器在创建Bean实例之后,会自动调用afterPropertiesSet()方法完成Bean的初始化。除了实现InitializingBean接口,还可以通过@Bean注解中的initMethod属性,或者在XML配置文件中使用元素的init-method属性来指定Bean的初始化方法。

postProcessAfterInitialization()方法在执行Bean的初始化方法之后被调用,可以对Bean进行自定义的后处理操作。例如,可以对Bean做一些额外的检查、修改某些属性值等等。此时,Bean已经执行了初始化方法,并且已经完全初始化。这个方法常常用于增强Bean的能力或者为Bean提供一些额外服务(如数据缓存、资源池等)。

销毁是Spring Bean生命周期的最后一个阶段,它是通过实现DisposableBean接口或通过配置destroy-method方法来实现。实现DisposableBean接口,需要实现destroy()方法,该方法会在Bean销毁前被调用。在容器关闭之前,Spring会先销毁Bean,并回调Bean的destroy()方法。在XML文件中,可以使用destroy-method属性指定Bean的销毁方法。Spring容器会在销毁Bean之前调用这个方法。通过实现DisposableBean接口,Bean类可以在调用destroy()方法之前实现销毁操作。该方法会在Bean销毁之前调用。销毁的具体过程可以自定义实现。在销毁Bean之前,需要先关闭应用上下文,释放Bean占用的资源。

举个例子:假设你是一位追求完美的花艺师。你在春天的花展上,带来了你独特的艺术作品。这次,你决定采用Spring Bean来打造花展中的一个展位。

首先,你需要在花展中找到一个空间。这个空间就相当于Spring容器中的配置文件。你需要在配置文件中声明Bean,并为它们设置属性和依赖关系,这些都是在属性设置阶段完成的。在这个过程中,你会逐步构建出一个原型对象。

接下来,你需要把所有花束放在花架上。这就相当于初始化前阶段,你会在这个阶段进行一些准备工作,比如检查花束的数量、清洁花束、调整花束的位置等。在这个阶段,你也可以使用实现了InitializingBean接口的类,来执行一些自定义的初始化操作。

初始化后阶段就像你在花展前调整花束的最后一次机会。你可以使用实现了BeanPostProcessor接口的类,来拦截初始化过程,对花束进行修改或封装。在一切准备就绪后,你的花束就已经完全初始化了。

展示过程中,你需要不断调整花束的状态和位置,以确保它们始终保持最佳状态。这就相当于Bean的生命周期中的实例化和初始化阶段。在这个过程中,你可以使用反射机制或者Setter方法来注入属性,并使用xml文件中声明的init-method属性来指定初始化方法。

当所有展示结束后,你需要开始收拾花束,并将它们妥善地保存。这就相当于Bean的销毁阶段。在这个阶段,你可以使用实现了DisposableBean接口的类,来执行一些自定义的销毁操作。你也可以使用xml文件中声明的destroy-method属性来指定销毁方法。

总之,Spring Bean的生命周期就像是展示你的花艺作品一样。你需要在花展中找到空间、搭建花架、放置花束、调整花束的位置和状态、收拾花束和花架,这个过程非常重要,对于你的花艺作品和Spring应用的性能都至关重要。

🍊 Spring Bean线程安全

解释线程安全:首先,需要解释什么是线程安全。线程安全是多线程环境下保证数据安全的一种策略。在Spring中,Bean的线程安全是指确保多个线程可以共享一个Bean实例,而不会导致数据不一致或其他线程安全问题。在Spring框架中,由于默认的Bean定义是单例的,所以如果不进行特殊处理,Bean的生命周期方法,包括初始化方法和销毁方法,可能在不同线程中同时执行,这就存在线程安全问题。

Bean的作用域:Bean的作用域是影响线程安全的一个重要因素。在Spring框架中,默认的Bean作用域是单例的,这意味着所有线程共享一个Bean实例。如果Bean是有状态的,也就是它保存了数据,多个线程可能同时访问和修改它的状态,这就可能导致线程安全问题。

解决方法:为了解决Spring Bean的线程安全问题,可以采用以下方法。首先,可以将Bean的作用域设置为原型(prototype),这样每次获取Bean时都会创建一个新的实例,避免多个线程共享一个实例。其次,可以在Bean的生命周期方法上使用同步注解(@Synchronized),确保在同一时间只有一个线程执行这些方法,避免并发访问和修改状态。此外,还可以使用代理模式来包装Bean,通过代理的调用保证线程安全。而且Spring框架本身提供了很多线程安全的策略,例如使用ThreadLocal变量来存储线程的私有数据,避免数据竞争。也可以使用 AOP 拦截器来确保 Bean 的方法只由一个线程访问。可以使用 Spring Framework 的 @AspectJ 注释来创建拦截器,并指定拦截器适用于哪些 Bean 和方法。

安全性评估:最后,需要评估Spring Bean的线程安全性。对于无状态的Bean,由于没有数据存储功能,所以多个线程共享一个实例也不会产生线程安全问题。对于有状态的Bean,如果多个线程同时访问和修改其状态,就可能产生线程安全问题,需要采取相应的措施进行处理。

想象一下你正在经营一家快餐店,并且有很多顾客同时来到店里。如果你的服务员只有一张厨房订单,那么他们可能会在交叉的订单上工作,导致混乱和错误。为了确保订单正确无误,你决定让每个服务员都有自己的订单本子,他们可以在上面记录每个顾客的点餐内容。这样,即使有多个服务员同时处理订单,他们也不会相互干扰。

这就好比 Spring Bean 的线程安全问题。当多个线程同时访问同一个 Bean 实例时,可能会导致线程安全问题,例如竞争条件、死锁等等。为了解决这个问题,Spring Framework 通过提供不同的 Bean 作用域来保证线程安全。作用域为 singleton 的 Bean 实例是线程不安全的,因为单个实例将在整个应用程序中共享。如果多个线程同时访问相同的 singleton Bean 实例,则可能会发生冲突和数据损坏。

🎉 Spring Bean 的实现原理

Spring Bean的实现原理是通过IoC容器来控制对象的实例化和生命周期,并通过依赖注入来降低应用程序的耦合度。

在 Spring Framework 中,Bean 实例是通过 BeanFactory 或 ApplicationContext 接口来创建和管理的。BeanFactory 是 Spring Framework 的核心接口,它提供了创建、配置和管理 Bean 实例的方法。ApplicationContext 是 BeanFactory 接口的子接口,它提供了更高级别的特性,例如事件发布、国际化和各种应用程序层次结构上下文。

当应用程序启动时,Spring 容器将读取并解析 ApplicationContext 或 BeanFactory 配置,并创建和初始化所有标记为 Bean 的类。在创建 Bean 时,Spring 容器将根据 Bean 的作用域范围来决定创建新实例还是返回现有实例。如果 Bean 的作用域范围为单例模式,Spring 容器将创建一个实例并在整个应用程序中共享该实例。如果 Bean 的作用域范围为原型模式,则每次调用该 Bean 时都会创建一个新实例。无论哪种作用域,都可以在 Bean 的定义中指定。

Spring Bean 的线程安全性取决于其作用域范围以及其依赖项的线程安全性。如果 Bean 的作用域范围为 prototype,则每个线程将拥有自己的 Bean 实例,并且不会相互干扰,因此是线程安全的。如果 Bean 的作用域范围为 singleton,并且 Bean 的依赖项是线程安全的,则 Bean 也是线程安全的。如果 Bean 的依赖项是线程不安全的,则该 Bean 在多线程环境中可能会存在线程安全问题,即使是 prototype 作用域的 Bean。

在 Spring Framework 的实现中,Bean 实例是由对象工厂创建的。这些对象工厂实际上是 Spring 容器的基本组成部分,可以通过 BeanFactory 或 ApplicationContext 接口访问。对象工厂是一个工厂模式,它封装了对象的实际创建过程。在创建 Bean 实例时,对象工厂将使用 BeanDefinition 和 BeanWrapper 对象来指示如何创建和管理 Bean 实例。BeanDefinition 包含 Bean 的元数据信息,例如名称、作用域、类名、构造函数参数、属性、依赖项等等。BeanWrapper 是一个包装器对象,用于访问和操作 Bean 实例的属性。

🍊 单例模式的单例Bean

单例模式的概念:单例模式是一种设计模式,它保证在一个应用程序中只有一个实例存在。这个实例可以被全局访问,但是不能被实例化多次。

单例Bean的定义:在Spring框架中,单例Bean是指作用域为singleton的Bean。这意味着在Spring IoC容器中,只有一个Bean的实例被创建,并且这个实例可以被所有的Bean和应用程序访问。

单例Bean的实现方式:在Spring中,单例Bean的实现方式是通过在配置文件中指定Bean的作用域为singleton。当IoC容器创建一个Bean时,它会检查该Bean的作用域是否为singleton。如果是,IoC容器只会创建一个Bean实例,并将该实例保存到缓存中,以便后续的请求可以直接访问该实例。

单例Bean的生命周期:单例Bean的生命周期包括三个阶段:实例化阶段、属性赋值阶段、初始化阶段和销毁阶段。这些阶段可以通过相应的回调方法来实现,例如构造器、配置器、初始化器和销毁器。

单例Bean的线程安全问题:由于单例Bean只有一个实例,因此在多线程环境下可能会存在线程安全问题。如果多个线程同时访问该实例,可能会导致数据竞争或者线程安全问题。为了避免这种情况,我们可以使用同步机制或者ThreadLocal变量来确保线程安全。

单例Bean的优势和不足:单例Bean的优势在于它可以全局访问,避免了重复创建实例的开销。但是,单例Bean也存在一些不足,例如它可能会占用较多的内存,并且在多线程环境下可能需要额外的同步机制来确保线程安全。

举个例子:假设你是一名游戏开发者,正在开发一款多人在线游戏。在游戏中,玩家需要共享很多数据,例如游戏关卡、角色等级和经验值等等。为了确保数据的一致性和节约内存空间,你需要使用单例模式来管理这些数据。

你想到了一个方法,那就是在游戏启动时创建一个GameManager类,并将其设置为单例模式。GameManager类中保存了游戏的全部数据,并提供了各种方法来对外暴露这些数据。在玩家进入游戏时,GameManager会通过网络从服务器上获取最新的数据,并更新本地数据。在玩家离开游戏时,GameManager会将本地数据上传到服务器上。

在Spring中,单例Bean的实现原理跟这个GameManager类有些相似。Spring会在启动时创建所有的单例Bean,并将其放入一个单例池中。当有请求需要使用某个单例Bean时,Spring会从单例池中获取它并返回给请求方。同时,Spring会为每个单例Bean创建一个代理对象,用于提供一些基础的功能,例如依赖注入和AOP切面。最后,Spring会按照Bean依赖关系的顺序逐个完成单例Bean的初始化工作。

需要注意的是,如果在单例Bean中保存了共享状态,可能会在并发场景下出现问题,因此应该尽可能地避免这种情况。在编写业务逻辑时,应该尽量采用无状态的方式,将状态保存在局部变量中,而不是保存在单例Bean中。

🍊 Spring AOP底层实现原理

Spring AOP底层实现原理主要涉及以下三个方面:

  1. JDK动态代理:JDK动态代理是Spring AOP的默认实现方式。它基于Java反射机制,能够在运行时动态生成代理对象,从而实现对目标对象的方法拦截。Spring AOP使用JDK动态代理实现基于接口的代理,只有实现了接口的类才能被代理。

JDK 动态代理的流程如下:通过 java.lang.reflect.Proxy 类的 newInstance() 方法生成代理对象。在生成代理对象时需传递一个实现了 java.lang.reflect.InvocationHandler 接口的类的实例,即 InvocationHandler 对象。代理对象调用任何方法都会被转发到 InvocationHandler 对象的 invoke() 方法中执行。在 invoke() 方法中,根据方法名和参数类型等信息,使用反射机制调用被代理对象的原始方法。

  1. CGLIB动态代理:CGLIB动态代理是另一种实现AOP的方式,它是基于字节码实现的动态代理,可以代理那些没有实现接口的类。在运行时,CGLIB通过生成目标类的子类来拦截方法调用,从而实现AOP功能。

CGLIB 的流程如下:通过 CGLIB 提供的 Enhancer 类来生成代理对象。在生成代理对象时需传递一个实现了 MethodInterceptor 接口的类的实例,即 Callback 对象。代理对象调用任何方法都会被转发到 Callback 对象中的 intercept() 方法中执行。在 intercept() 方法中,根据方法名和参数类型等信息,使用反射机制调用被代理对象的原始方法。

  1. 代理链的创建:Spring AOP采用代理链来实现方法拦截。在代理链中,每个代理对象拦截目标对象的方法调用,并将请求传递给下一个代理对象,直到目标对象的方法被调用。代理链的创建是通过AOP配置文件或注解进行的。

总体来说,Spring AOP底层实现原理就是在运行时动态生成代理对象,通过代理链实现对目标对象的方法拦截。如果是基于接口的代理,则使用JDK动态代理;如果是基于类的代理,则使用CGLIB动态代理。

🍊 Spring循环依赖

首先,什么是循环依赖?

循环依赖指两个或多个类之间出现相互依赖的情况,形成环状依赖。当两个类相互依赖时,如果它们都需要在实例化时对对方进行注入,则会导致循环依赖。

Spring中的循环依赖问题指的是当两个或多个bean相互依赖时,Spring IoC容器无法正确地完成bean的创建和注入,因为它们相互依赖在一起,形成了一个循环。

解决方案:

Spring提供了三种解决方案:

  1. 构造函数注入

使用构造函数注入是避免循环依赖问题的最佳方式。此方法可以确保依赖项在实例化时已经可用,因此不会存在循环依赖的情况。

  1. setter方法注入

如果使用了setter方法注入,则可以使用@DependsOn注解来解决循环依赖问题。该注解指定了bean的初始化顺序,以便在需要时确保bean的实例化顺序。

  1. 代理注入

Spring还提供了代理注入的解决方案。这种方法涉及将一个代理对象注入到类中来解决循环依赖问题。当访问依赖项时,代理对象会延迟解析依赖项,以便在需要时处理循环依赖。

可以使用@Lazy注解来实现延迟加载。它可以在需要时创建bean实例,并在之后重复使用。使用此注解可以确保不会出现循环依赖问题。

相关文章
|
4天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
9天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
14天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
9天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
23 4
|
10天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
34 4
|
11天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
35 4
|
11天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
50 4
|
21天前
|
开发框架 JavaScript 前端开发
HarmonyOS UI开发:掌握ArkUI(包括Java UI和JS UI)进行界面开发
【10月更文挑战第22天】随着科技发展,操作系统呈现多元化趋势。华为推出的HarmonyOS以其全场景、多设备特性备受关注。本文介绍HarmonyOS的UI开发框架ArkUI,探讨Java UI和JS UI两种开发方式。Java UI适合复杂界面开发,性能较高;JS UI适合快速开发简单界面,跨平台性好。掌握ArkUI可高效打造符合用户需求的界面。
74 8
|
16天前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
17天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
34 2