Spring Boot 原理

介绍一下 Spring Boot 整体的启动流程?

Spring Boot 启动流程的核心是通过自动配置和上下文初始化,将零散的组件装配成一个可运行的应用上下文(ApplicationContext),整体分为 前置准备、上下文初始化、核心刷新、启动收尾 四大步

  1. 首先从 main 找到 SpringApplication.run(主类.class, args) 方法,在执行 run() 方法之前先 new 一个 SpringApplication 实例对象。

    实例化时做的具体核心内容:

    • 然后进行应用类型推断,判断当前应用是 Servlet 应用(Spring Web MVC)、Reactive 应用(Spring WebFlux)还是普通非 Web 应用。

    • 紧接着进行初始化器加载,从 META-INF/spring.factories 中加载所有 ApplicationContextInitializer 的实现类,用于在上下文刷新前进行自定义配置,包括 Spring Boot 配置的默认实现和开发者添加的自定义实现。

    • 接着加载所有实现ApplicationListener的类,监听启动过程中的应用生命周期事件

      进入 SpringApplication.run()启动事件监听机制,创建 SpringApplicationRunListeners 对象,它是所有监听器的 总调度器,发布 ApplicationStartingEvent 事件,通知所有监听器“应用要开始启动了”。监听器可以做一些前置工作

    • 推断出主启动类

    所以说这一步是启动前的资源预先加载的步骤

  2. 然后加载 Spring Boot 配置环境 ConfigurableEnviroment,把配置环境 Enviroment 加入监听对象中

    这一步是环境准备,为整个应用提供配置来源,是配置中心初始化,把所有外部配置统一抽象成 Environment,供后续上下文和 Bean 使用。

  3. 然后进行上下文初始化,创建应用上下文 ConfigurableApplicationContext,当作 run 方法的返回对象。这里会根据推断的应用类型,创建对应类型的上下文,然后调用所有加载的 Initializer 对上下文进行定制化配置,通知所有监听器应用开始启动。

    这一步是容器的 空壳创建,只完成了基础结构搭建,还没开始加载 Bean。

  4. 最后创建 Spring 容器,调用 refreshContext(context)进行容器上下文刷新,实现 starter 自动化配置和 Bean 加载和实例化,发布 ApplicationReadyEvent通知应用就绪

Spring Boot 只是在 Spring refresh() 之前加了自动配置、环境准备、内嵌容器等封装,核心容器逻辑还是 Spring 原生的。

Spring Boot 和 Spring 有什么区别?讲解一下 Spring Boot 的核心优势?为什么要用 Spring Boot?

Spring Boot 是基于 Spring 框架的快速开发脚手架,核心是 “约定大于配置”。

解决了 Spring 框架配置繁琐、依赖管理复杂的问题;而 Spring 是一套完整的企业级开发框架,核心是 IOC(控制反转)和 AOP(面向切面编程),提供基础的核心能力,但需要大量手动配置。

简单说:Spring 是 基础能力库,Spring Boot 是 基于 Spring 的高效的开发脚手架。

核心特点如下

  1. 约定大于配置

    Spring Boot 内置了大量默认配置,而且无需手动编写 XML 配置即可启动,而且使用注解就可以进行配置,比如开发一个 Web 项目,只需引入spring-boot-starter-web依赖,就会自动配置一系列框架

  2. 起步依赖 Starter

    将常用的依赖组合打包成 Starter,面对一个开发场景,直接引入 starter 就可以包含一系列的开发依赖

  3. 自动配置并且简化配置

    基于条件注解@Conditional等,根据类路径下的依赖、配置文件等自动创建 Bean

    而且不用各种复杂的 XML 配置,使用配置文件加上注解,可以让配置很轻松,把更多精力放在业务逻辑开发上

  4. 内嵌服务器

    内置 Tomcat、Jetty、Undertow 等 Web 服务器,无需手动部署 WAR 包到外部服务器。项目可直接打成 JAR 包,通过java -jar启动,部署、测试、运维更简单;

  5. 简化监控与运维

总之,使用 Spring Boot,相比传统的Spring MVC,开发效率提升非常明显。

什么是自动装配?SpringBoot自动装配原理是什么?

Spring Boot 的自动装配原理是基于 Spring Framework 的条件化配置和@EnableAutoConfiguration注解实现的。

自动装配就是通过注解或一些简单的配置就可以在 Spring Boot 的帮助下很方便的开启和配置各种功能,比如数据库访问、Web开发。

这种机制允许开发者在项目中引入相关的依赖,Spring Boot 将根据这些依赖自动配置应用程序的上下文和功能。

Spring Boot 定义了一套接口规范,这套规范规定:

  • Spring Boot 在启动时会扫描外部引用 jar 包中的 META-INF/spring.factories 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 Spring Boot 定义的标准,就能将自己的功能装进 Spring Boot。

对于 Spring Boot 自动装配的原理

  • @SpringBootApplication 注解的内部有个@EnableAutoConfiguration, 这个注解是实现自动装配的核心注解,其中这个注解的内容如下

    • @AutoConfigurationPackage,将项目 src 中 main 包下的所有组件注册到容器中,例如标注了 Component 注解的类等

    • @Import({AutoConfigurationImportSelector.class}),是自动装配的核心

      AutoConfigurationImportSelector 是 Spring Boot 中一个重要的类,它实现了 ImportSelector 接口,用于实现自动配置的选择和导入。具体来说,它通过分析项目的类路径,然后进行条件判断,来决定应该导入哪些自动配置类。

拦截器和过滤器的区别?各自的使用场景和执行时机?结合 Spring 框架聊聊

过滤器属于 Servlet 规范,工作在 Web 容器层面,主要处理字符编码、请求日志、跨域等底层 Web 层面的通用逻辑。

拦截器是 Spring MVC 的组件,运行在 Spring 容器内部,在 Controller 方法执行前后进行拦截。

过滤器的生命周期遵循 Servlet 规范的标准流程,过滤器只能访问ServletRequestServletResponse这些原始的 Servlet 对象,它看不到 Spring 的上下文,也无法进行依赖注入。

拦截器是Spring MVC框架的组件,所以它运行在Spring容器内部,不仅可以访问 HttpServletRequestHttpServletResponse,还能通过@Autowired注入任何 Spring 管理的 Bean,甚至可以获取到即将执行的 Controller 方法信息。

