Skip to content

本模块适合 Java后端研发(业务岗、架构岗)的同学。

讲讲 SpringBoot 的优点

SpringBoot 的优点主要有以下几点:

  1. 简化开发:SpringBoot 提供了自动配置、快速启动、依赖管理等功能,简化了开发流程,提高了开发效率。
  2. 零配置:SpringBoot 采用约定大于配置的方式,减少了配置文件,提高了代码的可读性、可维护性、可扩展性。
  3. 内嵌服务器:SpringBoot 内置了 Tomcat、Jetty、Undertow 等服务器,方便部署和运行。
  4. 微服务架构:SpringBoot 支持微服务架构,提供了 RESTful API、消息队列、服务注册、服务发现等功能,适合微服务架构。
  5. 生态系统:SpringBoot 有庞大的生态系统,提供了大量的插件、工具、框架等,方便开发和部署。

为什么选择 SpringBoot,不选择 Nodejs?(简历写全栈的同学必定会被问到)

SpringBoot 是一种 Java 框架,主要用于开发企业级应用,支持事务管理、安全认证、数据访问等,适合大型项目。

Nodejs 是一种 JavaScript 框架,主要用于开发 Web 应用,支持异步 IO、事件驱动、非阻塞 IO 等,适合小型项目。

选择 SpringBoot 的主要原因有以下几点:

  1. Java 生态系统:SpringBoot 是基于 Java 生态系统,有庞大的生态系统,提供了大量的插件、工具、框架等,方便开发和部署。
  2. 企业级应用:SpringBoot 支持事务管理、安全认证、数据访问等,适合开发大型项目,提高代码的可读性、可维护性、可扩展性。
  3. 性能稳定:SpringBoot 是基于 Java 虚拟机,性能稳定,适合高并发场景,提高系统的性能和可靠性。

因此,根据不同的需求选择合适的框架,SpringBoot 适合开发大型项目,Nodejs 适合开发小型项目。

讲讲 SpringBoot 的自动装配原理

SpringBoot 的自动装配原理主要有以下几个步骤:

  1. SpringBoot 启动时,会扫描类路径,查找带有 @SpringBootApplication 注解的主类。
  2. @SpringBootApplication 注解是一个组合注解,包含了 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan
    • @SpringBootConfiguration:标识这是一个 SpringBoot 配置类。
    • @EnableAutoConfiguration:启用 SpringBoot 的自动装配机制。
    • @ComponentScan:启用组件扫描,自动发现并注册 Spring 组件。
  3. @EnableAutoConfiguration 注解会触发 SpringFactoriesLoader 加载 META-INF/spring.factories 文件中配置的自动装配类。
  4. 自动装配类通常使用 @Conditional 注解,根据特定条件(如类路径中是否存在某个类、某个配置属性是否存在等)决定是否装配某个 Bean。
  5. spring-boot-autoconfigure 模块中包含了大量的自动配置类,这些类根据应用的环境和配置自动装配所需的 Bean。

SpringBoot 的自动装配原理是为了简化开发流程,提高开发效率,减少配置文件,提高代码的可读性、可维护性、可扩展性。

什么叫 AOP,SpringBoot 底层如何实现的 AOP?

AOP(Aspect-Oriented Programming)是一种面向切面编程的方法,亦是代理模式的直接实现。

AOP 提供另一个角度来考虑程序的结构,主要是为了解耦业务逻辑和横切关注点,提高代码的可读性、可维护性、可扩展性。

使用 AOP 的好处包括:

  1. 代码重用:将横切关注点抽象成切面,提高代码的可重用性。
  2. 代码解耦:将业务逻辑和横切关注点解耦,提高代码的可维护性。
  3. 代码扩展:将横切关注点动态织入业务逻辑,提高代码的可扩展性。

AOP 主要包含如下几个概念:

  1. 切面(Aspect):横切关注点,包括通知、切点、连接点等。
  2. 通知(Advice):在连接点上执行的动作,包括前置通知、后置通知、环绕通知、异常通知、最终通知等。
  3. 切点(Pointcut):连接点的集合,用于匹配连接点。
  4. 连接点(Joinpoint):程序执行的某个特定点,如方法调用、方法返回、方法异常等。
  5. 织入(Weaving):将切面应用到目标对象的过程,包括编译时织入、类加载时织入、运行时织入等。
  6. 代理(Proxy):代理对象,用于控制对目标对象的访问,包括静态代理、动态代理、CGLIB 代理等。
  7. 目标对象(Target):被代理的对象,包括业务逻辑、数据访问等。

实现 AOP 主要包含 JDK 动态代理和 CGLIB 动态代理两种方式:

  1. JDK 动态代理:基于接口的代理,通过java.lang.reflect.Proxy类实现,要求目标对象必须实现接口。
  2. CGLIB 动态代理:基于类的代理,通过net.sf.cglib.proxy.Enhancer类实现,不要求目标对象必须实现接口。

在 SpringBoot 中实现 AOP,一般需要经过以下几个步骤:

  1. 定义切面和通知
    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");
        }
    }
  2. 配置 Spring AOP(由 SpringBoot 自动完成)
  3. 使用目标对象
    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");
        }
    }
  4. 启动 SpringBoot 应用

此时,当调用UserServiceaddUser方法时,会自动执行LoggingAspectlogBeforeMethod方法。

通过这种方式,我们可以很轻易地为目标对象添加横切关注点,提高代码的可读性、可维护性、可扩展性。

