🧸本篇博客重在讲解SpringBoot、Spring、SpringMVC等相关面试题,将会实时更新,欢迎大家添加作者文末联系方式交流
📜JAVA面试题专栏:JAVA崭新面试题——2024版_dream_ready的博客-CSDN博客
📜作者首页: dream_ready-CSDN博客
📜有任何问题都可以评论留言,作者将会实时回复
📜未来将会更新数据结构面试题以及Spring和MySQL等等的面试题,将会涵盖JAVA后端所有技术框架,敬请期待!!!
注:本篇博客精简高效,问题对应的答案是直接可以口述给面试官
有的问题答案较长,可以仅记住前半部分。我写的时候也为这样的答案量身定做了一番,即便直接回答前半部分也不突兀,也能达到不错的水平
目录
1、什么是框架?
2、什么是Spring?
3、Spring有哪些模块
4、什么是SpringBoot?SpringBoot和Spring的关系
5、什么是SpringMVC?SpringMVC和三层架构的关系
6、Spring,SpringBoot和SpringMVC的关系以及区别
7、什么是maven?Spring和maven的区别?
8、什么是IOC?为什么要使用IOC?
9、什么是DI,DI和IOC有什么关系?
10、DI依赖注入的方式和区别?Bean的注入方式和区别?
11、@Autowired和@Resource有什么区别?
12、什么是AOP?它有哪些使用场景?
13、AOP是如何组成的?它有几种增强方法?
14、AOP底层是如何实现的?
15、JDK动态代理和CGLib有什么区别?
16、什么是Bean
17、Bean的生命周期?
18、BeanFactory和FactoryBean有什么区别?
19、Bean是线程安全的吗?实际工作中怎么保证其线程安全?
20、Spring 容器启动阶段会干什么?Spring启动阶段会干什么?
21、Spring Boot 中的常用注解有哪些?
22、如何实现自定义注解?实际工作中哪里会用到自定义注解?
23、什么是拦截器和过滤器以及二者的区别
什么是拦截器?
什么是过滤器?
拦截器和过滤器有什么区别?
24、当客户端发送请求到后端时的执行顺序
25、如何实现拦截器?
26、如何实现过滤器?
27、SpringBoot如何实现跨域?跨域问题的本质是什么?
28、Spring中使用了哪些设计模式?
29、SpringBoot如何实现缓存预热?
30、Spring如何解决循环依赖问题?什么是三级缓存?为什么一定要用三级缓存?
31、SpringBoot的启动流程是怎么样的?
32、Spring的自动装配(自动配置)
1、什么是框架?
什么是框架基本是不会被面试官提问到的,但很多人对什么是框架其实都处在一个模糊的概念上,所以我第一个书写的面试题就是 什么是框架
框架是在软件开发中用来提供一种基础架构和支持的软件库或工具集合
框架是一种预先设计好的软件架构,它定义了应用程序的基本结构,并提供了一套约定和接口,使得开发者可以在此基础上添加自己的业务逻辑。框架通常包含了一系列工具和库,帮助开发者更高效地开发应用程序。
以上都较为官方,其实像Spring就是别人写好的框架,咱使用Spring开发项目严格意义上就来说就是在使用别人开发好的软件架构进行二开(这句话勿死扣,知道我想表达的意思即可)
包括 若依、JeecgBoot等都算框架,还有前端的element广义上也算框架。(不要咬文嚼字,如果想要咬文嚼字的话就死背官方定义)
2、什么是Spring?
一句话概括:Spring 是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的框架。
非入侵式是软件开发中的一种设计方式,这种设计方式不会强迫开发者改变他们原有的编码习惯或添加额外的框架特定代码,以适应该系统或组件
Spring是一个轻量级Java开发框架,最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。
Spring 为 Java 程序提供了全面的基础架构支持,包含了很多非常实用的功能,如 Spring JDBC、Spring AOP、Spring ORM、Spring Test 等,这些模块的出现,大大的缩短了应用程序的开发时间,同时提高了应用开发的效率。
Spring的优点毋庸置疑:
- 方便解耦,简化开发
- Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理。
- AOP编程的支持
- Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。
- 声明式事务的支持
- 只需要通过配置就可以完成对事务的管理,而无需手动编程。
- 方便程序的测试
- Spring对Junit4支持,可以通过注解方便的测试Spring程序。
- 方便集成各种优秀框架
- Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。
- 降低JavaEE API的使用难度
- Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低
以上这些优点基于自己往常写的代码说几个就行,重要的是下面这句话
Spring的两大核心概念: IOC 和 AOP
IOC 和 AOP 在本篇博客下文中有更详细的介绍,此处先简单解释一下
IoC 是“控制反转”的意思,它不是一个具体的技术,而是一个实现对象解耦的思想。
控制反转的意思是将要使用的对象生命周期的控制权进行反转,传统开发是当前类控制依赖对象的生命周期的,现在交给其他人(Spring),这就是 控制(权)反转。
原本对象的生命周期,创建销毁等是由我们程序员手动控制的,但是在使用Spring后,对应的生命周期,创建销毁等都由Spring控制了,这就是 控制反转,其实说 控制权反转 更好理解
AOP 就是 面向切面编程,简单来说就是统一功能处理
AOP 可以说是 OOP(面向对象编程)的补充和完善,OOP 引入封装、继承和多态性等概念来建立一种公共对象处理的能力,当我们需要处理公共行为的时候,OOP 就会显得无能为力,而 AOP 的出现正好解决了这个问题。比如统一的日志处理模块、授权验证模块等都可以使用 AOP 很轻松的处理。
注:也有人会说,Spring的核心概念还有DI(依赖注入),其实DI是IOC的实现手段,IOC是思想,DI是实现方法,下文中有详细说明
3、Spring有哪些模块
其实这个时候问的就是广义的Spring,即Spring家族有哪些模块
Spring框架是一个全面的企业级Java应用开发平台,它由多个模块组成,每个模块负责不同的功能领域。以下是Spring框架的主要模块及其简要描述:
- Spring Boot - 提供了快速构建独立Spring应用的方法,简化了配置文件,自动配置了Spring,同时也提供了一些生产就绪的功能。
- Spring Web - 支持Web应用的开发,并提供了对Servlet API的集成。
- Spring Security - 提供了强大的安全性和认证/授权服务。
- Spring Data - 简化了数据访问层的开发,支持各种数据存储技术。
- Spring Test - 提供了支持测试的模块,尤其是针对Spring组件的单元测试和集成测试的支持。
- Spring AOP - 实现了面向切面编程(AOP)。它允许你定义方法拦截器(Method Interceptors)和切入点(Pointcuts),使得你可以在不修改代码的情况下增加新的行为。
- Spring Cloud - 提供了构建分布式系统的工具,如配置管理、服务发现、断路器等。
- Spring AI - 简化机器学习模型的集成、训练和部署,使其更容易与现有的Spring应用程序集成。
其实真要说的话还有很多,但以上供我们回复面试官是足够了,下面的有点印象就行
-
Spring Core - 提供了基本的核心容器功能,如依赖注入(DI)和IoC(控制反转)容器。它是Spring框架的基础,其他模块建立在其之上。
-
Spring Context - 建立在Spring Core之上,提供了丰富的配置方式,如属性文件、国际化支持、事件传播机制以及应用层的事件驱动模型等。
-
Spring DAO - 数据访问对象(DAO)模块简化了JDBC的使用,并且提供了一种异常层次结构,使得你可以从DAO层抛出一致的异常。
-
Spring ORM - 集成了主流的ORM框架,如Hibernate、MyBatis和JPA等,使你可以用这些框架来处理数据持久化。
-
Spring MVC - 实现了一个模型-视图-控制器(Model-View-Controller, MVC)框架,它是Spring的Web应用框架,也是Spring框架的一部分。
-
Spring Web Services - 支持创建和使用Web服务。
-
Spring Integration - 提供了企业集成模式的支持,帮助构建消息驱动的应用程序。
每个模块都可以单独使用,也可以组合起来使用,以满足不同应用场景的需求。(但这些模块之间并不是完全的隔离关系,是互相有关联的)
4、什么是SpringBoot?SpringBoot和Spring的关系
Spring Boot 本质上是 Spring 框架的延伸和扩展,它的诞生是为了简化 Spring 框架初始搭建以及开发的过程,使用它可以不再依赖 Spring 应用程序中的 XML 配置,为更快、更高效的开发 Spring 提供更加有力的支持。
总结:Spring 简化了 Java 程序的开发,而Spring Boot 简化了 Spring 的开发
当然,SpringBoot不只是集成并简化了Spring,他还集成了tomcat等容器。
Spring 和 Spring Boot 的区别就像,自己做菜需要买各种原料(类似 Spring),而预制菜(半成品)只需要加热炒熟(类似于 Spring Boot)一样。
其实挺好理解的,java程序的特点之一就是易封装,这使得java的框架层出不穷,不停地封装,甚至是封装再封装。Spring就是对Java的许多基础功能进行了封装,后来发现即便如此,开发起来还是麻烦,于是又对Spring进行了封装,得到了SpringBoot(这段话是为了更好的理解,死扣的话不是绝对性地正确,但讲给面试官是问题不大的)
细说区别:
- 简化配置:Spring Boot引入自动配置功能,开发者无需手动编写繁复的XML或注解配置,许多常见功能(如数据库连接、Web服务器、模板引擎等)都已预设默认配置。
- 一键启动:Spring Boot整合了内嵌的Servlet容器(如Tomcat),使得应用可直接作为Java应用启动,无需部署到单独的服务器。
- starter项目:Spring Boot通过起步依赖(Starters)简化了Maven或Gradle构建配置,只需添加少量依赖就能引入一组相关的库。
- 微服务友好:Spring Boot天生支持微服务架构,配合Spring Cloud,可以快速搭建分布式系统的各个模块。
5、什么是SpringMVC?SpringMVC和三层架构的关系
SpringMVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分。简化开发,减少出错,方便组内开发人员之间的配合。
以上是它的较为官方的概念,下面我将用大白话来诠释什么是SpringMVC
- MVC是思想,就是把一个项目分成了三部分
- SpringMVC进行了实现,称为SpringMVC
- Model模型层用来处理业务逻辑,处理数据,内部放置的是项目的逻辑以及方法的实现相关代码
- Controller控制器层选择处理模型,选择视图,实现前后端交互,是View层和Model层交流的桥梁
- View层面向用户,用于界面显示,人机交互
用户的请求在View层接收后,发送到Controller层,Controller层交给对应的,能处理用户请求的Model层
- View层相当于用户,Controller相当于前台,Model相当于各个部门
- 用户带着要求来到前台(View),前台(Controller)听完用户要求后,将其交给对应的销售部、广告部等部门(Model)
下面这张图只是用户的请求直接发给了Controller控制器层,没有经过View层
比如浏览器url路径直接发送请求或者PostMan等工具发送请求时就更适用于下面这张图
下面这篇博客更加的详细: (非常推荐,阅读完可以理清关系)
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?_分层解耦就是springmvc吗-CSDN博客
6、Spring,SpringBoot和SpringMVC的关系以及区别
- Spring是一个轻量级Java开发框架,简化了Java开发
- SpringBoot 是 Spring 框架的延伸和扩展,简化了 Spring 的开发
- SpringMVC是一个实现了MVC设计模式的的轻量级web框架
下面这篇博客更加的详细:
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习_面试题 spring springboit springmvc的区别-CSDN博客
7、什么是maven?Spring和maven的区别?
- Maven 是一个项目管理和构建工具,它提供了一种标准化的项目布局、依赖管理和构建生命周期,它通过 pom.xml 文件来管理项目的依赖项、构建过程和其他元数据。
- Spring 是一个应用框架,它提供了许多模块化的组件。这些组件可以通过 Maven 作为依赖项添加到项目中。
Spring 框架本身并没有直接“集成”Maven,但它们在实际开发中经常一起使用
通过 Maven 管理项目的依赖项和构建过程,而 Spring 则提供应用程序的业务逻辑和架构支持。Spring Boot 更进一步简化了这一过程,通过 spring-boot-maven-plugin 插件和 spring-boot-starter-parent POM 文件,使得开发者可以更加专注于业务逻辑的实现,而不是繁琐的配置和构建过程。
通过这种方式,Spring 和 Maven 的结合使用,使得 Java 开发更加高效和便捷。
8、什么是IOC?为什么要使用IOC?
IOC 和 AOP 是 Spring 中最核心的两个概念,IOC是AOP的基础
IOC 是 Inversion of Control 的缩写,翻译成中文是"控制反转”的意思,它不是一个具体的技术,而是一个实现对象解耦的思想。
IOC,控制反转,也叫依赖注入,其实我个人认为它叫 控制权反转 更好理解
控制反转的意思是将要使用的对象生命周期的控制权进行反转,传统开发是当前类控制依赖对象的生命周期的,现在交给其他人(Spring),这就是控制(权)反转。
原本对象的生命周期,创建销毁等是由我们程序员手动控制的,但是在使用Spring后,对应的生命周期,创建销毁等都由Spring控制了,这就是 控制反转。
其实很多人是对把对象交给Spring管理这句话是比较迷茫的,我这里用代码来解释一下:
正常情况下我们使用对象是需要实例化类来获得该类的实例对象的
不使用IOC容器:需要手动创建对象并显示地注入依赖项
User user = new User();
使用IOC容器:依赖注入由容器自动完成,代码更简洁,耦合度更低,易于测试和维护
@Autowired private User user;
可以看到,我们没有显示地创建对象,没有赋予 new User() 这个实例,但我们在代码中就可以使用user了,就是因为我们已经将对象交给Spring管理了,后续我们使用user使用的都是Spring交给我们的user对象
嘻嘻,到此处可能感觉把对象交给Spring管理好像没有方便许多,但其实交给Spring管理是为了更多地可能性,比如AOP统一功能处理
- 现在假设我想在所有方法执行前打印666,那我需要在所有方法体内第一行写上打印666的代码。但我将该方法或该类交给Spring管理后,我就可以写一个统一功能处理的代码,拿到所有方法的代理对象,然后在代理对象执行前打印666就行了。
- 但如果我没有将该方法或类交给Spring管理,那Spring拿不到对应的代理对象的话,就无法将这个统一功能处理应用到该方法上。
这也就是为什么说IOC是AOP(面向切面编程)的基础
IOC的优点:
- 解耦和松散耦合:IOC通过将组件之间的依赖关系从代码中分离出来,实现了松散耦合。这意味着组件不需要直接了解它们之间的详细实现,从而提高了代码的可维护性和可重用性。
- 代码简洁性:IOC使你的代码更加专注于业务逻辑,而不需要过多关注依赖的创建和管理。这使得代码更加清晰、简洁和易于理解。
- 生命周期管理:IOC容器可以管理组件的生命周期,确保它们在合适的时间进行创建、初始化和销毁。
- 可重用性:由于依赖关系由容器管理,可以更容易地将组件在不同的应用程序中重用。
- AOP 实现基础:IOC是实现 AOP(面向切面编程)的基础,允许你将横切关注点(如日志、安全性)与核心业务逻辑分离
9、什么是DI,DI和IOC有什么关系?
DI是“依赖注入”的意思。
IOC是思想,DI是具体的实现技术
依赖注入不是一种设计实现,而是一种具体的技术,它是在 IoC 容器运行期间,动态地将某个依赖对象注入到当前对象的技术就叫做 DI(依赖注入)
比如 A对象需要依赖 B对象,那么在 A运行时,动态的将依赖对象 B注入到当前类中,而非通过直接 new 的方式获取 B 对象的方式,就是依赖注入。
IOC 和 DI虽然定义不同,但它们所做的事情都是一样的,都是用来实现对象解耦的,而二者又有所不同:IOC 是一种设计思想,而 DI 是一种具体的实现技术。
举个例子吧,我想做一个木雕,以寄托对她的思念。想做一个木雕这是想法,也就是IOC,而我拿着一把刻刀,一段上好槐木做出来了这个木雕,这是具体实现,实现技术就是用刻刀和槐木声泪泣下地刻下她在我脑海中最后的身影,这就是DI
想实现年薪百万,这是IOC,学习JAVA炒粉通过炒粉年入百万,这是DI
IOC除了DI外,还有其他实现方式么?
- 有!
IOC除了DI依赖注入外,还可以通过依赖查找(Dependency search)来实现
在 Spring 框架中,依赖査找通过 ApplicationContext 接口的 getBean()方法来实现,如下代码所示:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class App {public static void main(String[] args) {// 1. 先得到 Spring 上下文对象ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");// 2. 得到 Bean (依赖查找 -> IoC 的一种实现)UserService userService = context.getBean("userService", UserService.class);// 3. 使用 Bean 对象userService.sayHi();}
}
10、DI依赖注入的方式和区别?Bean的注入方式和区别?
依赖注入是一种编程模式,而Bean的注入是这种模式在一个具体框架中的应用。
所以这里他俩的注入方式和区别都是一样的,只不过有的面试官喜欢提问DI依赖注入的方式和区别,有的喜欢提问Bean的注入方式和区别
Bean对象有以下3种注入方式:
- 属性注入
- Setter注入
- 构造方法注入
①、属性注入
属性注入是我们最熟悉,也是日常开发中使用最多的一种注入方式,说白了就是通过注解的方式注入,常用的注解有 @Autowired 和 @Resource
它的实现代码如下:
@RestController
public class UserController { // 类名应该遵循大驼峰命名规则@Autowiredprivate UserService userService; // 变量名也应该遵循小驼峰命名规则@RequestMapping("/add")public UserInfo add(String username, String password) {return userService.add(username, password);}
}
优点分析
- 属性注入最大的优点就是实现简单、使用简单,只需要给变量上添加一个注解(@Autowired),就可以在不 new对象的情况下,直接获得注入的对象了(这就是 DI的功能和魅力所在),所以它的优点就是使用简单。
缺点分析
- 功能性问题:无法注入一个不可变的对象(final 修饰的对象)
原因也很简单:在 Java 中 final 对象(不可变)要么直接赋值,要么在构造方法中赋值,所以当使用属性注入final 对象时,它不符合 Java 中 final 的使用规范,所以就不能注入成功了。
- 通用性问题:只能适应于 IOC 容器,Idea 也会提示你,不建议使用
Spring的IOC容器负责创建和管理应用程序中的bean,并在适当的时候将它们注入到其他bean中,这就导致脱离了Spring后这个注解就会出现错误
②、Setter注入
@RestController
public class UserController {private UserService userService;@Autowiredpublic void setUserService(UserService userService) {this.userService = userService;}@GetMapping("/add")public UserInfo add(String username, String password) {return userService.add(username, password);}
}
从上面代码可以看出,Setter 注入比属性注入要麻烦很多。
优点分析
- Setter注入符合单一职责的设计原则,因为每一个 Setter 只针对一个对象
缺点分析:
- 不能注入不可变对象(final 修饰的对象)
- 注入的对象可调用多次,也就是注入对象会被修改
拿上面的代码解释这一点,如果多次调用 setUserService 方法,userService 字段就会被多次更改。这样的话,userService字段就会被新的OtherUserService实例覆盖,这可能导致对象状态的不确定性和多线程环境下的问题
③ 构造方法注入
构造方法注入是 Spring 官方从 4.x 之后推荐的注入方式,它的实现代码如下:
它推荐归它推荐,咱该用啥还是用啥
@RestController
public class UserController {private final UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}@GetMapping("/add")public UserInfo add(String username, String password) {return userService.add(username, password);}
}
注:如果当前的类中只有一个构造方法,那么 @Autowired 也可以省略
优点分析:
构造方法注入相比于前两种注入方法,它可以注入不可变对象,并且它只会执行一次,也不存在像 Setter 注入那样,被注入的对象随时被修改的情况,它的优点有以下4个:
- 可注入不可变对象
- 注入对象不会被修改
- 注入对象会被完全初始化
- 通用性更好
缺点分析:
- 唯一缺点,没有属性注入写法简单(看起来没啥,但很致命哈哈bukebian
优点原因分析
- 注入对象不会被修改:构造方法在对象创建时只会执行一次,因此它不存在注入对象被随时(调用)修改的情况。
- 完全初始化:因为依赖对象是在构造方法中执行的,而构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化,这也是构造方法注入的优点之一。
- 通用性更好:构造方法和属性注入不同,构造方法注入可适用于任何环境,无论是 IOC 框架还是非 IOC 框架,构造方法注入的代码都是通用的,所以它的通用性更好
11、@Autowired和@Resource有什么区别?
@Autowired 和 @Resource 都是用来实现依赖注入的注解(在 Spring/Spring Boot 项目中)
相同点
对于下面的代码来说,如果是Spring容器的话,两个注解的功能基本是等价的,他们都可以将bean注入到对应的field(字段)中
@Autowired
private Bean beanA;@Resource
private Bean beanB;
不同点
- 来源不同:@Autowired 来自 Spring 框架,而 @Resource 来自于 Java JSR-250(Java规范提案)
- 依赖查找的顺序不同:@Autowired 先根据类型再根据名称查询,而 @Resource 先根据名称再根据类型查询
- 支持的参数不同:@Autowired 只支持设置1个required 参数,而 @Resource 支持设置多个参数(name、type 等)
- 依赖注入的用法支持不同:@Autowired 既支持构造方法注入,又支持属性注入和 Setter 注入,而@Resource 只支持属性注入和 Setter 注入
这两个哪个好?
- 其实没有根本性的好坏之分,取决于你们公司的代码规范吧
12、什么是AOP?它有哪些使用场景?
AOP (也叫面向切面编程) ,我更喜欢叫它 统一功能处理
统一功能处理嘛,这个从字面意思理解就行,进行一些统一功能,全局功能的操作,比如我要在所有方法执行前打印666,这个就属于 统一功能处理 的范畴,可以利用SpringAOP来完成这个操作
AOP 可以说是 OOP(面向对象编程) 的补充和完善,OOP 引入封装、继承和多态性等概念来建立一种公共对象处理的能力,当我们需要处理公共行为的时候,OOP 就会显得无能为力,而 AOP 的出现正好解决了这个问题。比如统一的日志处理模块、授权验证模块等都可以使用 AOP 很轻松的处理。
AOP 优点主要有以下几个:
- 集中处理某一类问题,方便维护
- 逻辑更加清晰
- 降低模块间的耦合度
AOP 常见使用场景有以下几个:
- 用户登录和鉴权,
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 声明式事务的实现
13、AOP是如何组成的?它有几种增强方法?
AOP 是由切面(Aspect)、切点(Pointcut)、通知(Advice)和连接点(Join Point)组成的。
①切面(Aspect)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
简单来说,切面就是当前 AOP 功能的类型,比如当前 AOP 是用户登录和鉴权的功能,那么它就是一个切面。
② 切点(Pointcut)
切点 Pointcut:它的作用就是提供一组规则,用来匹配连接点的。
简单来说,切点就是设置拦截规则的,满足规则的方法将会被拦截。
③ 通知(Advice)
切面也是有目标的,这个目标即它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
简单来说,当控制器(方法)被拦截之后,如果它符合我们定义的切点条件,那么就会触发我们事先定义好的一些额外行为,这些额外行为就是所谓的“通知”。
④ 连接点(Join Point)
连接点是指在应用程序执行过程中可以被拦截的特定点。换句话说,连接点是在程序执行过程中的某个特定位置,比如方法调用、方法执行、异常抛出等。
连接点是 AOP 中的基本单位,它代表了一个可以被切面拦截的位置,允许在这些位置插入额外的逻辑。切面可以通过连接点来捕获并应用它们的横切关注点(如日志、事务、安全性等)。
在 Spring AOP 中,一些连接点包括:
- 方法调用:当一个方法被调用时,连接点发生。
- 方法执行:当一个方法的实际代码开始执行时,连接点发生。
- 异常抛出:当方法抛出异常时,连接点发生。
- 字段访问:当访问一个类的字段时,连接点发生。
连接点通过一个表示方法或其他事件的点来标识。例如,表达式 execution(*com.example.Service.doSomething(..)) 表示在 com.example.Service 类的 doSomething 方法被调用或执行时的连接点。
连接点是实现 AOP 的基础,允许切面在特定位置插入增强逻辑,从而实现横切关注点的功能。切面可以选择在哪些连接点上应用(通过定义切点实现),以便将适当的逻辑织入到应用程序的执行流程中。
AOP有几种增强方法?
AOP 的增强方法也就是 Advice 通知,Spring AOP 总共有以下 5 种通知类型:
- 前置通知:使用 @Before 实现,通知方法会在目标方法调用之前执行
- 后置通知:使用 @After 实现,通知方法会在目标方法返回或者抛出异常后调用
- 返回通知:使用 @AfterReturning 实现,通知方法会在目标方法返回后调用
- 抛出异常通知:使用 @AfterThrowing 实现,通知方法会在目标方法抛出异常后调用
- 环绕通知:使用 @Arund 实现,通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
它的使用示例如下:
@Component
@Aspect
public class LoggingAspect {// 前置通知@Before("execution(* com.example.service.*.*(..))")public void beforeAdvice(JoinPoint joinPoint) {System.out.println("前置通知执行...");System.out.println("方法名称: " + joinPoint.getSignature().getName());}// 后置通知@After("execution(* com.example.service.*.*(..))")public void afterAdvice(JoinPoint joinPoint) {System.out.println("后置通知执行...");}// 返回后通知@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")public void afterReturningAdvice(JoinPoint joinPoint, Object result) {System.out.println("返回后通知执行...");System.out.println("返回结果: " + result);}// 抛出异常通知@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {System.out.println("抛出异常通知执行...");System.out.println("异常信息: " + ex.getMessage());}// 环绕通知@Around("execution(* com.example.service.*.*(..))")public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {System.out.println("环绕通知开始执行...");Object result = pjp.proceed(); // 继续执行目标方法System.out.println("环绕通知结束执行...");return result;}
}
14、AOP底层是如何实现的?
Spring 中的 AOP 底层是通过代理来实现的,而 Spring 中使用了以下两种代理:
- JDK 动态代理:对于实现了接口的目标类,Spring 使用 Java 提供的 java.lang.reflect.Proxy 类来创建代理对象。它要求目标类实现至少一个接口,并通过接口生成代理对象,代理对象实现了相同的接口,并且调用代理方法时会触发代理处理器的逻辑。JDK动态代理底层又是通过反射实现的。
- CGLib 代理:对于没有实现接口的目标类,Spring 使用 CGLIB 来创建代理对象。CGLIB 会生成目标类的子类作为代理,重写方法并在方法前后插入切面逻辑。CGLib 底层是通过生成目标类的字节码来实现的。
Spring AOP 默认使用的是 JDK 动态代理(这点在官方文档种有具体的说明)
然而,Spring Boot 2.0 之后却默认并只使用了 CGLib(源码中可看到,无论被代理类是否实现接口,都会使用CGLib来实现)
想要解决这个问题,可以在配置文件中设置以下配置:
# 启用AOP自动代理
spring.aop.auto=true# 代理方式: 设置为 true 表示强制使用 CGLIB 代理
spring.aop.proxy-target-class=true
15、JDK动态代理和CGLib有什么区别?
JDK 动态代理(JDK Proxy)和 CGLib 都是 Spring 中用于实现 AOP 的代理技术,但它们存在以下区别:
- 来源不同:JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现。Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;而 CGLib 是第三方提供的工具,基于 ASM (一个字节码操作框架) 实现的。
- 使用场景不同:JDK Proxy 只能代理实现了接口的类,而 CGLib 无需实现接口,它是通过实现目标类的子类来完成调用的,所以要求被代理类不能被 final 修饰。
- 性能不同:JDKProxy 在JDK7之前性能远不如 CGLib,但JDK7之后 (JDK7和JDK8都有优化) JDKProxy 性能就略高于 CGLib 了。
小结:JDK Proxy 是 Java 自带的,在 JDK 高版本性能比较高的动态代理工具,但它要求被代理类必须要实现接口,它的性能在 JDK7 之后也略高于 CGLib;而 CGLib 是基于字节码技术实现的第三方动态代理,它是通过生成代理对象的子类来实现代理的,所以要求被代理类不能被 final 修饰。
16、什么是Bean
初级开发的话,你就需要记住Bean就是对象,就是你理解的Java中的对象,是所有对象的统称
然后我们讨论Bean,实际就是在讨论对象
17、Bean的生命周期?
Bean 的大致生命周期为:实例化 -> 属性赋值 -> 初始化 -> 使用 -> 销毁
Bean 生命周期是指其在 Spring 容器中从创建到销毁的过程
在 Spring 源码中,AbstractAutowireCapableBeanFactory 是用于实现 Bean 的自动装配和创建的关键类。它里面的 doCreateBean 方法包含了 Bean 生命周期的实现逻辑(AbstractAutowireCapableBeanFactory 是 BeanFactory的子类,这里主要记住BeanFactory)
如下源码所示(以下源码基于 Spring 6):
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {BeanWrapper instanceWrapper = null;if (mbd.isSingleton()) {instanceWrapper = (BeanWrapper) this.factoryBeanInstanceCache.remove(beanName);}// a. 实例化 Beanif (instanceWrapper == null) {instanceWrapper = this.createBeanInstance(beanName, mbd, args);}// 忽略其他方法...Object exposedObject = instanceWrapper.getWrappedInstance();try {// b. 设置属性this.populateBean(beanName, mbd, instanceWrapper);// c. 执行初始化方法exposedObject = this.initializeBean(beanName, exposedObject, mbd);} catch (Throwable var18) {if (var18 instanceof BeanCreationException bce) {if (beanName.equals(bce.getBeanName())) {throw bce;}}throw new BeanCreationException(mbd.getResourceDescription(), beanName, var18.getMessage(), var18);}// 忽略其他方法...// 定义 Bean 的销毁回调方法try {this.registerDisposableBeanIfNecessary(beanName, exposedObject, mbd);} catch (BeanDefinitionValidationException var16) {// 处理 BeanDefinitionValidationExceptionthrow new BeanCreationException(mbd.getResourceDescription(), beanName, var16.getMessage(), var16);}return exposedObject;
}
所以从以上源码也可以看出 Bean 的大致生命周期为:
实例化 -> 属性赋值 -> 初始化 -> 使用 -> 销毁
Bean 生命周期主要会经历以下几个阶段:
- 实例化(Instantiation):从无到有,为 Bean 分配内存空间,调用 Bean 的构造方法
- 属性赋值(Populate Properties):在创建实例后,Spring 将会通过 依赖注入(DI) 的方式将 Bean 的属性赋值。
- 初始化(Initialization):初始化阶段主要有以下 3个步骤:
- 初始化前置处理:在 Bean 的初始化之前,会调用所有实现了 BeanPostProcessor 接口的类的postProcessBeforeInitialization 方法。
- 初始化方法调用:如果在 Bean 上定义了初始化方法,通过 @PostConstruct 注解或实现了 InitializingBean 接口。
- 初始化后置处理:在 Bean 的初始化之后,会调用所有实现了 BeanPostProcessor 接口的类的postProcessAfterInitialization 方法。
- 使用(Using):在初始化之后,Bean 可以被使用。在这个阶段,Bean 执行它的业务逻辑。
- 销毁(Destruction):销毁 Bean 实例。
18、BeanFactory和FactoryBean有什么区别?
BeanFactory 和 FactoryBean 完全不同的两个接口,BeanFactory 是用来管理 Bean 对象的,而 FactoryBean本质上一个 Bean,也归 BeanFactory 管理,但使用 FactoryBean 可以来创建普通的 Bean 对象和 AOP 代理对象,它们具体区别如下:
① BeanFactory
Beanfactory 是 Spring 框架的核心接口之一,它是一个工厂模式的实现,负责管理 bean 的生命周期、创建和销毁等操作。BeanFactory 提供了各种方法来获取 bean,包括按名称获取、按类型获取等。
其中,ApplicationContext 就是 BeanFactory 的子类,咱们通常会使用 ApplicationContext 来获取某个 Bean:
public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);MyService service = context.getBean(MyService.class);service.doWork();
}
刚才Bean的生命周期种,我们提到了在 Spring 源码中,AbstractAutowireCapableBeanFactory 是用于实现 Bean 的自动装配和创建的关键类,这里又提到了ApplicationContext,这里我用继承关系图来演示他们的关系:
AbstractAutowireCapableBeanFactory 和 ApplicationContext 都是 BeanFactory 的子类,BeanFactory是祖宗哈哈
BeanFactory|+-- ListableBeanFactory| || +-- ApplicationContext|+-- AutowireCapableBeanFactory|+-- AbstractAutowireCapableBeanFactory
因为Bean的创建和销毁等操作一般都是交给Spring的,所以其实我们使用BeanFactory 的主要场景是从 IoC 容器中获取 Bean 对象
② FactoryBean
它是一个 Bean,但又不仅仅是一个普通的 Bean,它是一个能生成 Bean 对象的工厂。
它的使用示例如下:
@Component
public class MyBean implements FactoryBean<MyBean> {private String message;public MyBean() {this.message = "通过构造方法初始化实例";}@Overridepublic MyBean getObject() throws Exception {// 方法增强return new MyBean("通过 FactoryBean.getObject() 创建实例");}@Overridepublic Class<?> getObjectType() {return MyBean.class;}@Overridepublic boolean isSingleton() {return true; // 或者 false,取决于是否需要单例}public String getMessage() {return message;}
}
FactoryBean 在 Spring 中最为典型的一个应用就是用来创建 AOP 的代理对象
FactoryBean 源码如下:
import org.springframework.beans.factory.FactoryBean;public interface FactoryBean<T> {/*** 从工厂中获取 Bean。** @return 返回由 FactoryBean 创建的对象实例。* @throws Exception 如果创建对象时出现异常。*/@NullableT getObject() throws Exception;/*** 获取 Bean 工厂创建的对象的类型。** @return 返回由 FactoryBean 创建的对象类型。*/@NullableClass<?> getObjectType();/*** 判断 Bean 工厂创建的对象是否是单例模式。** @return 如果是单例模式返回 true,否则返回 false。*/default boolean isSingleton() {return true;}
}
既然已经有了 Beanfactory,功能也足够强大,那为什么要使用 FactoryBean呢?
- 自由度更高:FactoryBean 本身也是一个 Bean,这意味着它可以像其他 Bean 一样被配置和管理
- 复杂对象的创建:对于那些需要复杂初始化逻辑的对象,FactoryBean 提供了一种方便的方式来创建和初始化这些对象。(看代码)
- 延迟初始化:FactoryBean 可以在需要时才创建对象,这有助于节省资源和提高性能。
- 增强对象创建过程:通过 Factoryeean 可以在创建对象的过程中添加额外的逻辑,如装饰器模式的应用(不懂装饰器模式的话建议这一条不要说,防止被面试官追问)
19、Bean是线程安全的吗?实际工作中怎么保证其线程安全?
默认情况下,Bean 是非线程安全的。因为默认情况下 Bean 的作用域是单例模式,那么此时,所有的请求都会共享同一个 Bean 实例,这意味着如果这个 Bean 实例在多线程下,被同时修改(成员变量)时,就可能出现线程安全问题。
Bean 的作用域指的是确定在应用程序中创建和管理 Bean 实例的范围。也就是在 Spring 中,可以通过指定不同的作用域来控制 Bean 实例的生命周期和可见性。例如,单例模式就是所有线程可见并共享的,而原型模式则是每次请求都创建一个新的原型对象。
注:这里简单补充一下什么是单例模式?后续我出一篇专门博客介绍这些设计模式
- 单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点
单例Bean一定是非线程安全的吗?
并不是,单例 Bean 分为以下两种类型:
- 无状态 Bean (线程安全):Bean 没有成员变量,或多线程只会对 Bean 成员变量进行査询操作,不会修改操作。
- 有状态 Bean (非线程安全):Bean 有成员变量,并且并发线程会对成员变量进行修改操作。
所以说:有状态的单例 Bean 是非线程安全的,而无状态的 Bean 是线程安全的。
但在程序中,只要有一种情况会出现线程安全问题,那么它的整体就是非线程安全的,所以总的来说,单例Bean 还是非线程安全的。
① 无状态的Bean
无状态的 Bean 指的是不存在成员变量,或只有查询操作,没有修改操作,它的实现示例代码如下:
注:其实大部分情况下,咱的service实现类的代码都是无状态的Bean,没有成员变量
有的同学说,多线程下插入操作也不是线程安全的呀,确实,但这个线程安全和Bean的线程安全无关。
@Service
public class YwAssessPersonServiceImpl implements IYwAssessPersonService {@Overridepublic List<YwAssessPerson> selectYwAssessPersonList(YwAssessPerson ywAssessPerson) {// 执行查询操作,不保存任何状态}@Overridepublic YwAssessPerson selectYwAssessPersonBySid(Long sid) {// 根据ID查询,不保存任何状态}@Overridepublic AjaxResult insertYwAssessPerson(YwAssessPerson ywAssessPerson) {// 插入操作,不保存任何状态}@Overridepublic AjaxResult updateYwAssessPerson(YwAssessPerson ywAssessPerson) {// 更新操作,不保存任何状态}@Overridepublic int deleteYwAssessPersonBySids(Long[] sids) {// 删除操作,不保存任何状态}// 其他方法...
}
② 有状态的Bean
有成员变量,并且存在对成员变量的修改操作,如下代码所示:
@Service
public class YwAssessPersonServiceImpl implements IYwAssessPersonService {// 成员变量,用于存储一些状态信息private Map<Long, YwAssessPerson> personCache = new ConcurrentHashMap<>();private AtomicInteger counter = new AtomicInteger(0);@Autowiredprivate YwAssessPersonRepository repository;@Overridepublic List<YwAssessPerson> selectYwAssessPersonList(YwAssessPerson ywAssessPerson) {// 查询逻辑,并缓存结果}@Overridepublic AjaxResult insertYwAssessPerson(YwAssessPerson ywAssessPerson) {// 插入操作,并更新缓存}
}
实际工作中如何保证Bean的线程安全?
想要保证有状态 Bean 的线程安全,可以从以下几个方面来实现:
- 使用 ThreadLocal (线程本地变量):每个线程修改自己的变量,就没有线程安全问题了
- 使用锁机制:例如 synchronized 或 ReentrantLock 加锁修改操作保证线程安全。
- 设置 Bean 为原型作用域:将 Bean 的作用域设置为原型,这意味着每次请求该 Bean 时都会创建一个新的实例,这样可以防止不同线程之间的数据冲突,不过这种方法增加了内存消耗。
- 使用线程安全容器:例如使用 Atomic 家族下的类(如 AtomicInteger)来保证线程安全,此实现方式的本质还是通过锁机制来保证线程安全的,Atomic 家族底层是通过乐观锁 CAS(比较并替换)来保证线程安全的。
具体实现如下:
①、使用 ThreadLocal 保证线程安全
实现代码如下:
@Service
public class UserService {// 使用ThreadLocal来为每个线程提供独立的计数器private final ThreadLocal<Integer> count = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};// 增加计数器的值public void incrementCount() {int currentValue = count.get();count.set(currentValue + 1);}// 获取当前线程的计数器值public int getCount() {return count.get();}
}
使用 ThreadLocal 需要注意一个问题,在用完之后记得调用 ThreadLocal 的 remove 方法,不然会发生内存泄漏问题。
② 、使用锁机制
锁机制中最简单的是使用 synchronized 修饰方法,让多线程执行此方法时排队执行,这样就不会有线程安全问题了,如下代码所示:
@Service
public class UserService {private int count = 0;// 增加计数器的值public synchronized void incrementCount() {count++; // 非原子操作,并发存在线程安全问题}// 获取当前计数器的值public int getCount() {return count;}
}
现在理论上UserService 这个Bean是线程安全的,但因为count++ 是非原子操作,所以实际上并发还是存在线程安全问题的
因为在Java中, count++ 操作实际上包含了三个步骤:
- 读取当前值 (
read
):从内存中读取count
的当前值。 - 增加当前值 (
modify
):在寄存器中将读取的值加1。 - 写回新值 (
write
):将修改后的值写回到内存中。
这三个步骤在JVM中不是原子的,也就是说,它们可能被拆分成多个CPU指令来执行。在多线程环境中,如果多个线程同时执行count++
操作,可能会出现以下问题:
- 读取冲突:多个线程可能同时读取了
count
的旧值。 - 修改冲突:多个线程可能基于相同的旧值进行增加操作。
- 写入覆盖:最后只有一个线程能够成功写回新值,其他线程的修改可能会被覆盖掉。
如果还有迷惑,请及时留言,我会考虑出一篇专门博客详细讲解这一块
③ 、设置为原型作用域
原型作用域通过 @Scope("prototype”) 来设置,表示每次请求时都会生成一个新对象(也就没有线程安全问题了),如下代码所示:
@Service
@Scope("prototype")
public class UserService {private int count = 0;// 增加计数器的值public synchronized void incrementCount() {count++; // 非原子操作,并发存在线程安全问题}// 获取当前计数器的值public int getCount() {return count;}
}
④ 、使用线程安全容器
我们可以使用线程安全的容器 AtomicInteger 来替代 int,从而保证线程安全,如下代码所示:
@Service
public class UserService {private AtomicInteger count = new AtomicInteger(0);// 增加计数器的值public void incrementCount() {count.incrementAndGet();}// 获取当前计数器的值public int getCount() {return count.get();}
}
实际工作中会使用哪种方案来保证Bean的线程安全?
实际工作中,通常会根据具体的业务场景来选择合适的线程安全方案,但是以上解决线程安全的方案中,ThreadLocal 和原型作用域会使用更多的资源,占用更多的空间来保证线程安全,所以在使用时通常不会作为最佳考虑方案
而锁机制和线程安全的容器通常会优先考虑,但需要注意的是 AtomicInteger(线程安全容器) 底层是乐观锁 CAS 实现的,因此它存在乐观锁的典型问题 ABA 问题(如果有状态的 Bean 中既有 ++ 操作,又有 -- 操作时,可能会出现 ABA 问题),此时就要使用锁机制,或 AtomicStampedReference 来解决 ABA 问题了
20、Spring 容器启动阶段会干什么?Spring启动阶段会干什么?
其实本质上Spring启动阶段即Spring容器启动阶段,但是现在俗称的Spring其实是Spring家族,所以Spring启动阶段往往还包含其他Spring模块的启动,如Spring Web、Spring Data、Spring Security等
此处讲的是Spring容器的启动阶段,给面试官回答这个即可,但上面那句话也要加上
Spring的IOC容器启动的过程,其实可以划分为两个阶段: 容器启动阶段 和 Bean 实例化阶段 。(这两个不是完全的先后关系)
其中容器启动阶段主要做的工作是加载和解析配置文件,保存到对应的Bean定义中。(主要就是下面的前两个阶段)
Spring容器的启动流程主要分为以下几个步骤:
- 加载配置文件:Spring容器会从指定的配置文件中读取配置信息,包括bean的定义、依赖关系、AOP切面等。
- 创建容器:Spring容器启动后会创建一个容器实例,容器负责管理bean的生命周期和依赖关系。
- 扫描包并创建bean定义:Spring容器会扫描指定的包路径,自动创建包中标注了@Component、@Service、@Controller、@Repository等注解的类的bean定义。
- 解析依赖关系:Spring容器会根据bean定义中的依赖关系,自动将依赖的bean注入到需要的bean中。
- 初始化bean:容器会按照指定的顺序依次对bean进行初始化,包括实例化、属性注入、初始化方法执行等。
- 设置代理对象:如果bean需要被AOP切面增强,则容器会为其创建代理对象。
- 完成容器初始化:所有bean初始化完成后,Spring容器启动完成。
21、Spring Boot 中的常用注解有哪些?
更详细博客:java 常用注解大全、注解笔记_实体类注解表名当前值的属性-CSDN博客
注:更详细的博客会持续更新,只不过最近比较忙,但催更的话会加急更新
Spring Boot 中的常用注解有很多,比如以下这些:
@SpringBootApplication:标记一个主要的 Spring Boot 应用的启动类
@Controller:将类标记为控制器,处理 HTTP 请求
@RestController:类似于 @Controller,但是它还将返回的对象自动转换为 JSON 格式
@RequestMapping:用于映射 HTTP 请求到具体的处理方法
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping:用于分别处理 GET、POST、PUT、DELETE 请求。
@RequestParam:用于将请求参数绑定到处理方法的参数上
@PathVariable:用于将 URL 路径变量绑定到处理方法的参数上
@RequestBody:用于将请求体绑定到处理方法的参数上
@Autowired:用于自动装配依赖关系,通过类型进行依赖注入
@Value:用于注入配置属性值
@Component:将类标记为 Spring 容器的组件
@Service:将类标记为服务层组件
@Repository:将类标记为数据访问层组件(通用)
@Mapper:将类标记为数据访问层组件(MyBatis提供,实际这个用的更多)
@Configuration:标记类为配置类,用于定义配置项
@Bean:在配置类中使用,用于声明一个 Bean 对象,
java 常用注解大全、注解笔记_实体类注解表名当前值的属性-CSDN博客
22、如何实现自定义注解?实际工作中哪里会用到自定义注解?
在 Java 中,自定义注解使用 @interface 关键字来定义,它可以实现如:日志记录、性能监控、权限校验等功能。
自定义注解可以标记在方法上或类上,用于在编译期或运行期进行特定的业务功能处理
自定义注解实现方式:
- 通过 AOP(面向切面编程)实现
- 通过拦截器(Interceptor)实现
这里拿AOP实现自定义注解举例
AOP 实现自定义注解
下面我们先使用 AOP 的方式来实现一个打印日志的自定义注解,它的实现步骤如下:
- 添加 Spring AOP 依赖
- 创建自定义注解
- 编写 AOP 拦截(自定义注解)的逻辑代码
- 使用自定义注解
具体实现如下:
1. 添加 Spring AOP 依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 创建自定义注解
首先定义一个自定义注解,比如叫做 @LogExecutionTime
,用于标记需要记录执行时间的日志的方法:
/*** 自定义注解,用于标记需要记录执行时间的方法。*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {/*** 是否启用日志记录,默认为true。** @return 是否启用日志记录*/boolean enable() default true;
}
在上面的例子种,我们定义了一个名为 LogExecutionTime 的注解,它有一个属性,enable,设置了默认值
- @Target(ElementType.METHOD) 指定了该注解只能应用于方法级别
- @Retention(RetentionPolicy.RUNTIME) 表示这个注解在运行时是可见的,这样 AOP 代理才能在运行时读取到这个注解
这两个注释是元注解(总共有四个,此处展示两个):
- @Target:用于描述注解的使用范围,该注解可以使用在什么地方
- @Retention:表明该注解的生命周期
3. 编写 AOP 拦截(自定义注解)的逻辑代码
接下来,创建一个Aspect类,用来实现拦截和日志记录的逻辑:
根据 enable
属性决定是否记录日志
@Aspect
@Component
@Order(1) // 可以设置优先级
public class LoggingAspect {private static final Logger LOGGER = Logger.getLogger(LoggingAspect.class.getName());@Around("@annotation(logExecutionTime)")public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {if (!logExecutionTime.enable()) {return joinPoint.proceed();}long start = System.currentTimeMillis();try {return joinPoint.proceed(); // 继续执行原方法} finally {long executionTime = System.currentTimeMillis() - start;LOGGER.info(joinPoint.getSignature().toString() + " executed in " + executionTime + "ms");}}
}
其实上面这段代码就是利用AOP的机制,检查所有使用了 LogExecutionTime 注解的方法,然后进行一些统一功能处理
4.使用自定义注解
将自定义注解应用于需要进行日志记录的方法上,如下代码所示:
import org.springframework.stereotype.Service;@Service
public class SomeService {@LogExecutionTime(enable = true)public void performSomeOperation() {// 执行一些耗时操作try {Thread.sleep(2000); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException(e);}}
}
实际工作中哪里会用到自定义注解?
- 权限验证
- 日志记录
- 性能监控
- 幂等性判断(这个不会就不要讲)
后续会出一个万字长文详细讲解自定义注解,以及不同场景下如何写对应的自定义注解
23、什么是拦截器和过滤器以及二者的区别
很多人容易将拦截器和过滤器搞混,实际上他俩功能确实差不多哈哈
拦截器(Interceptor)和过滤器(Filter)都是在Web应用程序中常用的机制,用于在请求到达目标资源之前或之后执行某些操作。它们在概念上有一定的相似之处,但在实现细节和应用场景上有所不同
什么是拦截器?
拦截器 (Interceptor) 是一种在应用程序中用于拦截、处理和转换请求和响应的组件。(Spring框架提供)
在 Web 开发中,拦截器是种常见的技术,用于在请求到达控制器之前或响应返回浏览器之前进行干预和处理。
比如以下几种:
- 预处理(Pre-processing):在请求达到控制器之前执行某些操作,如身份验证、权限校验等。
- 后处理(Post-processing):在请求返回视图之前执行某些操作,如修改模型数据等。
- 最终处理(After completion):在请求完全处理完毕之后执行某些操作,如记录日志等。
控制器简单理解为MVC中的Controller,视图就是前端页面嘛
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?_分层解耦就是springmvc吗-CSDN博客
什么是过滤器?
过滤器 (Filter) 是一种常见的 Web 组件,用于在 Servlet 容器中对请求和响应进行预处理和后处理。(Servlet 提供)
过滤器提供了一种在请求和响应的处理链上干预和修改数据的机制,可以用于实现一些与应用程序业务逻辑无关的通用功能。
过滤器可以执行以下几种任务:
- 请求前处理:在请求达到目标资源之前执行某些操作,如身份验证、编码转换等。
- 请求后处理:在请求处理完毕之后执行某些操作,如压缩响应内容、添加响应头等。
- 异常处理:在请求处理过程中出现异常时执行某些操作,如记录日志等
拦截器和过滤器有什么区别?
拦截器 (Interceptor) 和过滤器 (Filter) 都是用于在请求到达目标资源之前或之后进行处理的组件,但它们是完全不同的 2 个组件,它们的区别主要体现在以下5点:
- 所属框架不同:过滤器来自于 Servlet,而拦截器来自于 Spring 框架
- 执行时机不同:请求的执行顺序是:请求进入容器 >进入过滤器 >进入 Servlet >进入拦截器 >执行控制器(Controller),所以过滤器和拦截器的执行时机,是过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。
- 底层实现不同:过滤器是基于方法回调实现的,拦截器是基于 Spring 框架中的执行流程(调用 Controller 之前,先验证所有的拦截器,判断是否可以继续通行)实现的。
- 支持的项目类型不同:过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application或 Swing 程序中。
- 使用场景不同:因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能
24、当客户端发送请求到后端时的执行顺序
此处仅简单说明下,主要还是让大家理清 拦截器和过滤器
当客户端发送请求到/
时,执行顺序如下:
- 过滤器(Filter):MyFilter的doFilter方法执行。
- 拦截器(Interceptor):MyInterceptor的preHandle方法执行。
- 控制器(Controller):MyController的home方法执行。
- 拦截器后处理(Interceptor Post-Handling):MyInterceptor的postHandle方法执行。
- 过滤器后处理(Filter Post-Processing):MyFilter的doFilter方法执行。
25、如何实现拦截器?
在 Spring Boot 中拦截器的实现分为两步
- 创建一个普通的拦截器,实现 HandlerInterceptor 接口,并重写接口中的相关方法。
- 将上一步创建的拦截器加入到 Spring Boot 的配置文件中,并配置拦截规则。
具体实现如下:
1、实现自定义拦截器
@Component
public class TestInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("拦截器: 执行 preHandle 方法。");return true; // 返回 true 表示继续流程,false 则结束流程}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("拦截器: 执行 postHandle 方法。");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("拦截器: 执行 afterCompletion 方法。");}
}
其中:
- preHandle 方法:在目标方法执行前被调用,也就是调用目标方法之前被调用。比如我们在操作数据之前先要验证用户的登录信息,就可以在此方法中实现,如果验证成功则返回 true,继续执行数据操作业务;否则就返回 false,后续操作数据的业务就不会被执行了
- postHandle 方法:调用请求方法之后执行,但它会在 DispatcherServlet 进行渲染视图之前被执行
- afterCompletion 方法:会在整个请求结束之后再执行,也就是在 DispatcherServlet 渲染了对应的视图之后再执行
2、配置拦截规则
最后,我们再将上面的拦截器注入到项目配置文件中,并设置相应拦截规则,具体实现代码如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {@Autowiredprivate TestInterceptor testInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(testInterceptor).addPathPatterns("/**") // 拦截所有地址.excludePathPatterns("/login"); // 放行/login路径}
}
26、如何实现过滤器?
过滤器可以使用 Servlet 3.0 提供的 @WebFilter 注解,配置过滤的 URL 规则,然后再实现 Filter 接口,重写接口中的 doFiter 方法,具体实现代码如下:
@Component
@WebFilter(urlPatterns = "/*")
public class TestFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("过滤器: 执行 init 方法。");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {System.out.println("过滤器: 开始执行 doFilter 方法。");// 请求放行filterChain.doFilter(servletRequest, servletResponse);System.out.println("过滤器: 结束执行 doFilter 方法。");}@Overridepublic void destroy() {System.out.println("过滤器: 执行 destroy 方法。");}
}
其中:
- void init(FilterConfig filterConfig):容器启动(初始化 Filter)时会被调用,整个程序运行期只会被调用一次。用于实现 Fiter 对象的初始化。
- void dofilter(ServletRequest request, ServletResponse response,FilterChain chain):具体的过滤功能实现代码,通过此方法对请求进行过滤处理,其中 FilterChain 参数是用来调用下一个过滤器或执行下一个流程。
- void destroy():用于 Filter 销毁前完成相关资源的回收工作。
27、SpringBoot如何实现跨域?跨域问题的本质是什么?
在 Spring Boot 中跨域问题有很多种解决方案,例如以下5 个:
- 使用 @CrossOrigin 注解实现跨域【局域类跨域】
- 通过配置文件实现跨域【全局跨域】
- 通过 CorsFilter 对象实现跨域【全局跨域】
- 通过 Response 对象实现跨域【局域方法跨域】
- 通过实现 ResponseBodyAdvice 实现跨域 (全局跨域)
这五种方式的代码就不再展示了,毕竟是面试题专栏,搞太多代码也乱。后续有人催更的话我会专门出一篇这五种方法的使用教程
跨域问题的本质是什么?
跨域问题的本质在于浏览器的安全策略,即同源策略。同源策略是为了保护用户数据安全和防止恶意行为而设计的一种安全措施。
所以浏览器为了保证用户的访问安全,防止恶意网站窃取数据,就搞了这个同源策略
而解决跨域问题只需要告诉浏览器,这是一个安全的请求“这是自己人","它的实现是在响应头中加了一个 Access-Control-Allow-Origin 为 “*” 的响应头而已,告诉浏览器“是自己人”。
哈哈,单体架构的项目没想到在相应头里加个东西就好了吧,大道至简
前端包括nginx也都能解决跨域,跨域本身不是我们初中级打工仔该考虑的事情,而且本身也是个极其简单的事情
28、Spring中使用了哪些设计模式?
Spring 中常用的设计模式有以下几个:(主要的,不代表Spring中就用到了七个)
工厂模式:Spring 容器本质是一个大工厂,使用工厂模式通过 BeanFactory、 ApplicationContext 创建 bean 对象。
代理模式:Spring AOP 功能功能就是通过代理模式来实现的,分为动态代理和静态代理。
单例模式:Spring 中的 Bean 默认都是单例的,这样有利于容器对Bean的管理。
模板模式:Spring 中 JdbcTemplate、RestTemplate 等以 Template结尾的对数据库、网络等等进行操作的模板类,就使用到了模板模式。
观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式:Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。
策略模式:Spring中有一个Resource接口,它的不同实现类,会根据不同的策略去访问资源。
这些设计模式不懂得可以去搜搜资料,后续我会出一篇八大设计模式给讲透彻
29、SpringBoot如何实现缓存预热?
SpringBoot 缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制
在 Spring Boot 启动之后,可以通过以下手段实现缓存预热:
- 使用启动监听事件实现缓存预热。
- 使用 @PostConstruct 注解实现缓存预热。
- 使用 CommandLineRunner 或 ApplicationRunner 实现缓存预热。
- 通过实现 InitializingBean 接口,并重写 afterPropertiesSet 方法实现缓存预热。
具体实现方案
①、 启动监听事件
可以使用 ApplicationListener 监听 ContextRefreshedEvent 或 ApplicationReadvEvent 等应用上下文初始化完成事件,在这些事件触发后执行数据加载到缓存的操作,具体实现如下:
@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {@Autowiredprivate CacheManager cacheManager;@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}
或监听 ApplicationReadyEvent 事件,如下代码所示:
@Component
public class CacheWarmer implements ApplicationListener<ApplicationReadyEvent> {@Autowiredprivate CacheManager cacheManager;@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 执行缓存预热业务...cacheManager.put("key", dataList);}
}
ContextRefreshedEvent Vs ApplicationReadyEvent
ContextRefreshedEvent 和 ApplicationReadyEvent 发生的时间和应用场景略有不同:
- ContextRefreshedEvent(上下文刷新事件):当 ApplicationContext 容器初始化或刷新时触发该事件,这个事件在 Bean 的初始化方法之前触发,意味着在该事件完成之前,Bean 已经完成实例化和配置了。(Bean在初始化之前就要实例化,不懂得去看15、Bean的生命周期)
- ApplicationReadyEvent(应用就绪事件):当 Spring Boot 应用完全启动并准备接受请求时触发该事件。在该事件触发时,Spring Boot 应用的所有初始化工作已完成,包括 Bean 的实例化、依赖注入等。这个事件通常用于在应用启动完成后执行一些特定的操作,比如启动定时任务、加载缓存数据等。
所以说,ContextRefreshedEvent 事件被触发时,Spring容器中的 Bean 已经准备好了,但应用可能还没有完全启动,尚未准备好接收请求。而 ApplicationReadyEvent 事件则表示应用已经完全启动并准备好处理请求。
②、@PostConstruct
在需要进行缓存预热的类上添加 @Component 注解,并在其方法中添加 @Postconstruct 注解和缓存预热的业务逻辑,具体实现代码如下:
@Component
public class CachePreloader {@Autowiredprivate CacheManager cacheManager;@PostConstructpublic void preloadCache() {// 执行缓存预热业务cacheManager.getCache("yourCacheName").put("key", dataList);}
}
③、commandLineRunner 或 ApplicationRunner
CommandLineRunner 和 ApplicationRunner 都是 Spring Boot 应用程序启动后要执行的接口,它们都允许我们在应用启动后执行一些自定义的初始化逻辑,例如缓存预热。CommandLineRunner实现不例如下:
@Component
public class MyCommandLineRunner implements CommandLineRunner {@Autowiredprivate CacheManager cacheManager;@Overridepublic void run(String... args) throws Exception {// 执行缓存预热业务cacheManager.getCache("yourCacheName").put("key", dataList);System.out.println("Cache preloading completed.");}
}
ApplicationRunner 实现示例如下:
@Component
public class MyApplicationRunner implements ApplicationRunner {@Autowiredprivate CacheManager cacheManager;@Overridepublic void run(ApplicationArguments args) throws Exception {// 执行缓存预热业务cacheManager.getCache("yourCacheName").put("key", dataList);System.out.println("Cache preloading completed.");}
}
CommandLineRunner 和 ApplicationRunner 区别如下:
1、方法签名不同:
- CommandlineRunner 接口有一个 run(String. args) 方法,它接收命令行参数作为可变长度字符串数组。
- ApplicationRunner 接口则提供了一个 run(ApplicationArguments args)方法,它接收一个。ApplicationArguments 对象作为参数,这个对象提供了对传入的所有命令行参数(包括选项和非选项参数)的访问。
2、参数解析方式不同:
- CommandLineRunner 接囗更简单直接,适合处理简单的命令行参数。
- ApplicationRunner 接口提供了一种更强大的参数解析能力,可以通过 ApplicationArquments 获取详细的参数信息,比如获取选项参数及其值、非选项参数列表以及查询是否存在特定参数等。
3、使用场景不同:
- 当只需要处理一组简单的命令行参数时,可以使用 CommandLineRunner。
- 对于需要精细控制和解析命令行参数的复杂场景,推荐使用 ApplicationRunner。
④、实现InitializingBean接口
实现 InitializingBean 接口并重写 afterPropertiesSet 方法,可以在 Spring Bean 初始化完成后执行缓存预热,具体实现代码如下:
@Component
public class CachePreloader implements InitializingBean {@Autowiredprivate CacheManager cacheManager;@Overridepublic void afterPropertiesSet() throws Exception {// 执行缓存预热业务cacheManager.getCache("yourCacheName").put("key", dataList);System.out.println("Cache preloading completed.");}
}
总结
Spring Boot 缓存预热是指在 Spring Boot 项日启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。它可以通过监听 ContextRefreshedEvent 或 ApplicationReadyEvent 启动事件,或使用 @PostConstruct 注解,或实现 CommandLineRunner 接口、ApplicationRunner 接囗,和 InitializingBean 接口的方式来完成。
30、Spring如何解决循环依赖问题?什么是三级缓存?为什么一定要用三级缓存?
注:看27前先把15Bean的生命周期看了
循环依赖问题指的是在对象之间存在相互依赖关系,形成一个闭环,导致无法准确地完成对象的创建和初始化。当两个或多个对象彼此之间相互引用,而这种相互引用形成一个循环时,就可能出现循环依赖问题。
例如以下这两种情况:
@Service
public class ServiceA {@Autowiredprivate ServiceB serviceB;}@Service
public class ServiceB {@Autowiredprivate ServiceA serviceA;}
如以上情况,两个Bean就发生了互相依赖
什么是三级缓存?
Spring 是通过三级缓存来解决循环依赖问题的,所谓的三级缓存,其实就是 Spring 中定义的三个 Map 集合,源码如下图所示:
这三个级别的缓存分别是:(耐心读,多加思考,理清楚)
单例对象缓存(singletonObject):Spring 中的一级缓存,用于存储完全创建好的单例 bean 对象。在创建单例 bean 时,会先从一级缓存中尝试获取该bean的实例,如果能够获取到,则直接返回该实例,否则继续创建该bean
提前暴露的对象缓存(earlySingletonObjects):Spring 中的二级缓存,用于存储已经创建但还未完全初始化的 bean 对象。如果发现该bean存在循环依赖,则先会创建该bean的“半成品”对象,并将“半成品”对象存入二级缓存中,以便其他bean可以引用它(其实只要三级缓存中有任意一级缓存有bean,那么其他依赖该bean的对象都可以引用该bean去创建自身)。当循环依赖的bean创建完成后,Spring会将完整的bean实例对象存储到一级缓存中,这样可以保证单例bean的创建过程不会出现循环依赖问题。
代理对象缓存(singletonFactory):Spring 中的三级缓存,用于存储单例 bean 的创建工厂。当一个单例bean 被创建时,Spring 会先将该 bean 的创建工厂存储到三级缓存中,然后再执行创建工厂的getObject()方法,生成该 bean 的实例对象。在该 bean 被其他 bean 引用时,Spring 会从三级缓存中获取该bean的创建工厂,创建出该 bean 的实例对象,并将该 bean 的实例对象存储到一级缓存中
如何解决循环依赖?
如下图:
其实这里我给大家一个简单的思路,现在有且仅有两杯热水,如何让他快速地达到能喝的温度,很简单,再拿一个空杯子,然后三个杯子来回颠倒倒茶
解决循环依赖的前提条件:
- 互相依赖的 Bean 必须要是单例的 Bean
- 依赖注入的方式不能都是构造函数注入的方式
为什么一定要使用三级缓存?
总结:其实,使用二级缓存也能解决循环依赖的问题,但是如果完全依靠二级缓存解决循环依赖,在AOP代理的应用场景下,会违反AOP的代理设计原则,例如实现对象初始化和生成代理对象的解耦
首先,一级缓存一定是要的,因为 Bean 是单例模式,需要存放到某个容器中,而一级缓存就是这个缓存容器。
二级缓存可以不要(看完),我们可以把半成品 Bean(提前暴露的 Bean)放到一级缓存中,但这样需要给一级缓存中添加标识,标识那些是完整对象,那些是半成品对象,这样有几个问题:
- 增加了 Spring(源码)设计的复杂性
- 在查询时,需要先判断标识,查询效率变低了
- 违反单一设计原则
因此为了解决这些个问题,二级缓存也是必须要的
三级缓存也可以不要,但这样也有一个问题,那就是 Spring 的设计模式中,它在生成代理时,为了实现对象初始化和生成代理对象的解耦,所以代理对象是在 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器的最后一步生成 AOP 代理对象的。如果不要三级缓存,那么我们需要在所有类创建之前,先将代理类创建出来这样在遇到循环依赖就可以直接拿出代理对象来使用了,但这种方式的缺点是打破了原来 Spring 的设计理念(实现对象初始化和生成代理对象的解耦)。
所以最优的方案是,不提前创建代理对象,而是使用三级缓存存储创建对象的表达式,等遇到循环依赖,再按照 Spring 的设计模式来生成代理对象。
所以,综合来看,Spring 使用三级缓存并非必须之举,但使用三级缓存是 Spring 解决循环依赖的最优解决方案,它能保证高效性和模式上的统一性。
注:三级缓存的理由有些抽象,有人没看懂的话评论区留言我再整整
31、SpringBoot的启动流程是怎么样的?
这里说的比较精简好理解,如果想看长篇大论的细节,可以搜一下看其他文章,这类文章非常非常多
@SpringBootApplication
public class Application {public static void main(String[] args) {// 这里调用静态方法 run 来启动 Spring Boot 应用SpringApplication.run(Application.class, args);}
}
- 首先从main找到run()方法,在执行run()方法之前new一个SpringApplication对象
- 进入run()方法,创建应用监听器SpringApplicationRunListeners开始监听
- 然后加载SpringBoot配置环境(ConfigurableEnvironment),然后把配置环境(Environment)加入监听对象中
- 然后加载应用上下文(ConfigurableApplicationContext),当做run方法的返回对象
- 最后创建Spring容器,refreshContext(context),实现starter自动化配置和bean的实例化等工作
32、Spring的自动装配(自动配置)
通过
@EnableAutoConfiguration
注解在类路径的META-INF/spring.factories文件中找到所有的对应配置类,然后将这些自动配置类加载到spring容器中。
什么是Spring的自动配置?
简单来说就是用注解来对一些常规的配置做默认配置,简化xml配置内容,使你的项目能够快速运行。
其实正常情况下,我们要想连接数据库,需要指定很多东西的,包括数据源,连接池等等,但很多时候我们自己写的项目中,在xml中往往三行就搞定了数据库的连接,指定一下地址,账号,密码等就可以了。
那么那些理论上同样必要的繁琐的配置哪里去了呢,就是Spring通过注解来做了一些默认的常规配置,简化了我们xml的内容,使我们的项目能够快速运行
@EnableAutoConfiguration注解说明:
好像我们没见过这个注解,其实有的,而且我们根本离不开它。
在启动类中我们可以看到@SpringBootApplication注解,它是SpringBoot的核心注解,也是一个组合注解。
进入这个注解后,我们可以看到里面又包含了其他很多注解,其中就有@EnableAutoConfiguration注解