在实际开发中有一个简单的判断原则——如果这个功能需要访问Spring容器中的Bean,或者需要处理业务逻辑,就选择拦截器;如果只是对HTTP请求响应做一些通用的预处理,就用过滤器。

例如,过滤器的经典应用场景包括字符编码处理,这是最基础但也最重要的场景。在处理中文参数时,会用过滤器统一设置字符编码,确保所有请求都能正确处理中文字符。CORS跨域处理也是过滤器的典型应用,前后端分离的项目中,经常用过滤器来处理跨域问题,因为跨域本质上是HTTP协议层面的限制,用过滤器处理最合适。

而拦截器的应用场景要突出它与业务逻辑的紧密结合。用户权限校验是最常见的场景,需要调用UserService来验证token,还要根据用户角色判断是否有访问特定接口的权限。

Spring Boot 框架知识

Spring IoC 和 AOP 介绍一下?IoC和AOP是通过什么机制来实现的?

  • IoC:控制反转,它是一种创建和获取对象的技术思想,依赖注入 DI 是实现这种技术的一种方式。

    传统开发过程中,我们需要通过new关键字来创建对象。使用 IoC 思想开发方式的话,我们不通过 new 关键字创建对象,而是通过 IoC 容器来帮我们实例化对象,把创建对象和对象之间的依赖管理的权利从自己身上交给了 Spring 容器,这就是控制反转的含义。 通过 IoC 的方式,可以大大降低对象之间的耦合度。

  • AOP:是面向切面编程,能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。

    Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。

在 Spring 框架中,IOC 和 AOP 结合使用,可以更好地实现代码的模块化和分层管理。例如:

  • 通过 IOC 容器管理对象的依赖关系,然后通过 AOP 将横切关注点统一切入到需要的业务逻辑中。
  • 使用 IOC 容器管理 Service 层和 DAO 层的依赖关系,然后通过 AOP 在 Service 层实现事务管理、日志记录等横切功能,使得业务逻辑更加清晰和可维护

Spring IOC 实现机制

  • 反射:Spring IOC容器利用 Java 的反射机制动态地加载类、创建对象实例及调用对象方法,反射允许在运行时检查类、方法、属性等信息,从而实现灵活的对象实例化和管理。
  • 依赖注入:IoC 的核心概念是依赖注入,即容器负责管理应用程序组件之间的依赖关系。Spring 通过构造函数注入、属性注入或方法注入,将组件之间的依赖关系描述在配置文件中或使用注解。
  • 设计模式 - 工厂模式:Spring IoC 容器通常采用工厂模式来管理对象的创建和生命周期。容器作为工厂负责实例化Bean 并管理它们的生命周期,将 Bean 的实例化过程交给容器来管理。
  • 容器实现:Spring IoC 容器是实现 IoC 的核心,通常使用 BeanFactoryApplicationContext来管理 Bean。BeanFactory是 IoC 容器的基本形式,提供基本的IoC功能;ApplicationContextBeanFactory的扩展,并提供更多企业级功能。

Spring AOP 实现机制

Spring AOP的实现依赖于动态代理技术。动态代理是在运行时动态生成代理对象,而不是在编译时,从而实现在不修改源码的情况下增强方法的功能。

Spring AOP支持两种动态代理:

  • 基于JDK的动态代理:使用java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现。这种方式需要代理的类实现一个或多个接口。
  • 基于CGLIB的动态代理:当被代理的类没有实现接口时,Spring 会使用 CGLIB 库生成一个被代理类的子类作为代理。CGLIB(Code Generation Library)是一个第三方代码生成库,通过继承方式实现代理。

什么是依赖注入?DI的实现方式有哪些?

传统编程中,对象直接使用 new 关键字创建依赖,这种硬编码方式让代码紧耦合,而 Spring 通过 DI 实现了 IoC 的设计思想,把对象创建和依赖管理的控制权从业务代码转移给外部容器。这样,业务代码只需要声明需要什么依赖,不用关心如何获取。

依赖注入(DI)其实是一种设计模式,通过外部容器向对象提供其所需的依赖对象,而不是对象内部主动创建依赖。

Spring IoC 容器启动时会扫描指定包路径,通过反射机制识别带有@Component等注解的类,将它们实例化为 Bean 并存储在容器中。当某个 Bean 需要依赖时,容器会根据类型或名称自动匹配并注入相应的 Bean 实例。

DI的实现方式主要有三种

  1. 构造器注入:通过构造函数参数传入依赖
  2. 属性注入:通过 setter 方法或直接字段赋值
  3. 接口注入:通过实现特定接口来接收依赖

Spring 的态度是不支持接口注入,因为侵入性太强,违背 “面向接口编程” 的初衷,Spring 的 DI 实现仅覆盖构造器注入和设值注入。

在实际开发中,构造器注入因为能保证依赖的不可变性和完整性,被认为是最佳实践。

Spring框架作为DI容器的典型实现,通过 IoC 容器管理对象的生命周期和依赖关系。

你可以使用@Autowired注解实现自动装配,@Component@Service@Repository等注解标记组件让Spring管理。

当遇到循环依赖问题时,Spring 通过三级缓存机制解决单例Bean的循环依赖问题。简单来说,Spring 在 Bean 实例化和初始化过程中,会提前暴露正在创建的 Bean 引用,让其他依赖它的 Bean 能够获取到这个引用,从而打破循环依赖的死锁。不过要注意,构造器循环依赖 Spring 无法解决,这也是推荐使用构造器注入时需要注意的设计问题。

怎么理解SpringBoot中的约定大于配置