什么是 IOC 和 DI?

IOC(Inversion of Control)是一种控制反转的思想,主要是将对象的创建和管理交给容器。

DI(Dependency Injection)是一种依赖注入的方式,主要是将对象的依赖关系交给容器。

实际上很多人认为 IOC 和 DI 是一码事,因为 IOC 是 DI 的一种实现方式,但是从概念上来说,IOC 是一种思想,DI 是一种方式,目的都是为了解耦对象的创建和使用,提高代码的可读性、可维护性、可扩展性。

SpringBoot 的 IOC 和 DI 主要有以下几个概念:

  1. Bean:SpringBoot 的核心对象,由容器管理,包括配置类、组件类、代理类等。
  2. Configuration:SpringBoot 的配置类,用于配置 Bean 的创建和管理,包括@Configuration@ComponentScan@EnableAutoConfiguration等。
  3. Component:SpringBoot 的组件类,用于定义 Bean 的依赖关系,包括@Component@Service@Repository@Controller等。
  4. Autowired:SpringBoot 的依赖注入,用于注入 Bean 的依赖关系,包括@Autowired@Qualifier@Resource@Inject等。
  5. BeanFactory:SpringBoot 的容器类,用于管理 Bean 的创建和管理,包括BeanFactoryApplicationContextAnnotationConfigApplicationContext等。

SpringBoot 的 IOC 和 DI 主要是为了解耦对象的创建和使用,提高代码的可读性、可维护性、可扩展性,根据不同的需求选择合适的配置类、组件类、代理类等。

讲讲 SpringBoot 的启动流程

SpringBoot 的启动流程主要有以下几个步骤:

  1. 加载配置:SpringBoot 会加载application.propertiesapplication.yml等配置文件,读取配置信息,初始化配置类。
  2. 扫描注解:SpringBoot 会扫描@SpringBootApplication@Configuration@ComponentScan@EnableAutoConfiguration等注解,初始化配置类。
  3. 自动装配:SpringBoot 会根据@EnableAutoConfiguration注解的配置,自动装配spring-boot-autoconfigure模块中的配置类,初始化 Bean 对象。
  4. 启动容器:SpringBoot 会启动TomcatJettyUndertow等服务器,监听端口,接收请求,处理响应。
  5. 处理请求:SpringBoot 会根据@Controller@RestController@RequestMapping等注解,处理请求,返回响应。

SpringBoot 的启动流程是为了简化开发流程,提高开发效率,减少配置文件,提高代码的可读性、可维护性、可扩展性。

什么是 Starter?

Starter 是 SpringBoot 的一个插件,本质是对依赖的集合,主要是为了简化依赖管理,提高代码的可读性、可维护性、可扩展性。

Starter 主要包含如下几个概念:

  1. Starter:SpringBoot 的插件,用于管理依赖关系,提供一组依赖的集合,方便开发和部署。例如,spring-boot-starter-web 包含了构建 Web 应用所需的常用依赖。
  2. Autoconfigure:SpringBoot 的自动配置机制,用于根据类路径中的依赖和自定义配置自动装配 Spring Bean,简化配置过程。
  3. Starter Parent:SpringBoot 的父项目,提供了一组默认的依赖管理和插件配置,简化了项目的构建配置。例如,spring-boot-starter-parent 提供了常用的依赖版本管理和插件配置。
  4. Starter Test:SpringBoot 的测试插件,包含了常用的测试依赖和配置,方便进行单元测试和集成测试。例如,spring-boot-starter-test 包含了 JUnit、Mockito 等常用测试框架。

Starter 是为了简化依赖管理,提高代码的可读性、可维护性、可扩展性,根据不同的需求选择合适的 Starter。

值得注意的是,如果是官方的 Starter,一般是以spring-boot-starter-开头,如spring-boot-starter-webspring-boot-starter-data-jpa等,如果是三方的 Starter,一般格式是xxx-spring-boot-starter,如mybatis-spring-boot-starterdubbo-spring-boot-starter等。

SpringBoot 在注入时如何解决循环依赖?(不常考,但如果说自己读过源码,必问)

循环依赖是一个非常棘手的问题,SpringBoot 是通过三级缓存解决的,具体来说,有如下三级缓存:

  1. singletonObjects:存放完全实例化好的 Bean,可以直接使用。类型是ConcurrentHashMap<String, Object>
  2. earlySingletonObjects:存放早期暴露出来的 Bean,还没有完成属性注入和初始化的 Bean,但是可以提前暴露出来给其他 Bean 使用。类型是ConcurrentHashMap<String, Object>
  3. 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 对应的类型的父类和接口,维护了allBeanNamesByTypesingletonBeanNamesByType两个 Map,类型是ConcurrentHashMap<String, String[]>,key 是 Bean 的类型,包括接口和父类,value 是 Bean 的名称数组。在 Bean 刚刚实例化的时候,会将 Bean 的类型和名称,以及整个继承链和实现链上的类型和名称都放入这两个 Map 中。

在依赖注入阶段,如果三级缓存都拿不到对应的 Bean,就意味着这个注入是基于类型的,那么就会从这两个 Map 中找到对应的 Bean 名称数组,然后又从一级缓存中找到对应的 Bean,进行注入。

这样就实现了基于里氏替换原则的类型注入,时间复杂度从 O(mn) 降低到了近乎 O(1)(因为哈希表会出现哈希冲突导致在链表或红黑树上查找,所以不是完全的 O(1))。