本模块适合 Java后端研发(业务岗、架构岗)
的同学。
讲讲 SpringBoot 的优点
SpringBoot 的优点主要有以下几点:
- 简化开发:SpringBoot 提供了自动配置、快速启动、依赖管理等功能,简化了开发流程,提高了开发效率。
- 零配置:SpringBoot 采用约定大于配置的方式,减少了配置文件,提高了代码的可读性、可维护性、可扩展性。
- 内嵌服务器:SpringBoot 内置了 Tomcat、Jetty、Undertow 等服务器,方便部署和运行。
- 微服务架构:SpringBoot 支持微服务架构,提供了 RESTful API、消息队列、服务注册、服务发现等功能,适合微服务架构。
- 生态系统:SpringBoot 有庞大的生态系统,提供了大量的插件、工具、框架等,方便开发和部署。
为什么选择 SpringBoot,不选择 Nodejs?(简历写全栈的同学必定会被问到)
SpringBoot 是一种 Java 框架,主要用于开发企业级应用,支持事务管理、安全认证、数据访问等,适合大型项目。
Nodejs 是一种 JavaScript 框架,主要用于开发 Web 应用,支持异步 IO、事件驱动、非阻塞 IO 等,适合小型项目。
选择 SpringBoot 的主要原因有以下几点:
- Java 生态系统:SpringBoot 是基于 Java 生态系统,有庞大的生态系统,提供了大量的插件、工具、框架等,方便开发和部署。
- 企业级应用:SpringBoot 支持事务管理、安全认证、数据访问等,适合开发大型项目,提高代码的可读性、可维护性、可扩展性。
- 性能稳定:SpringBoot 是基于 Java 虚拟机,性能稳定,适合高并发场景,提高系统的性能和可靠性。
因此,根据不同的需求选择合适的框架,SpringBoot 适合开发大型项目,Nodejs 适合开发小型项目。
讲讲 SpringBoot 的自动装配原理
SpringBoot 的自动装配原理主要有以下几个步骤:
- SpringBoot 启动时,会扫描类路径,查找带有
@SpringBootApplication
注解的主类。 @SpringBootApplication
注解是一个组合注解,包含了@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
。@SpringBootConfiguration
:标识这是一个 SpringBoot 配置类。@EnableAutoConfiguration
:启用 SpringBoot 的自动装配机制。@ComponentScan
:启用组件扫描,自动发现并注册 Spring 组件。
@EnableAutoConfiguration
注解会触发SpringFactoriesLoader
加载META-INF/spring.factories
文件中配置的自动装配类。- 自动装配类通常使用
@Conditional
注解,根据特定条件(如类路径中是否存在某个类、某个配置属性是否存在等)决定是否装配某个 Bean。 spring-boot-autoconfigure
模块中包含了大量的自动配置类,这些类根据应用的环境和配置自动装配所需的 Bean。
SpringBoot 的自动装配原理是为了简化开发流程,提高开发效率,减少配置文件,提高代码的可读性、可维护性、可扩展性。
什么叫 AOP,SpringBoot 底层如何实现的 AOP?
AOP(Aspect-Oriented Programming)是一种面向切面编程的方法,亦是代理模式的直接实现。
AOP 提供另一个角度来考虑程序的结构,主要是为了解耦业务逻辑和横切关注点,提高代码的可读性、可维护性、可扩展性。
使用 AOP 的好处包括:
- 代码重用:将横切关注点抽象成切面,提高代码的可重用性。
- 代码解耦:将业务逻辑和横切关注点解耦,提高代码的可维护性。
- 代码扩展:将横切关注点动态织入业务逻辑,提高代码的可扩展性。
AOP 主要包含如下几个概念:
- 切面(Aspect):横切关注点,包括通知、切点、连接点等。
- 通知(Advice):在连接点上执行的动作,包括前置通知、后置通知、环绕通知、异常通知、最终通知等。
- 切点(Pointcut):连接点的集合,用于匹配连接点。
- 连接点(Joinpoint):程序执行的某个特定点,如方法调用、方法返回、方法异常等。
- 织入(Weaving):将切面应用到目标对象的过程,包括编译时织入、类加载时织入、运行时织入等。
- 代理(Proxy):代理对象,用于控制对目标对象的访问,包括静态代理、动态代理、CGLIB 代理等。
- 目标对象(Target):被代理的对象,包括业务逻辑、数据访问等。
实现 AOP 主要包含 JDK 动态代理和 CGLIB 动态代理两种方式:
- JDK 动态代理:基于接口的代理,通过
java.lang.reflect.Proxy
类实现,要求目标对象必须实现接口。 - CGLIB 动态代理:基于类的代理,通过
net.sf.cglib.proxy.Enhancer
类实现,不要求目标对象必须实现接口。
在 SpringBoot 中实现 AOP,一般需要经过以下几个步骤:
- 定义切面和通知java
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBeforeMethod() { System.out.println("Method execution started"); } }
- 配置 Spring AOP(由 SpringBoot 自动完成)
- 使用目标对象java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { public void addUser() { System.out.println("User added"); } }
- 启动 SpringBoot 应用
此时,当调用UserService
的addUser
方法时,会自动执行LoggingAspect
的logBeforeMethod
方法。
通过这种方式,我们可以很轻易地为目标对象添加横切关注点,提高代码的可读性、可维护性、可扩展性。
什么是 IOC 和 DI?
IOC(Inversion of Control)是一种控制反转的思想,主要是将对象的创建和管理交给容器。
DI(Dependency Injection)是一种依赖注入的方式,主要是将对象的依赖关系交给容器。
实际上很多人认为 IOC 和 DI 是一码事,因为 IOC 是 DI 的一种实现方式,但是从概念上来说,IOC 是一种思想,DI 是一种方式,目的都是为了解耦对象的创建和使用,提高代码的可读性、可维护性、可扩展性。
SpringBoot 的 IOC 和 DI 主要有以下几个概念:
- Bean:SpringBoot 的核心对象,由容器管理,包括配置类、组件类、代理类等。
- Configuration:SpringBoot 的配置类,用于配置 Bean 的创建和管理,包括
@Configuration
、@ComponentScan
、@EnableAutoConfiguration
等。 - Component:SpringBoot 的组件类,用于定义 Bean 的依赖关系,包括
@Component
、@Service
、@Repository
、@Controller
等。 - Autowired:SpringBoot 的依赖注入,用于注入 Bean 的依赖关系,包括
@Autowired
、@Qualifier
、@Resource
、@Inject
等。 - BeanFactory:SpringBoot 的容器类,用于管理 Bean 的创建和管理,包括
BeanFactory
、ApplicationContext
、AnnotationConfigApplicationContext
等。
SpringBoot 的 IOC 和 DI 主要是为了解耦对象的创建和使用,提高代码的可读性、可维护性、可扩展性,根据不同的需求选择合适的配置类、组件类、代理类等。
讲讲 SpringBoot 的启动流程
SpringBoot 的启动流程主要有以下几个步骤:
- 加载配置:SpringBoot 会加载
application.properties
、application.yml
等配置文件,读取配置信息,初始化配置类。 - 扫描注解:SpringBoot 会扫描
@SpringBootApplication
、@Configuration
、@ComponentScan
、@EnableAutoConfiguration
等注解,初始化配置类。 - 自动装配:SpringBoot 会根据
@EnableAutoConfiguration
注解的配置,自动装配spring-boot-autoconfigure
模块中的配置类,初始化 Bean 对象。 - 启动容器:SpringBoot 会启动
Tomcat
、Jetty
、Undertow
等服务器,监听端口,接收请求,处理响应。 - 处理请求:SpringBoot 会根据
@Controller
、@RestController
、@RequestMapping
等注解,处理请求,返回响应。
SpringBoot 的启动流程是为了简化开发流程,提高开发效率,减少配置文件,提高代码的可读性、可维护性、可扩展性。
什么是 Starter?
Starter 是 SpringBoot 的一个插件,本质是对依赖的集合,主要是为了简化依赖管理,提高代码的可读性、可维护性、可扩展性。
Starter 主要包含如下几个概念:
- Starter:SpringBoot 的插件,用于管理依赖关系,提供一组依赖的集合,方便开发和部署。例如,
spring-boot-starter-web
包含了构建 Web 应用所需的常用依赖。 - Autoconfigure:SpringBoot 的自动配置机制,用于根据类路径中的依赖和自定义配置自动装配 Spring Bean,简化配置过程。
- Starter Parent:SpringBoot 的父项目,提供了一组默认的依赖管理和插件配置,简化了项目的构建配置。例如,
spring-boot-starter-parent
提供了常用的依赖版本管理和插件配置。 - Starter Test:SpringBoot 的测试插件,包含了常用的测试依赖和配置,方便进行单元测试和集成测试。例如,
spring-boot-starter-test
包含了 JUnit、Mockito 等常用测试框架。
Starter 是为了简化依赖管理,提高代码的可读性、可维护性、可扩展性,根据不同的需求选择合适的 Starter。
值得注意的是,如果是官方的 Starter,一般是以spring-boot-starter-
开头,如spring-boot-starter-web
、spring-boot-starter-data-jpa
等,如果是三方的 Starter,一般格式是xxx-spring-boot-starter
,如mybatis-spring-boot-starter
、dubbo-spring-boot-starter
等。
SpringBoot 在注入时如何解决循环依赖?(不常考,但如果说自己读过源码,必问)
循环依赖是一个非常棘手的问题,SpringBoot 是通过三级缓存解决的,具体来说,有如下三级缓存:
- singletonObjects:存放完全实例化好的 Bean,可以直接使用。类型是
ConcurrentHashMap<String, Object>
。 - earlySingletonObjects:存放早期暴露出来的 Bean,还没有完成属性注入和初始化的 Bean,但是可以提前暴露出来给其他 Bean 使用。类型是
ConcurrentHashMap<String, Object>
。 - singletonFactories:存放未完成属性注入和初始化的 Bean 的工厂,用于解决循环依赖问题。类型是
ConcurrentHashMap<String, ObjectFactory<?>>
。
在注册和依赖注入过程中,SpringBoot 首先会调用构造函数创建 Bean 的实例。创建完之后,会先将 Bean 的名称作为 Key,一个获得提前暴露的半成品 Bean 的工厂方法作为 value 放入三级框架中。注册完成后,进入属性注入阶段。
正常情况下,属性注入会先从一级缓存中获取 Bean,如果没有,再从二级缓存中获取 Bean,如果还没有,一般就出现了循环依赖问题,直接从三级缓存中获取工厂方法,得到半成品,移入二级缓存,删除三级缓存当中对应的方法,再进行属性注入。这个过程中,如果对象允许被创建代理,那么放入二级缓存的是代理对象。
完成注入后,再将 Bean 移入一级缓存,删除二级和三级缓存中对应的内容,这就是总体的注册和依赖注入过程。
SpringBoot 底层是如何解决基于里氏替换原则实现类型注入的?(不常考,但如果说自己读过源码,必问)
先说说里氏替换原则,里氏替换原则是面向对象设计的一个重要原则,主要是为了保证子类可以替换父类,提高代码的可读性、可维护性、可扩展性。
简单可以理解为,如果 A 是 B 的子类,那么在任何使用 B 的地方,都可以替换成 A,而不影响程序的正确性,即大装小,如:List<Number> list = new ArrayList<Integer>();
。
既然要实现基于里氏替换原则的类型注入,就意味着,我们必须从 Bean 对应类型的实现链和继承链中找到对应的类型,然后再进行注入。
但这样会导致每次类型注入都要遍历整个所有 Bean 的实现链和继承链,如果有 m 个字段,n 个 Bean,那么复杂度就是 O(mn),这种设计是不可取的。
SpringBoot 为了定位 Bean 对应的类型的父类和接口,维护了allBeanNamesByType
和singletonBeanNamesByType
两个 Map,类型是ConcurrentHashMap<String, String[]>
,key 是 Bean 的类型,包括接口和父类,value 是 Bean 的名称数组。在 Bean 刚刚实例化的时候,会将 Bean 的类型和名称,以及整个继承链和实现链上的类型和名称都放入这两个 Map 中。
在依赖注入阶段,如果三级缓存都拿不到对应的 Bean,就意味着这个注入是基于类型的,那么就会从这两个 Map 中找到对应的 Bean 名称数组,然后又从一级缓存中找到对应的 Bean,进行注入。
这样就实现了基于里氏替换原则的类型注入,时间复杂度从 O(mn) 降低到了近乎 O(1)(因为哈希表会出现哈希冲突导致在链表或红黑树上查找,所以不是完全的 O(1))。