约定大于配置是 Spring Boot 的特点,通过预设合理的默认行为和项目规范,大幅减少开发者需要手动配置的步骤,从而提升开发效率和项目标准化程度。可以从以下几个方面来解释:

  1. 自动化配置:Spring Boot 提供了大量的自动化配置,通过分析项目的依赖和环境,自动配置应用程序的行为。开发者无需显式地配置每个细节,大部分常用的配置都已经预设好了。

    例如,引入spring-boot-starter-web后,Spring Boot会自动配置内嵌Tomcat和Spring MVC,无需像 Spring 那样还需要手动编写XML中的很多配置才能启动。

  2. 默认配置:Spring Boot 为诸多方面提供大量默认配置,如连接数据库、设置 Web 服务器、处理日志等。开发人员无需手动配置这些常见内容,框架已做好决策。

  3. 约定的项目结构:Spring Boot 提倡特定的项目结构,通常主应用程序类(含 main 方法)置于根包,控制器类、服务类、数据访问类等分别放在相应子包。此约定使团队成员更易理解项目结构与组织,规范项目开发,新成员加入项目时能快速定位各功能代码位置,提升协作效率。

Spring Boot 框架相关注解

@Aspect注解的使用?如何定义切面和通知?结合 Spring 框架聊聊

@Aspect注解是 Spring AOP 中定义切面类的核心注解,需要配合@Component注册为Bean。一个标准的切面类需要同时使用@Aspect@Component两个注解

Spring AOP基于代理机制,所以只有通过Spring容器调用的方法才能被拦截,这也是为什么我们要确保被拦截的类要注册为Spring Bean的原因。

切面定义通过在类上添加@Aspect实现,切点定义使用@Pointcut注解指定拦截规则,比如@Pointcut("execution(* com.example.service.*.*(..))")表示拦截 service 包下所有方法。

谈到 @Aspect 时,你需要明确 Spring AOP 和 AspectJ 的关系:Spring AOP 实际上借鉴了 AspectJ 的注解体系,包括 @Aspect@Pointcut@Before 等注解都来自 AspectJ,但底层实现机制不同。Spring AOP 基于动态代理实现,只能拦截 Spring 管理的 Bean 的方法调用,而 AspectJ 是通过字节码织入实现的,功能更强大但也更复杂。在日常开发中,Spring AOP 已经能满足大部分场景需求。

通知类型包括五种:@Before在目标方法执行前运行,常用于参数校验;@After在方法执行后运行,用于资源清理;@AfterReturning在方法正常返回后执行,可获取返回值进行后置处理;@AfterThrowing在方法抛异常时执行,用于异常日志记录;@Around环绕整个方法执行,最为灵活,常用于性能监控和事务控制。

Spring 会在运行时为被拦截的 Bean 创建代理对象,当调用目标方法时实际执行的是代理逻辑,从而实现横切关注点的分离。记住切面只对 Spring 容器管理的 Bean 生效,直接 new 的对象无法被拦截。

对于多个切面可以进行执行顺序控制,当有多个切面作用于同一个方法时,Spring通过@Order注解来控制执行顺序。数值越小,优先级越高。但是对于这些注解并不是优先级高就一定先执行,例如对于@Before通知,Order值小的先执行;对于@After通知,Order值小的后执行

@ComponentScan注解的配置?包扫描的规则和过滤器?结合 Spring 框架聊聊?

@ComponentScan 是 Spring 框架中用于自动扫描和注册 Bean 的核心注解,它告诉 Spring 容器在哪些包路径下寻找标注了 @Component@Service@Repository@Controller 等注解的类。

Spring 容器的自动配置,依赖注入什么的都需要依赖于 Bean 的发现机制,当容器启动时需要知道哪些类应该被管理为 Bean,而@ComponentScan就是告诉容器去哪里找

基本配置很简单,通过 basePackagesvalue 属性指定扫描路径,而且还能配置excludeFilters 用于排除不需要的类等灵活处理的情况。如果不指定路径,默认扫描当前配置类所在包及其子包。你也可以用 basePackageClasses 指定基准类,Spring 会扫描这些类所在的包,这种方式在微服务和重构包名时的情况不容易出错。

包扫描规则遵循递归原则,在Spring容器的 refresh() 方法执行过程中,会有一个专门的阶段处理组件扫描,会递归遍历指定包路径下的所有 class 文件,通过反射机制检查每个类上的注解,符合条件的类会被实例化并注册到 IoC 容器中。

@Configuration@Component的区别?配置类的特殊性?结合 Spring 框架聊聊?

@Configuration@Component都是 Spring 中 标识受容器管理的 Bean 的注解

@Component是通用的 Bean 标识注解,而@Configuration是专门为配置类设计的,承载了 Spring 容器初始化、Bean 定义注册、依赖注入的核心逻辑。

@Configuration@Component的子类注解,它定义 Bean 的创建规则、管理 Bean 之间的依赖关系,是 Spring Java Config 的核心。

  1. @Component标记的类会被 Spring 容器管理,但方法调用遵循普通Java语义。

    @Configuration标记的类会被CGLIB代理增强,确保@Bean方法之间的调用返回同一个实例,维护Bean的单例特性。

  2. @Configuration作为配置类,支持@Import导入其他配置类,支持@PropertySource加载外部配置文件,支持@Enable*系列注解开启 Spring 特定功能;

    @Component不具备上述内容

  3. @Component仅能通过@Autowired/@Resource实现依赖注入,无法直接通过方法调用管理 Bean 依赖。

    @Configuration支持@Bean方法间的依赖传递,通过方法调用实现 Bean 依赖,无需手动注入。

简单说,@Component用于业务组件,@Configuration专门用于配置Bean的定义和装配,最重要的是区分这个

@Configuration注解的核心在于它会触发Spring的ConfigurationClassPostProcessor后置处理器。Spring容器在解析配置类时,会检测到@Configuration注解,然后使用CGLIB创建一个子类代理,这个代理类会拦截所有@Bean方法的调用。CGLIB代理不是简单的方法拦截,而是保证Bean实例的唯一性。代理的核心作用是确保当一个@Bean方法调用另一个@Bean方法时,返回的是Spring容器中已经存在的Bean实例,而不是重新创建。

@Component类中定义了多个@Bean方法,并且方法间有调用关系。结果发现每次调用都创建了新的实例,导致数据库连接数异常飙升。后来改成@Configuration注解,问题立即解决。

在实际项目中,一般在三种场景下会选择@Configuration第一是基础设施配置,比如数据库连接池、Redis客户端这些需要严格单例的组件。第二是第三方框架集成,像Swagger、定时任务框架的配置。第三是复杂的Bean依赖关系配置,特别是Bean之间有相互调用的场景。

@Profile注解的作用是什么?你都如何使用它?多环境配置下如何进行实践来进行环境隔离?

