高频面试题
spring bean 的生命周期
spring bean 的生命周期主要有5个过程。
- 实例化:Spring IOC 容器根据 bean 定义或 xml 文件创建 bean 实例,可以通过构造方法或者工厂方法实例化 bean。
- 属性赋值:spring ioc 容器将配置的属性值或者依赖注入到 bean 实例中,这个阶段也称为依赖注入。
- 初始化:在此阶段,bean 可以执行一些初始化操作,比如调用自定义的初始化方法。可以通过配置 init-method 属性或者实现 InitializingBean 接口来定义初始化方法。
- 使用:bean 被应用程序使用,执行业务逻辑
- 销毁:当容器关闭时,或者根据配置销毁 Bean 实例时,容器会调用 Bean 的销毁方法。可以通过配置 destroy-method 属性或者实现 DisposableBean 接口来定义销毁方法。
Spring IOC 的实现原理
spring ioc 的实现原理依靠两个核心概念:BeanFactory 和 ApplicationContext。
- BeanFactory:BeanFactory 是 Spring 框架的核心接口,负责管理 Bean 的生命周期和依赖关系。它通过读取配置文件(如 XML 文件或注解)来实例化和配置 Bean,然后将它们放置到容器中。在需要时,通过反射机制来创建 Bean 的实例,并将依赖关系注入到 Bean 中。BeanFactory 提供了对 Bean 的管理、查找和访问等功能。
- ApplicationContext:ApplicationContext 是 BeanFactory 接口的扩展,它提供了更多的企业级功能,如国际化支持、事件发布、AOP 集成等。ApplicationContext 通常是通过 XML 配置文件、注解或者 Java 代码来创建的。在应用程序启动时,ApplicationContext 会负责读取配置信息并初始化所有的 Bean,然后在需要时按需加载和注入 Bean。
Spring IOC 的实现原理主要依赖于 Java 反射和依赖注入技术。当容器启动时,会根据配置信息实例化和配置所有的 Bean,并根据依赖关系将它们注入到相应的对象中。这样,对象之间的依赖关系由容器来管理,而不是由对象自己来创建和维护,实现了控制反转的目的。
Spring 容器启动阶段会干什么
- 加载配置:IOC 容器会加载应用程序的配置信息(这些配置信息可以是 XML 文件、Java 配置类或者其他方式定义的配置)加载配置的过程包括读取配置文件或扫描配置类,并将其转换为内部数据结构以便后续处理。
- 分析配置信息:在加载配置之后,IOC 容器会对配置信息进行解析和分析,确定每个 Bean 的定义及其依赖关系。这个过程包括解析 XML 文件或扫描注解,识别 Bean 的名称、类型、作用域、依赖关系等信息。
- 装载 BeanDefinition:在分析配置信息之后,IOC 容器会根据解析得到的信息创建对应的 BeanDefinition 对象。BeanDefinition 包含了 Bean 的各种属性和依赖关系的描述,以便后续实例化和依赖注入。
- 其他后处理:在装载 BeanDefinition 之后,IOC 容器可能会执行一些其他的后处理操作,例如注册特定类型的 BeanPostProcessor、自定义 Bean 的注册策略、处理属性占位符等。这些后处理操作可以根据需要对容器进行定制化配置和扩展。
Spring 为什么要用三级缓存
使用三级缓存的主要目的是解决循环依赖问题。当 A 依赖于 B,同时 B 也依赖于 A,如果不采取措施,就会导致无限循环的创建过程。通过三级缓存,Spring 能够在创建过程中识别出循环依赖,并在合适时机提前暴露未完全初始化的Bean,保证 Bean 的正确创建和初始化顺利完成。
Spring 事务的种类
- 编程式事务:通过 TransactionTemplate 和 PlatformTransactionManager 实现,通过编码的方式指定事务的开始、提交和回滚,允许自定义事务的边界。
- 声明式事务:在方法上标上@Transactional 注解实现。
Spring 的事务传播机制
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。Spring 的默认传播行为。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW:总是启动一个新的事务,如果当前存在事务,则将当前事务挂起。
- NOT_SUPPORTED:总是以非事务方式执行,如果当前存在事务,则将当前事务挂起。
- NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前事务不存在,则行为与 REQUIRED 一样。嵌套事务是一个子事务,它依赖于父事务。父事务失败时,会回滚子事务所做的所有操作。但子事务异常不一定会导致父事务的回滚。
Spring 声明事务失效场景
Spring AOP 是什么?它的作用和注解?
AOP 就是面向切面编程,简单来说就是把业务逻辑中相同的代码抽到一个模块中,让业务逻辑更加清爽。
AOP 切面编程涉及到的一些专业术语:
术语 | 含义 |
---|---|
目标(Target) | 被通知的对象 |
代理(Proxy) | 向目标对象应用通知之后创建的代理对象 |
连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 |
切入点(Pointcut) | 被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点) |
通知(Advice) | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情 |
切面(Aspect) | 切入点(Pointcut)+通知(Advice) |
Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 |
可以用 @Order 注解实现切面的执行顺序,值越小优先级越高。
JDK 动态代理和 CGLIB 代理的区别?
JDK 动态代理
- 基于 Interface:JDK 动态代理要求目标对象必须实现一个或多个接口。代理对象不是直接继承自目标对象,而是实现了与目标对象相同的接口。
- 使用 InvocationHandler:在调用代理对象的任何方法时,调用都会被转发到一个 InvocationHandler 实例的 invoke 方法。可以在这个 invoke 方法中定义拦截逻辑,比如方法调用前后执行的操作。
- 基于 Proxy:Proxy 利用 InvocationHandler 动态创建一个符合目标类实现的接口实例,生成目标类的代理对象。
CGLIB 代理
- 基于继承,CGLIB 通过在运行时生成目标对象的子类来创建代理对象,并在子类中覆盖非 final 的方法。因此,它不要求目标对象必须实现接口。
- 基于 ASM,ASM 是一个 Java 字节码操作和分析框架,CGLIB 可以通过 ASM 读取目标类的字节码,然后修改字节码生成新的类。它在运行时动态生成一个被代理类的子类,并在子类中覆盖父类的方法,通过方法拦截技术插入增强代码。
Spring MVC 的核心组件
- DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
- HandlerMapping:处理器映射器,根据 URL 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
- HandlerAdapter:处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;
- Handler:请求处理器,处理实际请求的处理器。
- ViewResolver:视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端
Spring MVC 工作流程
- 客户端(浏览器)发送请求, DispatcherServlet拦截请求。
- DispatcherServlet 根据请求信息调用 HandlerMapping 。HandlerMapping 根据 URL 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
- DispatcherServlet 调用 HandlerAdapter适配器执行 Handler 。
- Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
- ViewResolver 会根据逻辑 View 查找实际的 View。
- DispaterServlet 把返回的 Model 传给 View(视图渲染)。
- 把 View 返回给请求者(浏览器)
Spring Boot 自动装配的原理
- 在Spring Boot项目中有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 其中@EnableAutoConfiguration是实现自动化配置的核心注解。
- 该注解通过@Import注解导入AutoConfigurationImportSelector,这个类实现了一个导入器接口ImportSelector。在该接口中存在一个方法selectImports,
- 该方法的返回值是一个数组,数组中存储的就是要被导入到spring容器中的类的全类名。在AutoConfigurationImportSelector类中重写了这个方法,
- 该方法内部就是读取了项目的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。
在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。
Spring Boot 是什么?它的好处?
Spring Boot 是一个开源的、用于简化 Spring 应用初始化和开发的框架。提供了一套默认配置,约定优于配置,来帮助我们快速搭建 Spring 项目骨架,提高我们的开发效率。
好处:
- 通过 Spring initializr 勾选我们想要的依赖,快速创建项目骨架
- Spring Boot 内嵌 Tomcat,Jetty,Undertow 容器,无需在服务器外挂 war 包,直接运行 jar 包就可启动项目
- 通过 application.yml/properties 集中、方便地配置多个框架和库。引入 spring-boot-starter-web,Spring Boot 会自动配置 tomcat 和 spring mvc
- Spring Boot 提供了一系列 starter,可以快速集成常用框架,还允许我们自定 starter
- Spring Boot 提供了一系列的 Actuator,帮助我们监控和管理应用,如健康检查、审计、统计。
Spring Boot 如何自定 Starter?
第一步,创建一个新的 Maven 项目,例如命名为 my-spring-boot-starter。在 pom.xml 文件中添加必要的依赖和配置:
1 | <properties> |
第二步,在 src/main/java 下创建一个自动配置类,比如 MyServiceAutoConfiguration.java:(通常是 autoconfigure 包下)。
1 |
|
第三步,创建一个配置属性类 MyStarterProperties.java:
1 |
|
第四步,创建一个简单的服务类 MyService.java:
1 | public class MyService { |
第五步,配置 spring.factories,在 src/main/resources/META-INF 目录下创建 spring.factories 文件,并添加:
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
第六步,使用 Maven 打包这个项目:
1 | mvn clean install |
MyBatis 使用过程?生命周期?
使用过程
- 1)创建 SqlSessionFactory
可以从配置或者直接编码来创建 SqlSessionFactory
1 | String resource = "org/mybatis/example/mybatis-config.xml"; |
- 2)通过 SqlSessionFactory 创建 SqlSession
SqlSession(会话)可以理解为程序和数据库之间的桥梁
1 | SqlSession session = sqlSessionFactory.openSession(); |
- 3)通过 sqlsession 执行数据库操作
可以通过 SqlSession 实例来直接执行已映射的 SQL 语句:
1 | Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101); |
更常用的方式是先获取 Mapper(映射),然后再执行 SQL 语句:
1 | BlogMapper mapper = session.getMapper(BlogMapper.class); |
- 4)调用 session.commit()提交事务
如果是更新、删除语句,我们还需要提交一下事务。
- 5)调用 session.close()关闭会话
生命周期
- SqlSessionFactoryBuilder
一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的生命周期只存在于方法的内部。
- SqlSessionFactory
SqlSessionFactory 是用来创建 SqlSession 的,相当于一个数据库连接池,每次创建 SqlSessionFactory 都会使用数据库资源,多次创建和销毁是对资源的浪费。所以 SqlSessionFactory 是应用级的生命周期,而且应该是单例的。
- SqlSession
SqlSession 相当于 JDBC 中的 Connection,SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的生命周期是一次请求或一个方法。
- Mapper
映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的,它的生命周期在 sqlsession 事务方法之内,一般会控制在方法级。
在 mapper 中如何传递多个参数?
MyBatis 是如何进行分页的?分页插件的原理是什么?
如何分页
- MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页
- 在 sql 内直接书写带有物理分页的参数来完成物理分页功能
- 也可以使用分页插件来完成物理分页
分页插件的原理
MyBatis 仅可以编写针对 ParameterHandler、 ResultSetHandler、 StatementHandler、 Executor 这 4 种接口的插件,MyBatis 使用 JDK 的动态代理对执行的 SQL 进行拦截,然后重写 SQL,根据方言添加对应的物理分页语句和参数。
RBAC 角色权限是怎么控制的?
- 角色定义:系统定一个一组角色,一个角色代表一组权限的集合。例如系统有“管理员”、“普通用户”、“审计员”等角色
- 权限分配:为每个角色赋予特定的权限,这些权限可以是访问特定的资源或者执行特定的操作。
- 用户分配角色:用角色分配给用户,一个用户可以拥有一个或多个角色。
- 权限校验:系统根据用户的角色进行权限校验,确定用户能够进行某些操作不能进行某些操作。
前端是如何发送请求的?
我的前端是通过 axios 发送请求的,我自定义了一个 axios 实例叫 myAxios,设置了基础 URL 便于请求后端,此外,我还设置了请求拦截器和响应拦截器,最后导出 axios 实例。其中两个拦截器用以在请求前打上日志和拦截后端的响应码,通过判断响应码是否等于 40100 验证用户是否登录,没有的话自动跳转登录页面。
从浏览器地址栏输入 url 到显示主页的过程?
- DNS 解析:浏览器发起一个 DNS 请求到 DNS 服务器,将域名解析为服务器的 IP 地址。
- TCP 连接:浏览器通过解析得到的 IP 地址与服务器建立 TCP 连接(通常是通过 443 端口进行 SSL 加密的 HTTPS 连接)。这一步涉及到 TCP 的三次握手过程,确保双方都准备好进行数据传输。
- 发送 HTTP 请求:浏览器构建 HTTP 请求消息,包括请求行(如 GET / HTTP/1.1)、请求头(包含用户代理、接受的内容类型等信息)和请求体(如果有);将请求发送到服务器。
- 服务器处理请求:服务器接收到 HTTP 请求后,根据请求的资源路径,经过后端处理(可能包括数据库查询等),生成 HTTP 响应消息;响应消息包括状态行(如 HTTP/1.1 200 OK)、响应头(内容类型、缓存控制等信息)和响应体(请求的资源内容)。
- 浏览器接收 HTTP 响应:浏览器接收到服务器返回的 HTTP 响应数据,开始解析响应体中的 HTML 内容;然后构建 DOM 树、解析 CSS 和 JavaScript 文件等,最终渲染页面。
- 断开连接:TCP 四次挥手,连接结束。
说说 DNS 解析过程
- 假设我们在浏览器地址栏里键入了 www.baidu.com 浏览器会首先检查自己的缓存中是否有这个域名对应的 IP 地址,如果有,直接返回;如果没有,进入下一步。
- 检查本地 DNS 缓存是否有该域名的记录。如果没有,向根域名服务器发送请求,根域名服务器将请求指向更具体的服务,如 com 顶级域名服务器。
- 顶级域名服务器再将请求指向权限域名服务器,权限域名服务器会找到对应的 DNS 服务器,并将 IP 地址返回给浏览器。
- 浏览器使用得到 IP 地址发送 HTTP 请求到目标服务器,然后该服务器返回对应的网页内容。
TCP 和 UDP 的区别?
说说 TCP 和 UDP 的应用场景?
- TCP: 适用于那些对数据准确性要求高于数据传输速度的场合。例如:网页浏览、电子邮件、文件传输(FTP)、远程控制、数据库链接。
- UDP: 适用于对速度要求高、可以容忍一定数据丢失的场合。例如:QQ 聊天、在线视频、网络语音电话、广播通信。容忍一定的数据丢失。
TCP 的三次握手和四次挥手
TCP(传输控制协议)的三次握手机制是一种用于在两个 TCP 主机之间建立一个可靠的连接的过程。这个机制确保了两端的通信是同步的,并且在数据传输开始前,双方都准备好了进行通信。
https://segmentfault.com/a/1190000022410446
三次握手:
- 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务端的确认;
- 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态;
- 三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入ESTABLISHED 状态,完成 TCP 三次握手。
当建立了 3 次握手之后,客户端和服务端就可以传输数据啦!
四次挥手:
- 第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。
- 第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
- 第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
- 第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
只要四次挥手没有结束,客户端和服务端就可以继续传输数据!
TCP 握手为什么是三次?不是两次或者四次?
为什么 TCP 握手不是两次?
- 防止服务器一直等
- 防止客户端已经失效的连接请求又传到了服务器
为什么 TCP 握手不是四次?
三次握手连接的连接已经足够可靠,没有再多握一次手。
HTTP 加密过程
- 建立连接:
- 客户端向服务器发起 HTTPS 请求,请求建立安全连接。
- 服务器在接收到请求后,会发送一个包含公钥的数字证书给客户端。
- 验证证书:
- 客户端接收到服务器发送的数字证书后,会验证证书的合法性。
- 客户端检查证书的颁发机构是否受信任、证书是否过期、域名是否与请求的域名匹配等。
- 如果验证通过,客户端生成一个随机的对称密钥,并使用服务器的公钥加密该密钥,然后将加密后的密钥发送给服务器。
- 建立加密通道:
- 服务器收到客户端发送的加密后的对称密钥后,使用自己的私钥解密,获取对称密钥。
- 客户端和服务器现在都拥有了相同的对称密钥,它们将使用这个密钥来加密和解密后续的通信内容。
- 安全传输数据:
- 客户端和服务器之间的通信内容现在都通过对称密钥进行加密和解密。
- 客户端和服务器可以安全地传输敏感数据,如用户登录信息、个人数据等,而不必担心数据被窃取或篡改。
- 维持连接:
- 一旦安全连接建立起来,客户端和服务器可以保持长时间的通信,直到一方关闭连接或超时。
HTTP 加密过程为什么要一次非对称和多次对称
这边讲一下多次对称加密和一次非对称加密的优缺点和使用场景就可以了,然后我再介绍了下常见的对称加密算法和非对称加密的算法,以及算法优化的一个历史,这边说了一下有些历史算法是不安全的,已经被破解过。
Synchronized和ReentrantLock区别
- 类型不同:synchronized是一个关键字,而 Lock 属于一个接口,其实现类主要有 ReentrantLock、ReentrantReadWriteLock。
- 使用方式不同:synchronized 可以直接在方法上加锁,也可以在代码块上加锁(无需手动释放锁,锁会自动释放),而 ReentrantLock 必须手动声明来加锁和释放锁。
- 如果需要更细粒度的控制(如可中断的锁操作、尝试非阻塞获取锁、超时获取锁或者使用公平锁等),可以使用 Lock。
- ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制。
- ReentrantLock 可以指定是公平锁还是非公平锁。
- ReentrantReadWriteLock 读写锁,读锁是共享锁,写锁是独占锁,读锁可以同时被多个线程持有,写锁只能被一个线程持有。这种锁的设计可以提高性能,特别是在读操作的数量远远超过写操作的情况下。
Lock 还提供了newCondition()方法来创建等待通知条件Conditionopen in new window,比 synchronized 与 wait()、 notify()/notifyAll()方法的组合更强大。
HashMap 的 put 流程?
- 通过 hash 方法计算 key 的哈希值
- 数组进行第一次扩容
- 根据哈希值计算 key 在数组中的下标,如果对应下标没有存数据,则直接插入,如果有数据判断 key 是否相同,是则覆盖;否则判断是否为树节点,是则向树中插入节点,否则向链表中插入数据
- 在链表中插入节点时,如果链表长度大于 8,则将链表转换为红黑树
- 所有元素处理完,还需判断是否超过 threshold,超过就扩容
HashMap 的查找
- 使用扰动函数,获取新的哈希值
- 计算数组下标,获取节点
- 当前节点和 key 匹配,直接返回
- 否则,当前节点是否为树节点,查找红黑树
- 否则,遍历链表查找
CompletableFuture
1 | package com.hjj.homiematching.service; |
Java 创建线程池的方式
- ExecutorService executorService = Executors.newFixedThreadPool(5)(创建固定大小的线程池)
- ExecutorService cachedThreadPool = Executors.newCachedThreadPool();(创建一个根据需要创建新线程的线程池)
- ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();(单线程化的 Executor,保证顺序的执行任务)
- ExecutorService executorService = new ThreadPoolExecutor();
Java 实现多线程的几种方式?
- 继承 Thread 类
- 实现 Runnable 接口
- 使用匿名内部类重写 run 方法
- 实现 Callable 接口
- 基于线程池实现
synchronized 和 Lock 的区别
- synchronized 是一个关键字,Lock 是接口
- synchronized 加锁和释放锁都由 jvm 完成,发生异常时会自动释放锁。Lock 加锁释放锁是手动的由开发者完成,发生异常时不会自动释放锁。
- synchronized 不能响应中断会一直等锁,Lock 等待锁的过程中可通过 interrupt 中断等待。
- synchronized 不知道有没有获取锁,Lock 可通过 tryLock 方法知道获没获取锁
- synchronized 是不公平的,Lock 是公平的
- 锁竞争激烈时,后者性能更高