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 Boot 和 Spring 有什么区别?

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

  4. 内嵌服务器

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

  5. 简化监控与运维

什么是自动装配?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 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)是一个第三方代码生成库,通过继承方式实现代理。

怎么理解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值小的后执行

如何理解@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 中的同名配置;
  • 多环境配置的加载优先级遵循上述「外置 > 内置」规则。

写过 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,直接抛自定义业务异常,代码非常干净,而且能够全局捕获,分类处理,让异常不再飞升老冯