@Profile 注解是 Spring 框架中用于条件化加载 Bean 的一个注解,通过指定激活的环境配置来控制特定组件的实例化。

可以在类级别或方法级别使用它,比如 @Profile("dev")@Profile({"prod", "staging"})。标注在 @Bean 方法上就是精准控制单个 Bean 的加载,标注在配置类上就是控制整个配置类的加载

在多环境配置的实际应用中,我通常会为不同数据源创建专门的配置类,开发环境使用 @Profile("dev") 配置开发用的数据库,生产环境用 @Profile("prod") 配置 MySQL 集群。

激活 Profile 的方式包括在 application.properties 中设置 spring.profiles.active=dev,或通过 JVM 参数 -Dspring.profiles.active=prod 启动应用,这样就能用 @Profile 隔离差异化 Bean

一般情况下,环境隔离需要通过多文件配置策略实现,将 application-dev.yml作为开发环境配置、application-prod.yml作为生产环境配置,等环境特定配置分离,主配置文件只保留公共配置。

对于复杂的微服务架构,可以组合使用多个 Profile,比如 @Profile("prod & !docker") 这样的表达式来处理更精细的环境区分。

另外,避免在业务代码中硬编码环境判断,而是通过 @ConditionalOnProfile 或直接使用 @Profile 让 Spring 容器自动处理依赖注入,这样既保证了代码的清洁性,又提高了环境切换的灵活性。

Spring Boot 中都有哪些注入 Bean 的方式?@Autowired@Resource@Inject注解的区别?为什么 Spring 官方不推荐使用 @Autowired

虽然我们常说 注解注入,但从底层机制上,Spring 主要支持以下三种方式来进行 DI:

  1. 构造器注入

    这是官方推荐的,它通过类的构造函数来明确依赖关系。通过 final 修饰,能够保证依赖不为空

    1
    2
    3
    4
    5
    6
    private final UserService userService;

    // 4.3+ 单构造器可省略@Autowired
    public UserController(UserService userService) {
    this.userService = userService;
    }
  2. 通过 setXxx 方法注入依赖。

    它比较灵活,允许依赖在之后被修改或重新注入。但是无法保证依赖在对象使用前已完全初始化,可能导致NPE

    1
    2
    3
    4
    5
    6
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
    this.userService = userService;
    }
  3. 字段注入

    直接在成员变量上注解

    1
    2
    @Autowired
    private UserService userService;

@Autowired@Resource@Inject 这三个注解都能实现自动装配 Bean,但是, @Autowired@Resource@Inject本身是 依赖注入的注解,不是 固定的某一种注入方式,只是日常开发中大家最常用在字段上。

它们之间的区别就是

注解 所属规范 匹配优先级 支持的注入形式 核心区别
@Autowired Spring 专属 优先类型(byType)→ 再按名称(byName) 字段 /setter/ 构造器 支持required=false@Qualifier指定 Bean 名
@Resource JSR-250(Java 标准) 优先名称(byName)→ 再按类型(byType) 字段 /setter(不支持构造器) 可通过name属性指定 Bean 名
@Inject JSR-330(Java 标准) 优先类型(byType) 字段 /setter/ 构造器 需导入javax.inject依赖,支持@Named指定 Bean 名

详细的查找逻辑是这样的

  • @Autowired

    • 首先根据 类型 (Type) 查找。

    • 如果找到多个同类型的 Bean,则尝试按 名称 (Name) 匹配。

    • 如果名称也匹配不上,且没有 @Qualifier,则报错。

  • @Resource

    • 首先根据 名称 (Name) 查找(默认取字段名)。

    • 如果按名称没找到,则回退到按 类型 (Type) 查找。

    • 它的逻辑更符合 Java 原生语义,且与框架解耦。

为什么 Spring 官方不推荐使用 @Autowired 字段注入呢?明明只有它是 Spring 生态中的内容?难道考虑的是兼容性吗?只能说是不止,其实是这样

  1. 违反了单一职责原则

    使用字段注入太方便了,以至于开发者很容易在一个类里塞入 10 个甚至 20 个依赖,而不会觉得有什么问题。那么就可能出现了类违反单一职责,循环依赖或无用依赖的情况。如果是构造器注入,看到一个拥有 20 个参数的构造函数,开发者会立刻意识到这个类承载了太多职责,需要重构。

  2. 无法使用 final 修饰符

    字段注入是无法使用 final 修饰符的,因为字段注入是在对象实例化之后通过反射设置的。这破坏了 不可变对象 (Immutable Objects) 的设计模式,可能在高并发等情况下出现问题

  3. 与 Spring 容器过度耦合

    这是最致命的一点。字段注入必须依赖 Spring 的反射机制。如果你想在不启动 Spring 容器的情况下(如纯 JUnit 测试)实例化这个类,你必须手动编写反射代码来注入依赖,否则该字段永远是 null

  4. 隐藏了循环依赖风险

    如果是构造器注入,存在循环依赖,Spring 在项目启动时就会抛出 BeanCurrentlyInCreationException,提醒你设计有问题。而字段注入因为反射不会在启动时候检查,而是等问题出现才报错。

既然官方不推荐,那为什么我看到的 demo 全是 @Autowired 字段注入?

  • 没办法,字段注入太简单了,以至于在实际开发中,它仍然主流
  • 但是,我们可以配合 Lombok@RequiredArgsConstructor 注解,它能自动生成带 final 字段的构造函数,这样,使用构造器注入就很方便了

@Qualifier注解的作用?如何解决多个候选 Bean 的问题?结合你的实践聊聊?

首先,我们都知道,@Autowired默认按类型匹配进行注入,@Resource默认按名称匹配,而在实际开发中,如果你的项目可能迁移到其他容器或需要标准化,@Resource更具可移植性,所以说我们用的更多的都是@Resource

当 Spring 容器中存在多个同一类型的 Bean 实例时,依赖注入会触发NoUniqueBeanDefinitionException异常。但在实际开发中,同一类型存在多个 Bean 实例 的场景很常见(比如多数据源、多缓存实现、多支付渠道),例如,在项目中,我需要同时集成 PostgreSQL 和 MongoDB 两种数据源,且都实现了DataSource接口,此时容器中会存在两个DataSource类型的 Bean,直接注入就会冲突

