之前写的博客太杂,最近想把后端框架的知识点再系统的过一遍,主要是Spring Boot和Mybatis相关,带着自己的理解使用简短的话把一些问题总结一下,尤其是开发中和面试中的高频问题,基础知识点可以参考之前写java后端专栏,这篇不再赘述。
目录
Spring(Boot)
yml等配置文件的配置优先级
命令行参数 (程序实参)> 系统属性参数(VM选项) > properties参数 > yml参数 > yaml参数
循环依赖相关
详细介绍–>Spring是如何解决循环依赖问题的?
1.什么是Spring中的循环引用?
2.具体解决流程是怎样?
3.构造方法出现了循环依赖怎么解决?
1.循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
spring框架依据三级缓存已经解决了大部分的循环依赖(也有无法解决的场景,见详细原文介绍)
①一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的
bean对象
②二级缓存:缓存早期的bean对象(生命周期还没走完)
③三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的
2.具体解决流程:
第一,先实例A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories
第二,A在初始化的时候需要B对象,这个走B的创建的逻辑
第三,B实例化完成,也会创建ObjectFactory对象存入三级缓存singletonFactories
第四,B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键
第五,B通过从通过二级缓存earlySingletonObjects 获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects
第六,回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects
第七,二级缓存中的临时对象A清除
3.构造方法出现了循环依赖
由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建;还可以改用字段注入@Autowired或者setter方法注入的方式。另外大部分情况都是代码问题,第一选择还是代码重构解决循环依赖
过滤器与拦截器的区别
详细介绍–>SpringBootWeb案例——Tlias智能学习辅助系统(3)——登录校验
区别主要是三点:
- 首先是归属和定位不同:
- 过滤器(Filter) 属于 Servlet 规范,也就是说它是 Java EE 标准的一部分,跟 Spring 没关系,属于“全局级别”的组件;
- 拦截器(Interceptor) 是 Spring MVC 提供的,属于 Spring 的生态,专门用于拦截 Spring Controller 的请求。
- 然后是执行流程上的区别:在请求生命周期中,过滤器优先于拦截器执行。一般顺序是:
请求进来 -> 过滤器链 -> DispatcherServlet -> 拦截器链 -> Controller
- 而返回响应时这个流程会倒过来走。它们各自也都支持多个实例的链式调用:
- 过滤器的执行顺序在 web.xml 或配置类中定义;
- 拦截器的顺序在 Spring 的配置里定义,比如 WebMvcConfigurer 的 addInterceptors 方法。
- 再说说拦截范围和使用场景:
- 过滤器 拦截的是所有的请求:静态资源、Servlet、JSP、甚至非 Spring 管理的资源都能拦。
- 拦截器 只能拦截被 Spring MVC 处理的请求,比如进入 Controller 的那些。
过滤器:
- Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
-
- 使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器处理完毕之后,才可以访问对应资源。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
拦截器
- 是一种动态拦截方法调用的机制,类似于过滤器。拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
整个流程如上图所示。
当浏览器来访问部署在web服务器当中的web应用时,定义的过滤器会拦截到这次请求。拦截后,先执行放行前逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先行到DispatcherServlet,再将请求转给Controller。
在controller当中的方法执行完毕之后,再回过来执行postHandle() 这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
如何解决跨域问题?
详细介绍–>什么是跨域问题,SpringBoot如何解决?
大致分为三种方式:
- JSONP (JSON with Padding) 一种比较老的方法,目前已经慢慢被替代。它通过动态插入一个<script>标签到HTML中来获取跨域数据。
- CORS(跨源资源共享) 一种W3C规范,它允许在服务器端设置哪些源可以访问服务器的资源。这是通过服务器发送特殊的HTTP头来实现的。
- 代理服务器反向代理(推荐) 比如通过nginx,不需要在前后端做任何配置,只需要在nginx进行配置,此时浏览器请求的是同一个域,但浏览器检测不到你进行了跨域,因为我们是在nginx这一层转到我们的后端,但是后端跟后端之间是不存在跨域的,这便解决了前端的跨域异常。
Spring相关
谈谈你对Spring的理解?有哪些缺点?
在我看来,Spring不仅仅是一个框架,更是一个完整的生态系统,旨在简化企业级应用的开发。具体来说,Spring Framework 的核心在于其控制反转(IoC)和依赖注入(DI)的机制,这使得组件之间的耦合度降低,代码更加模块化和易于测试。此外,Spring 的面向切面编程(AOP)功能允许我们将诸如日志记录、事务管理等横切关注点与业务逻辑分离,提升了代码的可维护性和复用性。
除了核心功能,Spring 还提供了丰富的模块支持,如数据访问、事务管理和Web开发等,这些模块极大地简化了与数据库交互、处理事务以及构建Web应用的流程。例如,使用**Spring Data(问题1)**可以方便地进行数据库操作,而Spring MVC则提供了强大的Web开发框架,帮助快速搭建健壮的Web应用。
然而,Spring 也存在一些不足。首先,学习曲线较陡,对于初学者来说,全面掌握Spring的各种概念和配置可能需要较多时间。其次,尽管Spring Boot在一定程度上简化了配置,但在大型项目中,复杂的配置仍然可能导致维护困难。此外,Spring的运行时性能相对较低(问题2),特别是在高并发场景下,频繁的对象创建和依赖注入可能会带来性能瓶颈。还有,过度依赖Spring的各种功能和模块,可能会降低项目的可移植性和灵活性,增加对框架的依赖度。最后,尽管Spring拥有庞大的社区和丰富的文档,但在某些特定问题上,资源和支持可能仍然不足,需要开发者具备较强的自我解决问题的能力。
总体而言,Spring凭借其强大的功能和灵活性,依然是开发企业级应用的首选框架,但在使用过程中需要权衡其复杂性和性能,合理设计架构以充分发挥其优势。
为什么市面上大部分用的是mybatis
问题 1、"例如,使用Spring Data可以方便地进行数据库操作"那为什么市面上大部分用的是mybatis
这是一个很好的问题,反映出在实际项目中选择技术栈时需要权衡多方面因素。虽然Spring Data确实提供了简化数据库操作的优势,但许多项目仍然选择使用MyBatis,原因主要包括以下几点:
- 对SQL的控制力更强 : MyBatis允许开发者手写SQL语句,这在处理复杂查询、性能优化或需要精确控制数据库操作时非常有用。相比之下,Spring Data(尤其是Spring Data JPA)更多依赖于自动生成的SQL,对于复杂的业务逻辑可能不够灵活。
- 更适合复杂的数据库结构 :在一些项目中,数据库设计可能比较复杂或者使用了大量的存储过程和自定义函数。MyBatis能够更容易地映射这些复杂的数据库结构,而Spring Data在这方面可能需要更多的配置和调整。
- 性能优化 : 因为MyBatis允许手写和优化SQL,开发者可以针对具体的数据库和查询需求进行性能调优,这在高性能要求的系统中尤为重要。而Spring Data的自动化特性有时会导致生成的SQL不够高效,需要额外的优化。
- 现有团队经验和项目遗留 : 很多团队在选择技术栈时会考虑现有成员的经验。如果团队成员对MyBatis更熟悉,或者项目之前已经使用了MyBatis,继续使用MyBatis会减少学习成本和迁移风险。
- 灵活性和透明度 : MyBatis提供了更高的透明度,开发者可以清晰地看到每一条SQL语句是如何执行的。这对于调试和维护来说非常有帮助。而Spring Data的抽象层次较高,有时在排查问题时不够直观。
- 社区和生态支持 :尽管Spring Data有广泛的社区支持,但MyBatis也有其专属的生态和丰富的插件,能够满足特定需求。例如,MyBatis Generator可以自动生成映射文件,提升开发效率。
总结来说,选择MyBatis还是Spring Data,主要取决于项目的具体需求、团队的技术背景以及对数据库操作的控制要求。在需要高度定制化和优化的场景下,MyBatis往往是更好的选择,而在追求快速开发和简化配置的项目中,Spring Data则更具优势。
怎么解决Spring的运行时性能相对较低
问题 2、“此外,Spring的运行时性能相对较低,特别是在高并发场景下”那现在企业如果有高并发需求一般怎么解决Spring的运行时性能相对较低的问题呢
企业通常会通过多种策略来优化和提升Spring应用的性能,以满足高并发需求。以下是几种常见的解决方案:
-
1.优化Spring配置和Bean管理
- 使用单例Bean:尽量使用单例模式管理Bean,减少对象的频繁创建和销毁,降低内存开销。
- 懒加载:对于不常用的Bean,可以配置为懒加载,避免启动时加载所有Bean,提升启动速度。
- 减少不必要的依赖:精简应用的依赖,避免加载不必要的组件,减少资源消耗。
-
2.采用异步处理和非阻塞编程
- 异步方法:利用Spring的@Async注解,将耗时操作异步化,避免阻塞主线程,提高响应速度。
- Spring WebFlux:在需要极高并发的情况下,可以采用Spring WebFlux框架,基于响应式编程模型,支持非阻塞IO操作,提升系统的吞吐量和响应性能。
-
3.使用高效的缓存机制
- Spring Cache:集成缓存解决方案,如Ehcache、Caffeine等,缓存频繁访问的数据,减少数据库查询次数。
- 分布式缓存:使用Redis、Memcached等分布式缓存系统,提升缓存的可扩展性和高可用性,适应大规模并发访问。
-
4.数据库优化
- 优化SQL查询:确保SQL语句高效,使用索引、避免全表扫描,提升数据库访问性能。
- 连接池:配置高效的数据库连接池(如HikariCP),优化连接管理,减少连接建立和释放的开销。
- 读写分离:通过主从数据库架构,将读操作和写操作分离,提升数据库的并发处理能力。
-
5.水平扩展和微服务架构
- 负载均衡:使用Nginx、HAProxy等负载均衡器,将请求分发到多个实例,分散压力,提升系统的整体处理能力。
- 微服务:将单体应用拆分为多个微服务,每个服务独立部署和扩展,针对性地优化各个服务的性能,提升整体系统的并发处理能力。
-
6.使用消息队列
- 异步消息处理:通过Kafka、RabbitMQ等消息队列,将高并发请求进行异步处理,平滑峰值流量,避免系统过载。
- 解耦系统组件:消息队列可以解耦不同系统组件,提升系统的弹性和可扩展性。
-
7.性能监控与调优
- 监控工具:使用Prometheus、Grafana、New Relic等监控工具,实时监控系统性能,及时发现和解决性能瓶颈。
- 性能分析:通过JProfiler、YourKit等性能分析工具,深入分析应用的性能问题,进行有针对性的优化。
-
8.前端优化
- 减少请求次数:通过合并资源、使用CDN等方式,减少服务器的请求压力。
- 优化静态资源:压缩和缓存静态资源,降低服务器负载,提高页面加载速度。
-
9.硬件和基础设施优化
- 高性能服务器:使用性能更强的服务器硬件,提升系统的处理能力。
- 云服务弹性:利用云服务的弹性伸缩特性,根据负载动态调整资源,确保系统在高并发下依然稳定运行。
总结:通过以上多种策略的综合应用,企业能够有效提升Spring应用在高并发场景下的运行性能。关键在于根据具体的业务需求和系统瓶颈,选择合适的优化手段,确保系统在高并发下依然能够稳定、高效地运行。
事件监听相关
Spring事件监听的核心机制是什么?好处?举例
Spring事件监听的核心机制围绕观察者模式展开:
观察者模式 : 它允许一个对象(称为主题或被观察者)维护一组依赖于它的对象(称为观察者),并在主题状态发生变化时通知观察者。
它包含三个核心:
- 事件 (Event) : 事件是观察者模式中的主题状态变化的具体表示,它封装了事件发生时的信息。在Spring中, 通常是继承自
ApplicationEvent
的类,代表一个特定的动作或状态变化。比如用户注册成功的事件、订单生成的事件等,用于传递数据或上下文信息。 - 事件发布者 (Publisher) : 在Spring中,事件发布者充当主题的角色,负责触发并发布事件。它通常实现了
ApplicationEventPublisher
接口或使用注解@Autowired
注入ApplicationEventPublisher
来获得事件发布功能。 只需要通过调用publishEvent()
方法来发布事件。 - 事件监听器: 事件监听器充当观察者的角色,负责监听并响应事件的发生。它实现了
ApplicationListener
接口,通过onApplicationEvent()
方法来处理事件。
总之,Spring事件监听机制的核心机制是观察者模式,通过事件、事件发布者和事件监听器的协作,实现了松耦合的组件通信,使得应用程序更加灵活和可维护。
Spring事件监听的好处:
- 松耦合:发布者和监听器之间没有直接依赖关系,只通过事件进行交互,增强了模块的可维护性。
- 灵活性:可以轻松添加或移除监听器,或者针对不同事件设计不同的监听器。
- 异步处理:Spring 支持将事件监听器配置为异步执行 (使用 @Async 注解) ,进一步提高系统性能。
举个简单的例子:
- 创建一个事件类,这个类
UserRegisteredEvent
是一个自定义的事件,用于表示用户注册成功。
import org.springframework.context.ApplicationEvent;
public class UserRegisteredEvent extends ApplicationEvent {
private String username;
public UserRegisteredEvent(Object source, String username) {
super(source);
this.username = username;
}
public String getUsername() {
return username;
}
}
- 创建事件发布者(Publisher),在
UserService
中,当用户成功注册后,UserRegisteredEvent
事件被发布。
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
public class UserService {
private final ApplicationEventPublisher publisher;
public UserService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void registerUser(String username) {
// 注册逻辑
System.out.println("User " + username + " registered successfully.");
// 发布用户注册事件
UserRegisteredEvent event = new UserRegisteredEvent(this, username);
publisher.publishEvent(event);
}
}
- 创建事件监听器(Listener),
WelcomeEmailListener
类是监听器,当监听到UserRegisteredEvent
事件时,自动触发handleUserRegisteredEvent
方法,执行发送欢迎邮件的逻辑。
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class WelcomeEmailListener {
@EventListener // @EventListener 注解的方法,默认会监听其方法参数类型对应的事件。
public void handleUserRegisteredEvent(UserRegisteredEvent event) {
// 处理逻辑,例如发送欢迎邮件
System.out.println("Sending welcome email to " + event.getUsername());
}
}
核心流程:
- 用户注册成功时,
UserService
发布了UserRegisteredEvent
。 - Spring 的事件发布机制会将这个事件通知给所有监听该事件的监听器。
WelcomeEmailListener
作为监听器,自动捕获事件并执行相应的处理逻辑。
消息队列和Spring事件监听机制对比
消息队列和Spring事件监听机制在事件驱动的应用中都起到了解耦的作用,但它们各自适用于不同的场景,有着不同的优缺点。接下来我会从以下几个方面来对比为什么在某些情况下,Spring事件监听机制更适合,或者说不一定要用消息队列。
1.应用场景
- Spring事件监听机制:
- Spring事件监听通常用于单个应用程序内的组件通信。它主要用于在应用程序内部发布和监听事件,实现组件之间的解耦。它的典型场景是同步操作或是应用内部的轻量级异步处理。
- 例如,一个用户注册成功事件可以触发发送欢迎邮件的逻辑,这种情况可以用Spring事件监听机制直接实现,因为事件发布和处理都是在同一个JVM内进行的,不涉及跨系统的通信。
- 消息队列:
- 消息队列适用于跨服务、跨应用的通信,特别是分布式系统。它可以用于跨进程、跨网络的消息传递,确保消息在不同系统或模块之间传递,通常用于异步处理。
- 比如,用户注册在服务A中完成,而欢迎邮件的发送逻辑在服务B中,这时可以通过消息队列来异步通知服务B执行后续的操作。
2.技术复杂度和运维成本
- Spring事件监听机制:
- Spring事件监听机制相对简单,属于Spring框架的内置功能,几乎没有额外的配置成本。它不需要维护额外的外部系统,也不需要处理消息的持久化、消息丢失等问题。
- 适合应用内部的小型解耦需求,运维成本低,不需要额外的基础设施如消息代理(RabbitMQ、Kafka等)。
- 消息队列:
- 消息队列是一种更为复杂的系统,通常需要一个独立的消息中间件(如RabbitMQ、Kafka、ActiveMQ等)。需要处理消息的持久化、重试机制、消息消费确认、队列管理等运维问题。
- 消息队列的优势在于它可以处理大规模、跨系统的异步消息传递,但它的技术复杂度更高,需要额外的配置和维护成本。
3.同步 vs 异步
- Spring事件监听机制:
- 默认情况下,Spring事件是同步的。即事件发布者发布事件时,监听器会在同一个线程内处理事件,发布者需要等监听器处理完成后再继续执行后续逻辑。虽然也可以通过配置为异步监听,但这需要额外配置
@Async
,并且依赖线程池的管理。 - 适合一些需要立即处理的场景,或者不涉及复杂的并发需求的异步处理场景。
- 默认情况下,Spring事件是同步的。即事件发布者发布事件时,监听器会在同一个线程内处理事件,发布者需要等监听器处理完成后再继续执行后续逻辑。虽然也可以通过配置为异步监听,但这需要额外配置
- 消息队列:
- 消息队列的核心是异步处理,消息发布者和消费者之间是完全解耦的。消息发布者只需将消息发送到队列,消费者异步地从队列中取出消息进行处理,发布者不需要等待消费者处理完成。
- 适用于跨服务的异步处理、需要处理大量并发消息的场景。
4.消息可靠性和持久化
- Spring事件监听机制:
- Spring事件监听通常依赖于内存中的对象,没有消息持久化机制。如果应用程序崩溃或重启,未处理的事件将会丢失。因此,它不适合需要强可靠性保障的场景。
- 适用于那些对可靠性要求不高,或可以容忍部分事件丢失的场景。
- 消息队列:
- 消息队列有持久化、重试机制,可以保证消息的可靠传递,确保消息不会丢失。比如Kafka、RabbitMQ等都支持消息的持久化存储和重发机制,保证即使在系统崩溃或网络异常的情况下,消息仍然可以被最终消费。
- 适用于高可靠性场景,如订单系统、支付系统等,不能容忍消息丢失。
5.分布式 vs 单体应用
- Spring事件监听机制:
- 适合在单体应用中使用,因为它是在同一个JVM中工作的。如果是分布式架构,它就不太适用了,因为不同服务之间不能直接通过Spring事件机制进行通信。
- 消息队列:
- 在分布式系统中,消息队列是非常重要的解耦工具,能实现跨服务的异步通信,适合分布式架构下的场景。不同的服务或模块通过消息队列进行消息传递,保证系统的扩展性和解耦性。
总结:
- Spring事件监听机制:适用于单体应用或小型系统中的同步事件驱动或轻量级的异步处理。它简单易用,几乎没有额外的技术负担和运维成本。
- 消息队列:适用于分布式系统中的异步处理、高并发、高可靠性要求的场景。它可以跨服务、跨系统,提供可靠的消息传递,但其复杂性和运维成本也更高。
什么时候用Spring事件监听机制:
- 如果你的需求是在单个应用内发生的,并且不涉及跨服务的通信、消息的持久化或复杂的异步处理,Spring事件监听机制完全可以满足需求,而且简单直接。
什么时候用消息队列:
- 如果你是分布式系统,或者需要跨多个微服务传递消息,并且消息的可靠性和异步处理是你系统的核心需求,那么消息队列会更合适。
换句话说,消息队列是一个更为强大且复杂的解决方案,但在小规模场景下不一定是最优选择,Spring事件监听机制则提供了一个轻量级的替代方案。
事件监听失效bug
如果事件的发布是通过Bean的@PostConstruct,在init方法中发布事件时,且此时是监听器是通过注解的方式去声明的就会失效。
但是此时如果将监听器改为由接口实现,监听却又能够生效了。
我们先梳理一下整个流程。在我们去启动spring boot的时候,也就是当我们去通过spring boot的启动类去运行springApplication.run方法,那么在这个run方法里面它就会去创建spring容器,从而创建我们的bean,那么进而就会去创建我们的这个发布事件的bean,还有监听器的bean。那么当他创建到发布事件这个bean的时候呢,他发现你有一个方法上面有@PostConstruct,那么他在创建这个的bean的时候呢他就会去调用你的这个init方法,这就是整这个流程。
然后这个事件监听器的接口实现的方式,还有事件监听器注解实现的方式,它分别是在哪里去解析的呢。因为解析事件接口在spring的启动流程里是发生在创建发表事件的Bean之前的,所以调用init方法的时候是能够找到监听器的,但是通过注解的方式去实现的监听器是在创建Bean之后才去解析的,此时在初始化方法里去发布事件就导致失效了。
BeanFactory和FactoryBean有什么区别
BeanFactory 和 FactoryBean 的区别可以总结为以下几个关键点:
1.BeanFactory 是 Spring 核心容器接口
- BeanFactory 是 Spring 框架的核心接口之一,用于管理和获取 Bean 对象,也被称为 Spring 的 IoC 容器。
- 它使用了简单工厂模式,通过
getBean()
方法来获取和管理 Bean 实例。 - 它主要负责 Bean 的生命周期管理,比如创建、配置、组装和销毁。
- BeanFactory 本身并不负责创建 Bean 的具体逻辑,它只是根据配置信息来管理和提供 Bean。
2.FactoryBean 是一个特殊的 Bean
- FactoryBean 是 Spring 中的一种特殊的 Bean,它是一个接口,专门用于创建复杂 Bean 或进行 Bean 创建的自定义逻辑。
- FactoryBean 是工厂模式的一种表现形式,它的作用是自定义 Bean 的创建逻辑。
getObject()
方法用于返回实际的 Bean 实例,即最终创建的对象。getObjectType()
方法用于返回创建的 Bean 的类型。
- FactoryBean 并不直接作为最终的 Bean 注入,而是负责创建或代理其他 Bean。举例来说,MyBatis 集成 Spring 时使用的
SqlSessionFactoryBean
就是一个典型的FactoryBean
,通过其getObject()
方法创建SqlSessionFactory
,并将其注入到 IoC 容器中。 - 使用 FactoryBean 可以在创建 Bean 之前进行额外的逻辑处理,例如对象的缓存、代理等功能。
3.BeanFactory 和 FactoryBean 之间的关系
- BeanFactory 和 FactoryBean 并没有直接的关联。可以说,BeanFactory 管理了 FactoryBean,而 FactoryBean 则作为一种特殊的 Bean 存在。
- 可以把BeanFactory比作"后台的管理者",负责管理Bean的生命周期,而FactoryBean则是"前台的定制工厂",负责生成特殊类型的Bean对象。
- BeanFactory 可以管理 FactoryBean 实例,并通过
getBean()
方法调用 FactoryBean 的getObject()
,来获取最终的 Bean 实例。 - 如