除了@Resourcename属性,我们使用@Qualifier + @Autowired也能解决这个问题。

@Qualifier是 Spring 提供的 Bean 名称限定注解,在按类型匹配的基础上,进一步按 Bean 名称筛选,解决多候选 Bean 的歧义问题。

使用方式很直接,在@Autowired旁边添加@Qualifier("beanName")来指定具体的Bean名称。比如你有两个DataSource类型的Bean,一个叫primaryDataSource,另一个叫secondaryDataSource,注入时就用@Qualifier("primaryDataSource")明确告诉Spring要哪个。

如何理解@SpringBootApplication注解,为什么标注了这个注解的类能够作为主启动类?

@SpringBootApplication 是 Spring Boot 核心的组合注解,它整合了 3 个注解。

  • @ComponentScan做组件扫描,扫描当前包及子包下的 @Controller/@Service/@Component 等注解的类
  • @EnableAutoConfiguration开启自动配置,自动加载 Spring Boot 内置的默认配置
  • @SpringBootConfiguration也是标记该类为配置类,本质是 @Configuration

标注了该注解的类之所以能作为主启动类,是因为它完成了 开启自动配置、扫描组件、标记配置类 三大核心工作,是 Spring Boot 约定大于配置 思想的集中体现。

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 核心组合注解
@SpringBootConfiguration // 替代 @Configuration,标记该类为配置类
@EnableAutoConfiguration // 开启自动配置(最核心)
@ComponentScan // 扫描组件(Controller/Service/Mapper等)
public @interface SpringBootApplication { ... }

启动类的核心是 main 方法 + SpringApplication.run(),但真正让它成为 启动入口 的是 @SpringBootApplication 的三大能力:

  1. @ComponentScan从启动类所在包开始,递归扫描所有子包下的组件

    如果你的 Controller 放在启动类的上层包,会扫描不到,我被坑过,需要指定路径

  2. @EnableAutoConfiguration 会通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件中的自动配置类,自动配置类通过 @Conditional 注解实现按需要生效,只需配置配置文件 yaml 中的相关信息,无需手动创建 Bean,就是自动配置的体现。

  3. @SpringBootConfiguration 让启动类具备配置类的能力,可直接在启动类中通过 @Bean 定义全局组件

执行SpringApplication.run(),进行的是 Spring Boot 项目启动那一套了

  1. 加载 @SpringBootApplication 注解的配置;
  2. 扫描组件并注册到 Spring 容器;
  3. 启动自动配置流程;
  4. 启动内嵌 Tomcat(如果引入 web 依赖);
  5. 初始化 Spring 上下文,项目启动完成。

Spring Boot 实践相关

如何在 Spring Boot 中定义和读取自定义配置?Spring Boot 配置文件加载优先级你知道吗?

Spring Boot 支持多种配置文件格式,而自定义配置项的核心就是定义配置项 + 读取配置项

定义自定义配置

先在配置文件中定义自定义配置项,这就是我们把配置写到配置文件里

1
2
3
4
5
6
7
8
9
10
# application.yml
app:
database:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
servers:
- 192.168.1.100
- 192.168.1.101
- 192.168.1.102

而读取配置项有几种方式

  1. @Value 注解:${配置项key:默认值}

    直接注入单个配置项,需要注意配置文件需要成为 Bean 交给 Spring Boot 管理,加@Component

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;

    // 必须交给Spring容器管理,否则无法注入
    @Component
    public class AppConfig {
    // 注入单个配置项,冒号后是默认值
    @Value("${app.name:default-app}")
    private String appName;

    @Value("${app.version:1.0.0}")
    private String appVersion;

    // 注入嵌套配置项
    @Value("${app.database.url}")
    private String dbUrl;
    }
  2. @ConfigurationProperties

    它是批量绑定配置项,适合配置项多、有层级的场景。一般情况下,我们需要自定义一系列自定义配置,就使用这个注解编写对应的配置类,业务中可以直接注入使用

    一般需要如下几步

    1. 编写配置绑定类,也就是在这里声明里的yaml中需要绑定哪些自定义配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.stereotype.Component;

      import java.util.List;

      // 前缀:绑定yaml配置文件中以app开头的配置项
      @ConfigurationProperties(prefix = "app")
      // Spring Boot 2.2+ 也可通过 @EnableConfigurationProperties 启用
      @Component
      // 必须有getter/setter,否则无法绑定
      @Getter
      @Setter
      public class AppProperties {
      // 字段名与配置项后缀一致(name → app.name)
      private String name;
      private String version;
      private String env;

      // 嵌套配置(对应 app.database)
      private Database database;
      // 数组配置(对应 app.servers)
      private List<String> servers;

      // 内部类:绑定嵌套配置
      public static class Database {
      private String url;
      private String username;
      private String password;
      }
      }

      如果不写 @Component,可在启动类加 @EnableConfigurationProperties

    2. 然后就可以使用配置,注入依赖后直接使用就可以了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;

      @RestController
      public class ConfigController {
      @Autowired
      private AppProperties appProperties;

      @GetMapping("/config")
      public String getConfig() {
      return "应用名称:" + appProperties.getName() +
      "<br>数据库地址:" + appProperties.getDatabase().getUrl() +
      "<br>服务器列表:" + appProperties.getServers();
      }
      }

然后,Spring Boot 会从多个位置加载配置文件,优先级高的配置会覆盖优先级低的,从高到低优先级如下

  1. 命令行参数

    启动时通过启动命令传入的参数,会覆盖所有配置,适合临时修改配置。

  2. 操作系统环境变量

    Spring Boot 会自动识别操作系统的环境变量

  3. JVM系统属性

    启动时通过 JVM 属性传入的配置,-Dapp.name=prod 传入,如 java -Dapp.name=prod -jar app.jar

  4. 外置的配置文件

    (项目根目录 config/ > 项目根目录)

    项目根目录下的 config/ 文件夹:./config/application.yml

    项目根目录:./application.yml

  5. 内置的配置文件

    classpath/config/ > classpath 根目录)

    类路径(resources)下的 config/ 文件夹:classpath:/config/application.yml

    类路径根目录:classpath:/application.yml

  6. 配置类中的默认值

    @Value("${app.name:default}") 中的 default,是最后兜底的默认值

如果配置了多环境,优先级规则如下

  • 通过 spring.profiles.active=dev 指定环境后,对应环境的配置(如 application-dev.yml)会覆盖 application.yml 中的同名配置;
  • 多环境配置的加载优先级遵循上述「外置 > 内置」规则。

如何实现自定义的AOP功能?如何在AOP中获取方法参数和返回值?ProceedingJoinPoint的使用?

在Spring中,你可以通过@Aspect注解标记一个 @Component 的 Bean 作为 AOP 切面对象,配合@Around@Before@After等注解快速实现切面逻辑

在实际项目中,AOP的应用场景主要集中在四个方面:日志记录、性能监控、权限校验和缓存处理。自定义注解配合AOP是一种优雅的设计思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String value() default "";
}

@Aspect
@Component
public class OrderAspect {

@Around("@annotation(operationLog)")
public Object logOperation(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
String operation = operationLog.value();
String userId = getCurrentUserId();

try {
Object result = joinPoint.proceed();
logService.recordSuccess(userId, operation, joinPoint.getArgs());
return result;
} catch (Exception e) {
logService.recordFailure(userId, operation, e.getMessage());
throw e;
}
}
}

切面最佳实践方面,建议遵循单一职责原则,一个切面只处理一类横切关注点,避免在切面中写复杂业务逻辑。切点表达式要精确,防止误拦截不相关方法。异常处理要谨慎,避免切面异常影响主业务流程。在Spring环境下,@Transactional就是经典的AOP应用,声明式事务通过切面自动管理事务边界。

那么,在 AOP 中获取方法参数和返回值主要通过 ProceedingJoinPoint 实现。这个接口是Spring AOP提供的连接点对象,专门用于环绕通知@Around中。

通过 joinPoint.getArgs() 可以获取目标方法的所有参数数组,获取返回值则需要调用 joinPoint.proceed() 执行目标方法,这个方法会返回目标方法的执行结果。

@Around通知的整个处理中,先通过getArgs()获取入参进行前置处理,然后调用proceed()执行目标方法并捕获返回值,最后对返回值进行后置处理。

需要注意的是,proceed()方法必须被调用,否则目标方法不会执行。如果目标方法抛出异常,proceed()会将异常向上传播,这时你可以在catch块中处理异常情况。通过joinPoint.getSignature()还能获取方法签名信息,包括方法名、参数类型等元数据,这在动态日志记录和权限校验中特别有用。

写过 Spring Boot Starter 吗?

  1. 创建Maven项目

    首先,需要创建一个新的Maven项目。在 pom.xml 中添加 Spring Boot 的 starter parent 和一些依赖

  2. 然后添加自动配置

    resources 文件夹下,在 META-INF/spring.factories中添加自动配置的元数据。

  3. 创建配置熟悉类

    一般情况下,我们的 starter 中需要有供用户定义的配置,创建一个配置属性类,使用@ConfigurationProperties注解来绑定配置文件中的属性。

  4. 创建服务和控制器

    创建一个服务类和服务实现类,以及一个控制器来展示和测试你的 starter 的功能。

  5. 发布 Starter

    测试后,将你的starter发布到Maven仓库,可以是私有的或是公共的

  6. 使用 Starter

    在你的主应用的pom.xml中添加你的starter依赖,然后在application.yml或application.properties中配置你的属性。

Spring Boot 如何处理跨域请求(CORS)?

跨域只存在于浏览器端,之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。不同的请求类型,CORS的处理也不一样

  • 简单请求GET/POST/HEAD,且请求头只有默认字段(如 Content-Typetext/plain),直接触发跨域检查;
  • 预检请求(OPTIONS)PUT/DELETE/ 自定义请求头Content-Typeapplication/json 时,浏览器会先发送 OPTIONS 预检请求,确认后端允许跨域后,再发送真实请求。

Spring Boot 处理跨域的 3 种方案如下

  1. 全局跨域配置

    通过配置类统一设置跨域规则,覆盖所有接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    @Configuration // 标记为配置类
    public class CorsConfig {

    // 方式 1:通过 WebMvcConfigurer 配置(更简洁,推荐)
    @Bean
    public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**") // 对所有接口生效
    // 允许的跨域源(前端域名),* 表示允许所有(生产环境建议指定具体域名)
    .allowedOriginPatterns("*")
    // 允许的请求方法(GET/POST/PUT/DELETE 等)
    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
    // 允许的请求头
    .allowedHeaders("*")
    // 是否允许携带 Cookie(跨域认证需要)
    .allowCredentials(true)
    // 预检请求的缓存时间(秒),减少 OPTIONS 请求次数
    .maxAge(3600);
    }
    };
    }

    // 方式 2:通过 CorsFilter 配置(更灵活,面试可提)
    /*
    @Bean
    public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    // 允许的源(Spring Boot 2.4+ 推荐用 allowedOriginPatterns,替代 allowedOrigins)
    config.addAllowedOriginPattern("*");
    // 允许的请求方法
    config.addAllowedMethod("*");
    // 允许的请求头
    config.addAllowedHeader("*");
    // 允许携带 Cookie
    config.setAllowCredentials(true);
    // 预检缓存时间
    config.setMaxAge(3600L);

    // 配置生效的路径
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsFilter(source);
    }
    */
    }
  2. 局部跨域配置 @CrossOrigin 注解

    针对单个接口 / 控制器生效,接口级 @CrossOrigin > 控制器级 @CrossOrigin > 全局配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    // 对整个控制器的所有接口生效
    @CrossOrigin(origins = "http://localhost:8080", maxAge = 3600)
    public class TestController {

    // 对单个接口生效(优先级高于控制器注解)
    @GetMapping("/api/test")
    @CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST})
    public String testCors() {
    return "跨域请求成功";
    }
    }
  3. 通过网关处理

    如果项目中使用 Spring Cloud Gateway/Zuul 网关,可在网关层统一配置跨域,避免每个微服务重复配置

一般情况下,除非开发时测试,否则禁止使用 allowedOriginPatterns("*"),必须指定具体的前端域名(如 http://www.xxx.com),避免安全风险。而且开启 allowCredentials(true) 时,前端必须同步设置 withCredentials: true

Spring Boot 的项目结构是怎么样的?

一个正常的企业项目里,通常会使用一种通用的项目结构和代码层级划分,来规范化项目开发和便于团队协作

一般情况下,一个 Spring Boot 项目里会有这样的一个结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com
└── pxd
└── project
├── ProjectApplication.java # 启动类(最顶层)
├── config/ # 配置类
├── controller/ # 控制层(API接口)
├── service/ # 业务层
│ ├── impl/ # 业务实现类
├── mapper/ # 数据访问层(DAO/Mapper)
├── entity/ # 实体/数据库映射
├── dto/ # 数据传输(请求/响应)
├── vo/ # 视图返回对象
├── common/ # 通用工具
│ ├── exception/ # 全局异常
│ ├── result/ # 统一返回结果
│ ├── util/ # 工具类
├── security/ # 安全/认证相关
└── aspect/ # AOP切面
  1. 配置层 config

    存放所有配置:Redis、Security、跨域配置、线程池等。包括你自定义配置中的内容。

  2. 控制器层 controller

    接收前端请求、参数校验、调用 service、返回统一结果。

  3. 业务逻辑层 service

    核心业务处理、调用DAO持久层,调用第三方服务等

  4. 数据访问层 dao

    和数据库交互,CRUD。

  5. 数据模型层 model

    1. 数据库实体 entity:对应数据库表结构
    2. 数据传输对象 dto /vo:
      • dto:前端传给后端的请求
      • vo:后端返回给前端的视图对象
  6. 通用模块 common

    里面可以放工具类,异常处理,自定义的一些注解啊什么

  7. 安全模块 security

    登录、认证、授权、JWT、OAuth2、2FA等安全业务在这里处理

  8. AOP 切面层 aspect

    日志、操作记录、权限校验、接口耗时统计、网站 UV 收集等

所以说,Spring Boot 的标准结构是典型的三层架构Controller → Service → Mapper,结构清晰、职责明确、便于团队协作和维护。

image-20260310201218523

Spring Boot 当中,你是如何实现统一异常处理的?

在 Spring Boot 里,我是通过 @ControllerAdvice + @ExceptionHandler 实现全局统一异常处理的,同时配合自定义业务异常统一返回格式,让所有接口的异常都能集中处理、格式一致、便于排查。

@ExceptionHandler异常处理器注解,指定方法处理某一类异常,比如业务异常、参数异常、系统异常。

@ControllerAdvice开启全局控制器增强,能捕获所有 Controller 抛出的异常。

这样代码不冗余、返回格式统一、安全不泄露堆栈、方便日志记录、便于前端处理

  1. 首先肯定要定义统一返回结果

    所有接口成功 / 失败都用同一个格式

  2. 然后写一个自定义业务异常 BusinessException

    区分业务错误系统错误

  3. 然后写一个全局异常处理器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @RestControllerAdvice
    @Slf4j
    public class GlobalExceptionHandler {

    // 1. 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
    log.error("业务异常:{}", e.getMessage());
    return Result.fail(e.getCode(), e.getMessage());
    }

    // 2. 处理参数校验异常(@Valid 校验失败)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
    String msg = e.getBindingResult().getFieldError().getDefaultMessage();
    return Result.fail(400, "参数错误:" + msg);
    }

    // 3. 兜底:处理所有未知系统异常
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
    log.error("系统异常", e);
    return Result.fail(500, "服务器繁忙,请稍后重试");
    }
    }

这样,所有 Controller 不写 try/catch,直接抛自定义业务异常,代码非常干净,而且能够全局捕获,分类处理,让异常不再飞升老冯

Spring Boot 中,你如何结合 Java 的多线程去做异步任务进行异步处理的?如何使用@Async@Async什么时候会失效?

在传统的同步请求中,主线程会阻塞等待每一个任务完成,发送邮件、生成报表、调用第三方接口,这种完全可以异步的耗时任务,我们就可以开启异步处理,让 Spring 交给辅助线程池处理,主线程立即返回响应。

使用 @Async 是 Spring 提供的一种基于 AOP 的简便实现。

首先,就是在启动类或配置类上标注 @EnableAsync。然后,在方法上标注注解@Async。如果需要获取返回值,推荐使用 CompletableFuture

1
2
3
4
@Async
public CompletableFuture<String> doTask() {
return CompletableFuture.completedFuture("任务完成");
}

@Async 失效的根本原因就一个:代理没生效

Spring 的 @Async 底层是通过 AOP 代理来实现的,只有调用走代理对象的方法,才能被拦截然后丢到线程池里执行。任何绕过代理的情况,都会让 @Async 变成摆设。

最常见的失效场景有这几种:

  1. 同一个类里内部调用。

    在 Service 的 A 方法里直接调 this.B(),这个 B 方法上的 @Async 压根不生效。因为 this 指向的是原始对象,不是代理对象,调用根本没经过 AOP 拦截。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Service
    public class OrderService {
    public void createOrder() {
    // 失效!this 指向原始对象,不是代理
    this.sendNotification();
    }

    @Async
    public void sendNotification() {
    // 期望异步执行,但实际是同步的
    }
    }
  2. 方法不是 public 的

    Spring 默认用的是基于接口的 JDK 动态代理或者基于类的 CGLIB 代理,这两种代理都只能拦截 public 方法

  3. 不能是 static 方法

    静态方法属于类,不属于 Spring Bean 实例,无法被代理

  4. 压根没开启异步支持

    @Async 不是开箱即用的,必须在配置类或启动类上加 @EnableAsync,Spring 才会去扫描 @Async 注解并创建代理。不加这个注解,@Async 就是个装饰品。

  5. 返回值类型不对。

    @Async 方法的返回值只能是 voidFutureCompletableFuture 这几种。你要是让它返回个普通 String,其实也不会报错,方法也执行了,但是调用方拿到的就是 null,异步执行的结果你拿不到,在需要结果的场景下,看起来好像就失效了。

@Async 的实现靠的是 AsyncAnnotationBeanPostProcessor 这个后置处理器。Spring 容器启动时,这个处理器会扫描所有 Bean,发现有 @Async 注解的方法,就给这个 Bean 套一层代理。

代理对象在方法被调用时,会先判断方法上有没有 @Async。有的话,就把这次调用包装成一个 Callable 任务,丢给 TaskExecutor 线程池去执行,然后立即返回。这就是异步的本质。

多说一句,@Async 默认用的是 SimpleAsyncTaskExecutor,这玩意每来一个任务就 new 一个线程,没有线程复用,生产环境直接用会出事。正确做法是自定义 ThreadPoolTaskExecutor,然后在 @Async 注解里指定用哪个线程池

Spring Boot 中你如何实现定时任务?

Spring Boot 内置了对定时任务的支持,实现起来非常简单

  1. 开启定时任务: 在启动类或配置类上标注 @EnableScheduling

  2. 创建定时方法: 在 Bean 的方法上标注 @Scheduled

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    public class MyTask {

    @Scheduled(fixedRate = 5000) // 每隔5秒执行一次
    public void task() {
    System.out.println("执行定时任务: " + LocalDateTime.now());
    }
    }

@Scheduled 注解的参数决定了定时的情况

  • fixedRate:固定频率,以任务开始执行的时间点算起。例如 fixedRate = 5000,无论任务执行多久,每隔 5 秒都会尝试启动下一个任务。
  • fixedDelay:固定延迟,以任务执行完成的时间点算起。例如 fixedDelay = 5000,任务执行完后,再等 5 秒才执行下一个。
  • cron:使用 Cron 表达式,最常用,因为能通过表达式控制精确时间。格式为[秒] [分] [时] [日] [月] [周]

我们都知道 JUC 中有一个专门用于执行定时任务的线程池,那么 Spring 的定时任务也是这样吗?是这样,但是

  • Spring 默认使用单线程的调度器。

    什么意思,如果你有多个定时任务(Task A 和 Task B),如果 Task A 阻塞了,Task B 即使到了时间也不会执行。

所以说,我们需要自定义 ScheduledExecutorService 线程池,设置合理的核心线程大小

1
2
3
4
5
6
7
8
9
10
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置核心线程池大小
scheduler.setThreadNamePrefix("my-scheduled-task-");
return scheduler;
}
}

分布式场景下的定时任务也有一些要注意的内容

  • 例如,在微服务架构下,如果你部署了 3 个服务实例,@Scheduled 会在 3 台机器上同时运行,导致任务重复执行

  • 怎么办?

    1. 分布式锁

      使用 Redisson 配合 Redis 或 MySQL。在执行任务前尝试抢占锁,抢到的机器执行,抢不到的跳过。

    2. 分布式任务调度平台

      • XXL-JOB:支持中心化管理、调度中心与执行器分离、失败重试、执行日志监控、分片任务等。
      • Quartz:老牌调度框架,功能强大但配置较复杂。

Spring Boot 中你如何使用拦截器,说说你在什么业务下如何使用的拦截器?

说说你对 Spring Boot 事件机制的了解?

你的项目中涉及到了多数据源啊?那么在 Spring Boot 中你是如何配置和实现多数据源的?

Spring Boot 中实现多数据源,本质上是绕过 Spring Boot 的自动装配机制,手动创建并管理多个 DataSource 实例。

例如,我的平台使用 PostgreSQL 处理结构化业务、MongoDB 存储非结构化文档,那么基本是这样实现的

  • 首先在启动类上排除默认的数据源自动配置类,避免和自定义数据源冲突,这是基础步骤,必须先做:

    1
    2
    3
    4
    5
    6
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
    public class ZyPlatformApplication {
    public static void main(String[] args) {
    SpringApplication.run(ZyPlatformApplication.class, args);
    }
    }
  • 在 application.yml 中配置所有数据源的连接参数,我项目里配了 PostgreSQL、MySQL、MongoDB 三个,核心是给每个数据源单独指定连接池、驱动、url、账号密码,用自定义前缀区分,同时配置连接池参数,按数据源的业务压力单独调优,示例核心配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    spring:
    datasource:
    # 结构化业务- PostgreSQL数据源
    pg:
    driver-class-name: org.postgresql.Driver
    jdbc-url: jdbc:postgresql://ip:port/zy_platform
    username: xxx
    password: xxx
    hikari:
    minimum-idle: 5
    maximum-pool-size: 20
    # 备用结构化- MySQL数据源
    mysql:
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://ip:port/zy_platform?useUnicode=true&characterEncoding=utf8
    username: xxx
    password: xxx
    # MongoDB非结构化数据源(单独配置,基于Spring Data MongoDB)
    data:
    mongodb:
    uri: mongodb://ip:port/zy_mongo
    database: zy_platform

    MongoDB 是非关系型数据库,基于 Spring Data MongoDB 单独配置,和关系型数据源的配置逻辑分离,这是多源异构的关键。

  • 新建配置类,通过@ConfigurationProperties绑定 yml 中的多数据源配置,然后为每个数据源手动创建对应的「数据源 + 会话工厂 + 事务管理器」,给每个数据源的组件单独命名,然后用@Primary指定主数据源,避免容器中 bean 冲突。

  • 分库路由,指定不同包 / 方法对应不同数据源,项目中用包扫描 + 注解结合的方式,简单高效

    • 包扫描路由:在配置类中,给不同数据源的EntityManagerFactory/SqlSessionFactory指定专属的 Repository 包路径,比如 PostgreSQL 对应com.zy.platform.repository.pg,MySQL 对应com.zy.platform.repository.mysql,MongoDB 对应com.zy.platform.repository.mongo,框架会自动根据包路径匹配数据源,开发时只需按包分层开发就能自动对应得上
    • 注解式路由:针对少数跨库操作的方法,自定义一个注解,结合 AOP 实现方法级的数据源动态切换
  • 事务管理,多数据源事务的处理方式

    • 单库:直接在 Service 方法上用@Transactional,指定对应的事务管理器,框架自动管理,和单数据源用法一致;
    • 跨库:如果是跨库事务,就涉及到分布式事务了,一般情况下使用 Seata 的 AT 模式都能搞定,整合 Seata 后,只需在跨库方法上加@GlobalTransactional,Seata 会自动管理多数据源的事务一致性,无需手动处理回滚;

注意,多数据源下禁用二级缓存共享,每个数据源单独配置缓存,要不然极容易脏读,嗷嗷