一篇文章,说说 SpringBoot 相关的面试题

99 阅读42分钟

Spring Boot 凭借其“约定优于配置”的理念和快速开发的特性,已成为 Java 后端开发的基石。

1. 什么是 Spring Boot?它解决了 Spring 框架的哪些痛点?

答案:

Spring Boot 是一个用于快速构建基于 Spring 框架的、独立的、生产级别的应用程序的框架。它基于 Spring 框架,通过提供一系列预设的配置和自动化功能,极大地简化了 Spring 应用程序的开发、部署和运行。

它主要解决了 Spring 框架以下痛点:

  • 繁琐的配置: 传统 Spring 应用需要大量的 XML 或 Java 配置,如数据源、事务管理器、MVC 配置等。Spring Boot 通过**自动配置(Auto-configuration)**机制,根据项目中引入的依赖自动推断并配置,大大减少了手动配置的工作量。
  • 依赖管理复杂: Spring 框架项目通常需要手动管理大量依赖的版本兼容性问题。Spring Boot 提供了Starter POMs,将相关依赖聚合在一起,并管理它们的版本,简化了依赖管理。
  • 部署复杂: 传统 Spring 应用通常需要部署到外部的 Web 服务器(如 Tomcat、Jetty)。Spring Boot 内嵌了主流的 Web 服务器(Tomcat、Jetty、Undertow),可以直接打包成可执行的 JAR 包运行,实现了内嵌式服务器,简化了部署流程。
  • 生产环境准备: Spring 应用在生产环境需要额外的监控、健康检查等功能。Spring Boot 提供了 Spring Boot Actuator,为生产环境提供了一系列开箱即用的功能,如健康检查、度量指标、审计等。
  • 快速启动和开发: 通过上述特性,Spring Boot 使得 Spring 应用的搭建和启动速度更快,开发效率显著提高。

2. Spring Boot 的核心组件和特性有哪些?

答案:

Spring Boot 的核心组件和特性包括:

  • 自动配置(Auto-configuration): 这是 Spring Boot 最核心的特性之一。Spring Boot 会根据项目中添加的 Jar 包依赖,自动配置应用程序所需的 Bean。例如,当检测到 spring-boot-starter-web 依赖时,会自动配置 Tomcat 和 Spring MVC。
  • Starter POMs: 一系列预定义的 Maven 或 Gradle 依赖,它们包含了构建特定类型应用所需的所有常用依赖,并管理了它们的版本。例如,spring-boot-starter-web 用于构建 Web 应用,spring-boot-starter-data-jpa 用于集成 JPA。
  • 内嵌式服务器(Embedded Servers): Spring Boot 应用可以打包成一个可执行的 JAR 包,其中包含了内嵌的 Tomcat、Jetty 或 Undertow 等服务器,无需单独部署 Web 服务器。
  • 命令行界面(CLI): Spring Boot 提供了一个命令行工具,可以快速创建、运行和测试 Spring Boot 应用程序,但目前使用较少,更常用的是 IDE 集成。
  • Spring Boot Actuator: 提供了一系列生产级别的特性,如健康检查、度量指标、信息暴露(info、env)、审计、跟踪等,方便对应用程序进行监控和管理。
  • 外部化配置(Externalized Configuration): 允许在不修改代码的情况下,通过属性文件、YAML 文件、环境变量、命令行参数等多种方式配置应用程序,方便在不同环境之间切换配置。
  • Spring Initializr: 一个 Web 工具,用于快速生成 Spring Boot 项目的骨架,可以选择项目类型、语言、Spring Boot 版本和所需的依赖。

3. 解释 Spring Boot 的自动配置原理。

答案:

Spring Boot 的自动配置原理是其“约定优于配置”理念的基石,主要依赖于以下几个核心机制:

  1. @EnableAutoConfiguration 注解:

    • 这个注解通常被包含在 @SpringBootApplication 注解中。
    • 它会触发 Spring Boot 的自动配置机制。
  2. spring.factories 文件:

    • 在 Spring Boot 的每个 Starter 模块(如 spring-boot-autoconfigure)的 META-INF/spring.factories 文件中,定义了需要自动配置的类。
    • 这些类通常是带有 @Configuration 注解的配置类,它们包含了创建 Bean 的逻辑。
  3. 条件化注解(Conditional Annotations):

    • 自动配置的核心是条件化注解,最常用的是 @ConditionalOnClass@ConditionalOnMissingBean
    • @ConditionalOnClass 表示只有当 classpath 中存在指定的类时,才会加载当前的配置类或 Bean。例如,WebMvcAutoConfiguration 只有当 DispatcherServlet 类存在时才会生效。
    • @ConditionalOnMissingBean 表示只有当 Spring 容器中不存在指定类型的 Bean 时,才会创建当前的 Bean。这允许开发者通过自定义 Bean 来覆盖 Spring Boot 的默认配置。
    • 还有其他条件注解,如 @ConditionalOnProperty(根据配置属性是否存在或匹配)、@ConditionalOnWebApplication(在 Web 应用环境下)等。

工作流程总结:

当 Spring Boot 应用程序启动时:

  1. @SpringBootApplication 中的 @EnableAutoConfiguration 会被激活。
  2. Spring Boot 会扫描所有 Jar 包下的 META-INF/spring.factories 文件,加载其中定义的自动配置类。
  3. 对于每个自动配置类,Spring Boot 会根据其上的条件化注解进行判断。
  4. 如果条件满足(例如,特定类存在,或特定 Bean 不存在),则该配置类会被加载并创建相应的 Bean,从而完成自动配置。

举例说明:

当您引入 spring-boot-starter-data-jpa 时,spring-boot-autoconfigure 模块中的 JpaAutoConfiguration 会被加载。它内部会使用 @ConditionalOnClass({ DataSource.class, JpaProperties.class, EntityManagerFactory.class }) 等条件,确保当这些类都存在时,才会自动配置 EntityManagerFactoryTransactionManager 等 JPA 相关 Bean。如果您在项目中自定义了一个 DataSource Bean,那么 Spring Boot 的默认 DataSource 自动配置就会因为 @ConditionalOnMissingBean(DataSource.class) 而失效,从而使用您自定义的 Bean。


4. Starter POMs 是什么?它们有什么作用?

答案:

Starter POMs 是 Spring Boot 提供的一系列方便的依赖描述符。它们实际上就是 Maven(或 Gradle)的依赖项,但其特殊之处在于它们聚合了特定功能或技术栈所需的所有常用依赖,并管理了这些依赖的版本兼容性。

作用:

  1. 简化依赖管理: 开发者无需手动添加和管理一大堆单个的依赖,只需引入一个 Starter POM,就可以获得该功能所需的所有核心和传递性依赖。例如,引入 spring-boot-starter-web 会自动引入 Spring MVC、Tomcat、Jackson 等 Web 开发所需的所有依赖。
  2. 版本兼容性: Starter POMs 已经预定义了所有子依赖的最佳兼容版本,避免了由于依赖版本冲突而导致的各种问题。开发者无需再为各个依赖的版本操心。
  3. 约定优于配置: Starter POMs 的命名通常能清晰地表达其功能(如 spring-boot-starter-data-jpa 用于 JPA),与自动配置机制协同工作,使得开发者可以快速搭建特定功能的应用程序。
  4. 提高开发效率: 减少了配置时间和潜在的错误,让开发者可以更专注于业务逻辑的实现。

示例:

  • spring-boot-starter-web:用于构建 Web 应用(包括 RESTful 应用),包含 Spring MVC、Tomcat、Jackson 等。
  • spring-boot-starter-data-jpa:用于集成 Spring Data JPA,包含 Hibernate、Spring ORM 等。
  • spring-boot-starter-test:用于单元测试和集成测试,包含 JUnit、Mockito、Hamcrest、Spring Test 等。
  • spring-boot-starter-security:用于 Spring Security。

5. Spring Boot Actuator 有哪些常用功能?如何启用和使用?

答案:

Spring Boot Actuator 是 Spring Boot 提供的一个生产就绪特性,它为应用程序提供了丰富的监控、管理和诊断功能。

常用功能:

  • 健康检查(Health Check): /actuator/health 端口用于检查应用程序的运行状态,例如数据库连接、磁盘空间、自定义健康指标等。通常用于监控系统或负载均衡器判断服务是否可用。
  • 信息(Info): /actuator/info 端口用于显示应用程序的自定义信息,如版本号、构建信息、Git 提交信息等。
  • 度量指标(Metrics): /actuator/metrics 端口用于暴露应用程序的各种度量数据,如 JVM 内存使用、CPU 使用、HTTP 请求计数、GC 信息等。可以通过 /actuator/metrics/{metricName} 查看具体指标。
  • 环境信息(Environment): /actuator/env 端口用于显示应用程序的所有配置属性,包括系统属性、环境变量、application.properties 中的属性等。
  • Bean 信息(Beans): /actuator/beans 端口显示 Spring 容器中所有 Bean 的列表及其依赖关系。
  • 映射(Mappings): /actuator/mappings 端口显示所有请求路径到控制器的映射关系。
  • 线程信息(Thread Dump): /actuator/threaddump 端口用于生成当前 JVM 的线程 Dump,有助于诊断线程阻塞或死锁问题。
  • 跟踪(Trace): /actuator/httptrace 端口用于显示最近的 HTTP 请求和响应信息。
  • 日志(Loggers): /actuator/loggers 端口用于查看和动态修改应用程序的日志级别。

如何启用和使用:

  1. 添加依赖:pom.xml 中添加 spring-boot-starter-actuator 依赖:

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  2. 配置暴露端点: 默认情况下,Actuator 的大部分端点是关闭的,或者只暴露了 healthinfo。需要在 application.propertiesapplication.yml 中进行配置以暴露更多端点。

    • 暴露所有端点(不推荐在生产环境这样做):

      Properties

      management.endpoints.web.exposure.include=*
      
    • 选择性暴露端点(推荐):

      Properties

      management.endpoints.web.exposure.include=health,info,metrics,env
      
    • 自定义管理端口(可选):

      Properties

      management.server.port=8081
      

      这样 Actuator 端点将通过 8081 端口访问,而应用程序业务逻辑仍通过 8080 端口访问。

  3. 访问: 应用程序启动后,可以通过 http://localhost:8080/actuator(如果未修改端口)或 http://localhost:8081/actuator(如果修改了端口)来访问 Actuator 的端点列表,然后通过具体端点(如 http://localhost:8080/actuator/health)访问相应功能。


6. Spring Boot 如何实现外部化配置?常用的外部化配置方式有哪些?

答案:

Spring Boot 的外部化配置允许您在不修改代码的情况下,在不同环境(开发、测试、生产)中运行相同的应用程序,并使用不同的配置。这是 Spring Boot 生产就绪特性的一个重要组成部分。

实现方式:

Spring Boot 采用**“约定优于配置”**的原则,默认会在特定位置和以特定顺序加载配置文件。它使用了 Spring Environment 抽象来管理配置属性,并提供了丰富的机制来外部化配置。

常用的外部化配置方式(优先级从高到低):

  1. 命令行参数(Command-line arguments): 最高优先级,通过 java -jar your-app.jar --server.port=8081 这种形式直接在命令行中指定属性。

  2. SPRING_APPLICATION_JSON 环境变量: JSON 格式的环境变量,用于在容器环境中传递配置。

  3. ServletConfig 初始化参数: 主要用于传统 WAR 包部署。

  4. ServletContext 初始化参数: 同上。

  5. JNDI 属性: 从 JNDI 获取属性。

  6. 系统属性(System properties): 通过 java -Dspring.profiles.active=prod -jar your-app.jar 这种形式设置的 JVM 系统属性。

  7. 操作系统环境变量(OS environment variables): 操作系统级别的环境变量,如 SERVER_PORT=8081。Spring Boot 会将环境变量名转换为点分表示法(大写字母转换为小写,下划线转换为点)。

  8. 应用程序配置文件:

    • application-{profile}.properties/application-{profile}.yml 特定环境的配置文件,通过 spring.profiles.active 激活。

    • application.properties/application.yml 默认配置文件。

    • 这些文件可以放在以下位置(优先级从高到低):

      • 当前目录的 /config 子目录。
      • 当前目录。
      • classpath 的 /config 包。
      • classpath 的根路径。
  9. @PropertySource 注解: 用于在 Spring 配置类中指定额外的属性文件。

  10. SpringApplication.setDefaultProperties 通过编程方式设置默认属性。

优先级规则:

Spring Boot 按照固定的优先级顺序加载这些配置源。优先级越高的配置源会覆盖优先级低的同名配置。这意味着您可以为开发环境设置默认值,然后通过更高优先级的命令行参数或环境变量在生产环境中覆盖它们。

示例:

假设 application.properties 中有 server.port=8080。 如果您在命令行运行 java -jar myapp.jar --server.port=9090,那么应用程序将监听 9090 端口,因为命令行参数的优先级更高。


7. Spring Boot 如何实现多环境配置?

答案:

Spring Boot 实现多环境配置的核心机制是使用** profiles(配置文件)**。它允许您为不同的环境(如开发、测试、生产)创建不同的配置文件,并在应用程序启动时激活相应的配置。

实现步骤:

  1. 创建多个配置文件:src/main/resources 目录下,除了默认的 application.propertiesapplication.yml,创建以 application-{profile}.propertiesapplication-{profile}.yml 命名的文件。

    • 例如:

      • application.properties (或 application.yml):包含所有环境的通用配置,或者开发环境的默认配置。
      • application-dev.properties (或 application-dev.yml):开发环境特有配置。
      • application-test.properties (或 application-test.yml):测试环境特有配置。
      • application-prod.properties (或 application-prod.yml):生产环境特有配置。
  2. 在特定配置文件中覆盖配置: 在特定环境的配置文件中,您可以覆盖或添加在该环境下特有的属性。 application.properties:

    Properties

    server.port=8080
    spring.datasource.url=jdbc:mysql://localhost:3306/dev_db
    spring.datasource.username=dev_user
    

    application-prod.properties:

    Properties

    server.port=80
    spring.datasource.url=jdbc:mysql://prod_host:3306/prod_db
    spring.datasource.username=prod_user
    
  3. 激活配置文件: 有多种方式可以激活指定的 profile:

    • 通过 application.propertiesapplication.yml 设置默认 profile:

      Properties

      spring.profiles.active=dev
      

      这将默认激活 dev profile。

    • 通过命令行参数: 这是最常用和推荐的方式,尤其是在部署到不同环境时。

      Bash

      java -jar your-app.jar --spring.profiles.active=prod
      
    • 通过环境变量:

      Bash

      export SPRING_PROFILES_ACTIVE=test
      java -jar your-app.jar
      
    • 通过 JVM 系统属性:

      Bash

      java -Dspring.profiles.active=dev -jar your-app.jar
      

优先级与合并:

当激活一个 profile 时,Spring Boot 会加载 application.properties(或 application.yml)以及对应的 application-{profile}.properties(或 application-{profile}.yml)文件。特定 profile 的配置会覆盖默认配置中同名的属性。

示例场景:

您可以在开发环境使用内置的 H2 数据库,在测试环境使用测试数据库,在生产环境使用高可用的 MySQL 数据库。通过激活不同的 profile,应用程序在不同环境下可以自动切换到相应的数据库配置,而无需修改代码。


8. Spring Boot 中 @RestController@Controller 有什么区别?

答案:

@RestController@Controller 都用于标记 Spring MVC 控制器类,但它们在功能和使用场景上有所不同。

  1. @Controller

    • 这是传统的 Spring MVC 控制器注解。
    • 它通常与视图解析器(View Resolver)结合使用,用于返回视图名称,然后视图解析器会解析到具体的视图模板(如 JSP、Thymeleaf、FreeMarker 等)。
    • 方法上通常会配合 @RequestMapping 使用,并返回 String 类型(表示视图名称)、ModelAndView 对象或 void
    • 如果需要返回 JSON 或 XML 等数据,需要在方法上额外添加 @ResponseBody 注解。
  2. @RestController

    • 这是 Spring 4.0 引入的注解,是 @Controller@ResponseBody 的组合注解。
    • 它专门用于构建 RESTful API
    • 默认情况下,被 @RestController 注解的类中的所有方法的返回值都会被自动转换为 HTTP 响应体(JSON 或 XML 等),而不会被解析为视图名称。
    • 无需在每个方法上添加 @ResponseBody 注解,简化了 RESTful API 的开发。
    • 它通常返回 Java 对象,然后由 Spring 的 HttpMessageConverter 自动将其序列化为 JSON 或 XML 格式的数据。

总结表格:

特性@Controller@RestController
主要用途处理 Web 请求,返回视图或少量数据构建 RESTful Web 服务,返回数据(JSON/XML)
@ResponseBody需要在每个返回数据的方法上显式添加自动包含 @ResponseBody,所有方法默认返回数据
返回类型通常返回视图名称(String)、ModelAndView通常返回 Java 对象,自动序列化为 JSON/XML
视图解析器依赖视图解析器来解析视图名称不依赖视图解析器,直接将返回值写入响应体
典型应用后端渲染(SSR),Web 页面控制器前后端分离项目(API 接口)、微服务

选择依据:

  • 如果您正在构建一个传统的 Web 应用程序,其中服务器负责渲染 HTML 页面并返回给客户端(如使用 Thymeleaf、JSP),则使用 @Controller
  • 如果您正在构建一个前后端分离的应用程序,或者一个提供 API 接口的微服务,只返回 JSON 或 XML 数据,则强烈推荐使用 @RestController,它能极大地简化代码。

9. Spring Boot 中如何处理异常?

答案:

Spring Boot 提供了多种方式来处理应用程序中的异常,以确保用户体验和系统稳定性。主要有以下几种方法:

  1. @ExceptionHandler 注解:

    • 作用: 在控制器内部或 @ControllerAdvice 类中,用于处理特定类型的异常。

    • 在控制器内部: 只能处理当前控制器抛出的异常。

      Java

      @RestController
      public class MyController {
      
          @GetMapping("/test-exception")
          public String testException() {
              throw new RuntimeException("Something went wrong!");
          }
      
          @ExceptionHandler(RuntimeException.class)
          public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
              return new ResponseEntity<>("Error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
          }
      }
      
    • @ControllerAdvice 中(推荐): 这是一个全局的异常处理机制。被 @ControllerAdvice 注解的类可以捕获所有控制器中抛出的异常。

      Java

      @ControllerAdvice
      public class GlobalExceptionHandler {
      
          @ExceptionHandler(RuntimeException.class)
          public ResponseEntity<String> handleGlobalRuntimeException(RuntimeException ex) {
              return new ResponseEntity<>("Global Error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
          }
      
          @ExceptionHandler(ResourceNotFoundException.class)
          @ResponseStatus(HttpStatus.NOT_FOUND) // 也可以直接设置HTTP状态码
          public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException ex) {
              return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
          }
      }
      
  2. @ResponseStatus 注解:

    • 作用: 直接标注在自定义异常类上,当该异常被抛出时,Spring 会自动返回指定的 HTTP 状态码。

    • 缺点: 只能返回固定的状态码,无法自定义响应体或进行更复杂的逻辑处理。

    • 示例:

      Java

      @ResponseStatus(HttpStatus.NOT_FOUND)
      public class ResourceNotFoundException extends RuntimeException {
          public ResourceNotFoundException(String message) {
              super(message);
          }
      }
      
  3. ErrorController 接口:

    • Spring Boot 提供了一个默认的 BasicErrorController 来处理所有 Spring MVC 未处理的异常。它会根据请求的 Accept 头来决定返回 HTML 错误页面或 JSON 错误信息。

    • 您可以实现 ErrorController 接口来自定义全局错误页面或错误处理逻辑。

    • 示例:

      Java

      @Controller
      public class MyErrorController implements ErrorController {
      
          @RequestMapping("/error")
          public String handleError(HttpServletRequest request) {
              // 获取错误状态码
              Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
              if (status != null) {
                  Integer statusCode = Integer.valueOf(status.toString());
                  if (statusCode == HttpStatus.NOT_FOUND.value()) {
                      return "error/404"; // 返回自定义的404页面
                  } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
                      return "error/500"; // 返回自定义的500页面
                  }
              }
              return "error/error"; // 默认错误页面
          }
      
          @Override
          public String getErrorPath() {
              return "/error"; // 映射到 /error 路径
          }
      }
      
  4. 自定义 ErrorAttributes

    • Spring Boot 使用 ErrorAttributes 接口来收集错误信息(如时间戳、状态、错误消息等)并将其添加到响应中。
    • 您可以实现自定义的 ErrorAttributes 来修改默认的错误响应内容。

最佳实践:

  • 对于特定业务异常,建议创建自定义异常类,并结合 @ControllerAdvice@ExceptionHandler 进行集中处理,返回统一的错误响应格式(如包含错误码、错误消息等)。
  • 对于一些简单的、只需返回特定 HTTP 状态码的异常,可以考虑在自定义异常类上使用 @ResponseStatus
  • 对于全局的、未被捕获的异常,Spring Boot 的默认错误处理已经足够,如果需要定制错误页面或更复杂的逻辑,可以实现 ErrorController
  • 始终在生产环境中隐藏详细的错误堆栈信息,只暴露必要的错误信息给客户端。

10. Spring Boot 中如何进行数据校验(Validation)?

答案:

Spring Boot 整合了 Bean Validation (JSR 380) 规范,通过在实体类或 DTO 对象的字段上添加注解,可以方便地进行数据校验。它通常与 Hibernate Validator(Bean Validation 的参考实现)一起使用。

步骤:

  1. 添加依赖: 如果使用 spring-boot-starter-web,通常已经包含了 spring-boot-starter-validation 依赖,无需额外添加。如果没有,则需要手动添加:

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 在 DTO 或实体类上添加校验注解: 在需要校验的字段上添加 Bean Validation 提供的注解,如 @NotNull, @NotEmpty, @NotBlank, @Size, @Min, @Max, @Email, @Pattern 等。

    Java

    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Size;
    
    public class UserDto {
    
        @NotBlank(message = "用户名不能为空")
        @Size(min = 3, max = 20, message = "用户名长度必须在3到20之间")
        private String username;
    
        @NotBlank(message = "密码不能为空")
        @Size(min = 6, message = "密码长度至少为6位")
        private String password;
    
        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        private String email;
    
        @NotNull(message = "年龄不能为空")
        @Min(value = 18, message = "年龄必须大于等于18岁")
        private Integer age;
    
        // Getters and Setters
    }
    
  3. 在 Controller 方法参数上添加 @Valid@Validated 在接收 DTO 或实体作为请求参数的方法上,使用 @Valid@Validated 注解来触发校验。

    Java

    import jakarta.validation.Valid;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.FieldError;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class UserController {
    
        @PostMapping("/users")
        public ResponseEntity<?> createUser(@Valid @RequestBody UserDto userDto, BindingResult bindingResult) {
            if (bindingResult.hasErrors()) {
                // 处理校验错误
                StringBuilder errorMessage = new StringBuilder("Validation errors: ");
                for (FieldError error : bindingResult.getFieldErrors()) {
                    errorMessage.append(error.getField()).append(" - ").append(error.getDefaultMessage()).append("; ");
                }
                return new ResponseEntity<>(errorMessage.toString(), HttpStatus.BAD_REQUEST);
            }
            // 校验通过,执行业务逻辑
            System.out.println("User created: " + userDto.getUsername());
            return new ResponseEntity<>("User created successfully!", HttpStatus.CREATED);
        }
    }
    

校验结果处理:

  • BindingResult 在控制器方法参数中添加 BindingResult 对象,可以捕获校验结果。如果 bindingResult.hasErrors()true,则说明校验失败,可以通过 bindingResult.getFieldErrors() 获取详细的错误信息。

  • 全局异常处理: Spring Boot 会将校验失败抛出 MethodArgumentNotValidException 异常。您可以结合 @ControllerAdvice@ExceptionHandler 来全局处理这种异常,返回统一的错误响应格式。

    Java

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.validation.FieldError;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @ControllerAdvice
    public class GlobalValidationExceptionHandler {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
            Map<String, String> errors = new HashMap<>();
            ex.getBindingResult().getAllErrors().forEach((error) -> {
                String fieldName = ((FieldError) error).getField();
                String errorMessage = error.getDefaultMessage();
                errors.put(fieldName, errorMessage);
            });
            return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
        }
    }
    

@Valid vs. @Validated

  • @Valid 是 JSR 380 (Bean Validation) 规范提供的注解,用于触发校验。

  • @Validated 是 Spring 框架提供的注解,它支持分组校验(Validation Groups),允许您定义不同的校验组,并在不同的场景下激活不同的校验规则。例如,您可以定义一个用于创建操作的校验组,一个用于更新操作的校验组。

    Java

    public interface CreateGroup {}
    public interface UpdateGroup {}
    
    public class UserDto {
        @NotBlank(groups = CreateGroup.class)
        private String username;
        // ...
    }
    
    // 在Controller中使用
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@Validated(CreateGroup.class) @RequestBody UserDto userDto) {
        // ...
    }
    

11. 什么是 Spring Boot Profile?它有什么作用?

答案:

Spring Boot Profile 是一种机制,用于根据当前运行环境(如开发、测试、生产)激活特定的配置 Bean 或配置属性。它允许同一个应用程序在不同的环境中以不同的方式运行,而无需修改代码。

作用:

  1. 环境隔离: 允许为不同的环境(例如开发、测试、生产、CI/CD)定义独立的配置,从而避免不同环境之间的配置冲突和混淆。
  2. 灵活部署: 同一个 JAR 包可以在不同的环境下部署,只需通过激活相应的 Profile 即可切换到对应的配置,这大大简化了部署流程。
  3. 资源管理: 可以根据环境配置不同的数据库连接、消息队列地址、API 密钥等资源,确保应用程序连接到正确的外部服务。
  4. 功能开关: 可以利用 Profile 来启用或禁用某些特定环境下的功能。例如,在开发环境中开启 H2 数据库和 Swagger UI,而在生产环境中禁用它们。
  5. 代码复用: 核心业务逻辑代码可以保持不变,通过 Profile 机制切换不同的基础设施配置。

如何使用(复习):

  • 定义配置文件: 创建 application-{profile}.propertiesapplication-{profile}.yml 文件。

  • 激活 Profile:

    • application.properties 中设置 spring.profiles.active=dev
    • 通过命令行参数:java -jar your-app.jar --spring.profiles.active=prod
    • 通过环境变量:SPRING_PROFILES_ACTIVE=test
    • 通过 JVM 系统属性:java -Dspring.profiles.active=dev

示例:

假设您有一个 DataSource Bean。您可以通过 @Profile 注解来定义不同环境下使用的 Bean:

Java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class DataSourceConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        // 配置开发环境的H2数据库
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        // 配置生产环境的MySQL数据库
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://prod_host:3306/prod_db");
        dataSource.setUsername("prod_user");
        dataSource.setPassword("prod_password");
        return dataSource;
    }
}

当激活 dev profile 时,devDataSource Bean 会被创建;当激活 prod profile 时,prodDataSource Bean 会被创建。


12. Spring Boot DevTools 是什么?它有什么作用?

答案:

Spring Boot DevTools(开发者工具)是一个为 Spring Boot 开发者提供的模块,旨在提高开发效率。它提供了多种开发时友好的功能,可以减少手动操作,加快开发循环。

作用:

  1. 自动重启(Automatic Restart):

    • 当 classpath 中的文件(如类、配置文件)发生更改时,DevTools 会自动重启应用程序。这比手动停止和启动应用程序要快得多,因为 Spring Boot 使用了一个特殊的类加载器,只有修改的类才需要重新加载。
    • 默认情况下,classpath 中的所有内容都会被监控。
  2. 实时加载(Live Reload):

    • 与一个浏览器插件(如 LiveReload 浏览器扩展)配合使用时,当静态资源(如 HTML、CSS、JavaScript)发生更改时,浏览器会自动刷新页面,无需手动刷新。
    • 这对于前端开发非常有用。
  3. 全局设置(Global Settings):

    • 提供了一些全局配置,例如,默认关闭 Thymeleaf 的缓存,这在开发期间非常有用,可以立即看到模板文件的修改效果。
  4. 远程调试(Remote Debugging):

    • 允许通过 SSH 或 HTTP 代理连接到远程运行的 Spring Boot 应用程序进行调试,方便远程故障排查。
  5. 默认属性覆盖(Default Property Overrides):

    • DevTools 会为一些常用的属性提供开发时的默认值,例如,缓存通常在开发时被禁用(如 Thymeleaf 缓存),以确保每次修改都能立即生效。

如何启用和使用:

  1. 添加依赖:pom.xml 中添加 spring-boot-devtools 依赖。注意:这个依赖通常设置为 optional,这意味着它不会被传递到生产环境的包中,确保生产环境不会包含不必要的开发工具。

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional> </dependency>
    
  2. IDE 配置(针对自动重启):

    • IntelliJ IDEA: 需要进行额外配置才能启用自动编译和重启。

      • 打开 File -> Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors,确保 Enable annotation processing 选中。
      • 打开 File -> Settings -> Build, Execution, Deployment -> Compiler,勾选 Build project automatically
      • 使用快捷键 Ctrl + Shift + A (Windows/Linux) 或 Cmd + Shift + A (Mac) 搜索 Registry...,找到并勾选 compiler.automake.allow.when.app.running
    • Eclipse: 通常会自动工作,或者确保 Project -> Build Automatically 被选中。

  3. 修改代码或资源: 正常修改您的 Java 代码、属性文件、HTML/CSS/JS 文件等,DevTools 会自动检测并触发相应的操作(重启或浏览器刷新)。

注意事项:

  • 不要在生产环境部署包含 DevTools 的应用程序。 正如上面所说,通过 optional 设置或在打包时排除可以避免这种情况。
  • 自动重启是基于类加载器实现的,对于大量修改,仍然会有一定的延迟。

13. Spring Boot 中的 @SpringBootApplication 注解具体包含哪些注解?它们各自的作用是什么?

答案:

@SpringBootApplication 是一个组合注解(meta-annotation),它是 Spring Boot 应用程序的入口点。它包含了三个核心注解,共同构成了 Spring Boot 应用的基本配置和功能。

这三个核心注解及其作用是:

  1. @SpringBootConfiguration

    • 作用: 这是一个特殊的 @Configuration 注解,用于标记主配置类。它表明当前类是一个 Spring 配置类,可以定义 Bean。
    • 特点: 与普通的 @Configuration 相比,它没有额外功能,但 Spring Boot 会优先识别带有此注解的类作为主配置类。
  2. @EnableAutoConfiguration

    • 作用: 这是 Spring Boot 自动配置机制的核心。它根据项目中引入的 Maven 或 Gradle 依赖,自动推断并配置应用程序所需的 Bean。
    • 实现原理: 通过扫描 META-INF/spring.factories 文件中定义的自动配置类,并结合条件化注解(如 @ConditionalOnClass, @ConditionalOnMissingBean 等)来决定哪些自动配置应该生效。
    • 例子: 如果引入了 spring-boot-starter-web,它会自动配置 DispatcherServlet、内嵌的 Tomcat、JSON 转换器等。
  3. @ComponentScan

    • 作用: 启用 Spring 的组件扫描机制。它会扫描指定包及其子包下的所有 Spring 组件(如 @Component, @Service, @Repository, @Controller, @RestController 等),并将它们注册为 Spring Bean。
    • 默认行为: 如果不指定 basePackagesbasePackageClasses@ComponentScan 默认会扫描当前注解所在的包及其子包。
    • 最佳实践: 通常将 @SpringBootApplication 放在项目的根包下,这样可以确保所有组件都能被扫描到。

总结:

@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan

  • @SpringBootConfiguration:声明这是一个 Spring 配置类。
  • @EnableAutoConfiguration:启用 Spring Boot 的自动配置功能。
  • @ComponentScan:启用组件扫描,发现并注册 Spring 组件。

通过这三个注解的组合,Spring Boot 应用程序能够快速启动,并自动配置大部分常用功能,同时允许开发者在特定情况下进行自定义配置和组件发现。


14. Spring Boot 中如何集成 Mybatis?

答案:

在 Spring Boot 中集成 Mybatis 非常简单,通常通过 mybatis-spring-boot-starter 来实现。

步骤:

  1. 添加 Maven 依赖:pom.xml 中添加 Mybatis 和数据库驱动(以 MySQL 为例)的 Starter 依赖。

    XML

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.1</version> </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
  2. 配置数据源:application.propertiesapplication.yml 中配置数据库连接信息。Spring Boot 会自动配置 HikariCP (默认连接池)。

    Properties

    # application.properties
    spring.datasource.url=jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=your_password
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
  3. 编写 Mapper 接口: 创建 Mybatis 的 Mapper 接口,用于定义数据库操作方法。

    Java

    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import com.example.demo.model.User; // 假设有User实体类
    
    @Mapper // 标记这是一个Mybatis Mapper接口
    public interface UserMapper {
    
        @Select("SELECT * FROM users WHERE id = #{id}")
        User getUserById(Long id);
    
        // 也可以定义更复杂的SQL操作,通过XML映射文件实现
        // List<User> getAllUsers();
        // void insertUser(User user);
    }
    
  4. 编写实体类(可选,如果使用 Mybatis-Plus 则需要):

    Java

    public class User {
        private Long id;
        private String name;
        private Integer age;
        // Getters and Setters
    }
    
  5. 在 Spring Boot 主应用类上添加 @MapperScan 在主应用类上添加 @MapperScan 注解,指定 Mapper 接口所在的包路径,Spring Boot 会自动扫描并注册这些 Mapper。

    Java

    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan("com.example.demo.mapper") // 指定Mapper接口的包路径
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
  6. 在 Service 层或 Controller 层注入并使用 Mapper:

    Java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import com.example.demo.mapper.UserMapper;
    import com.example.demo.model.User;
    
    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        public User findUserById(Long id) {
            return userMapper.getUserById(id);
        }
    }
    

XML 映射文件配置(可选):

如果您偏好使用 XML 映射文件来编写 SQL,可以在 application.properties 中配置 Mybatis 映射文件的位置:

Properties

mybatis.mapper-locations=classpath*:mapper/*.xml

然后在 src/main/resources/mapper 目录下创建相应的 XML 文件,例如 UserMapper.xml

XML

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    <select id="getAllUsers" resultType="com.example.demo.model.User">
        SELECT id, name, age FROM users
    </select>
</mapper>

确保 UserMapper.java 中有对应的方法签名:List<User> getAllUsers();

集成 Mybatis-Plus(推荐):

Mybatis-Plus 是一个 Mybatis 增强工具,它提供了大量的 CRUD 方法,大大简化了开发。集成 Mybatis-Plus 的步骤类似,只需将 mybatis-spring-boot-starter 替换为 mybatis-plus-spring-boot-starter,并在 Mapper 接口继承 BaseMapper<T>

XML

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version> </dependency>

Java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import com.example.demo.model.User;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 继承BaseMapper后,无需再写常用的CRUD方法
    // 也可以在此处定义自定义方法
}

15. Spring Boot 中集成 Redis 有哪些方式?

答案:

Spring Boot 提供了对 Redis 的良好集成,主要通过 Spring Data Redis 来实现。它支持两种主要的客户端连接库:Lettuce(默认)和 Jedis

集成方式:

  1. 添加依赖:pom.xml 中添加 spring-boot-starter-data-redis 依赖。它默认会引入 Lettuce 客户端。

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 配置 Redis 连接信息:application.propertiesapplication.yml 中配置 Redis 服务器的连接信息。

    Properties

    # application.properties
    spring.data.redis.host=localhost
    spring.data.redis.port=6379
    spring.data.redis.password= # 如果有密码
    spring.data.redis.timeout=5000ms
    spring.data.redis.database=0 # 默认数据库
    # 连接池配置 (Lettuce 默认使用连接池)
    spring.data.redis.lettuce.pool.max-active=8
    spring.data.redis.lettuce.pool.max-idle=8
    spring.data.redis.lettuce.pool.min-idle=0
    spring.data.redis.lettuce.pool.max-wait=-1ms # -1表示永不超时
    
  3. 使用 RedisTemplateStringRedisTemplate Spring Data Redis 提供了 RedisTemplateStringRedisTemplate 作为与 Redis 交互的核心类。

    • RedisTemplate<K, V> 默认使用 JDK 序列化器,可以存储任意 Java 对象。但如果需要跨语言交互或存储可读数据,需要自定义序列化器。
    • StringRedisTemplate RedisTemplate 的子类,专门用于操作字符串类型(键和值都使用 StringRedisSerializer),更适合存储 JSON、XML 等文本数据。

    Java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    
    import java.util.concurrent.TimeUnit;
    
    @Service
    public class RedisService {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate; // 更常用,适合存储JSON等文本
    
        @Autowired
        private RedisTemplate<Object, Object> redisTemplate; // 默认使用JDK序列化器
    
        // 使用StringRedisTemplate
        public void setString(String key, String value) {
            stringRedisTemplate.opsForValue().set(key, value);
        }
    
        public String getString(String key) {
            return stringRedisTemplate.opsForValue().get(key);
        }
    
        public void setStringWithExpire(String key, String value, long timeout, TimeUnit unit) {
            stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
        }
    
        // 使用RedisTemplate存储Java对象(需要自定义序列化器,否则默认使用JDK序列化)
        public void setObject(String key, Object value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        public Object getObject(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    }
    
  4. 自定义 RedisTemplate 序列化器(推荐): 默认的 RedisTemplate 使用 JDK 序列化器,存储到 Redis 中的数据是二进制的,不易读,且可能存在兼容性问题。通常会配置为使用 JSON 序列化器(如 GenericJackson2JsonRedisSerializerFastJsonRedisSerializer)来存储 Java 对象。

    Java

    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    public class RedisConfig {
    
        @Bean
        @SuppressWarnings("all")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(factory);
    
            // Json序列化配置
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 解决反序列化时类型转换问题
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            // String序列化
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
            return template;
        }
    }
    
  5. 使用 Spring Cache 整合 Redis: Spring Boot 的缓存抽象层可以轻松与 Redis 集成,只需添加 spring-boot-starter-cache 依赖,并配置 Redis 作为缓存提供者。

    Properties

    # application.properties
    spring.cache.type=redis
    

    然后在需要缓存的方法上使用 @Cacheable, @CachePut, @CacheEvict 等注解。

    Java

    import org.springframework.cache.annotation.CacheConfig;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    @Service
    @CacheConfig(cacheNames = "users") // 定义缓存名称
    public class UserService {
    
        @Cacheable(key = "#id") // 缓存方法返回值,key为id
        public User getUserById(Long id) {
            System.out.println("Getting user from DB: " + id);
            // 模拟从数据库查询
            return new User(id, "User " + id);
        }
    
        @CacheEvict(key = "#id") // 删除缓存
        public void deleteUser(Long id) {
            System.out.println("Deleting user from DB: " + id);
            // 模拟删除数据库数据
        }
    }
    

16. Spring Boot 中如何使用定时任务?

答案:

Spring Boot 使用 Spring Framework 提供的 @EnableScheduling@Scheduled 注解来方便地创建和管理定时任务。

步骤:

  1. 在主应用类或配置类上启用调度: 在 Spring Boot 的主应用程序类上(通常是 @SpringBootApplication 所在的类)或任何一个 @Configuration 注解的类上添加 @EnableScheduling 注解。

    Java

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    @EnableScheduling // 启用定时任务
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
  2. 创建定时任务类和方法: 创建一个普通的 Spring Bean(使用 @Component@Service 等注解),并在需要执行定时任务的方法上添加 @Scheduled 注解。

    Java

    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import java.time.LocalDateTime;
    
    @Component // 确保Spring能够扫描到这个Bean
    public class MyScheduler {
    
        // 固定速率执行,每5秒执行一次,不论上次任务是否完成
        @Scheduled(fixedRate = 5000)
        public void fixedRateTask() {
            System.out.println("Fixed Rate Task - " + LocalDateTime.now());
        }
    
        // 固定延迟执行,上次任务完成后,等待5秒再执行下一次任务
        @Scheduled(fixedDelay = 5000)
        public void fixedDelayTask() {
            try {
                Thread.sleep(2000); // 模拟任务执行时间
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Fixed Delay Task - " + LocalDateTime.now());
        }
    
        // 首次延迟5秒后,每3秒执行一次(fixedRate)
        @Scheduled(initialDelay = 5000, fixedRate = 3000)
        public void initialDelayFixedRateTask() {
            System.out.println("Initial Delay Fixed Rate Task - " + LocalDateTime.now());
        }
    
        // 使用 Cron 表达式执行定时任务(例如:每天凌晨2点30分执行)
        // Cron表达式:秒 分 时 日 月 周
        @Scheduled(cron = "0 30 2 * * ?") // 每天凌晨2点30分
        public void cronTask() {
            System.out.println("Cron Task executed at " + LocalDateTime.now());
        }
    }
    

@Scheduled 注解的常用属性:

  • fixedRate 固定速率执行。从上一次任务开始时算起,经过指定时间后再次执行。不关心上次任务是否完成。单位是毫秒。
  • fixedDelay 固定延迟执行。从上一次任务完成时算起,经过指定时间后再次执行。单位是毫秒。
  • initialDelay 首次执行的延迟时间。应用程序启动后,等待指定时间后第一次执行任务。单位是毫秒。
  • cron 使用 Cron 表达式定义任务执行时间。Cron 表达式是一个字符串,表示秒、分、时、日、月、周(可选年份)。例如,"0 0 10 * * ?" 表示每天上午10点整执行。

注意事项:

  • 默认单线程: 默认情况下,所有 @Scheduled 注解的任务都在一个单线程中执行。如果任务执行时间较长,可能会阻塞后续任务的执行。

  • 配置线程池: 对于并发的定时任务,建议配置一个线程池来处理。可以通过实现 SchedulingConfigurer 接口或自定义 TaskScheduler Bean 来配置。

    Java

    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    
    @Configuration
    public class SchedulerConfig implements SchedulingConfigurer {
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
            taskScheduler.setPoolSize(10); // 设置线程池大小
            taskScheduler.setThreadNamePrefix("my-scheduled-task-");
            taskScheduler.initialize();
            taskRegistrar.setTaskScheduler(taskScheduler);
        }
    }
    
  • 分布式定时任务: Spring 自带的 @Scheduled 适用于单机应用。如果应用程序部署在多个实例上,或者需要更复杂的任务调度功能(如任务持久化、故障转移、管理界面),则需要考虑使用专业的分布式调度框架,如 QuartzXXL-JobElasticJob 等。


17. Spring Boot 实现事务管理的方式?

答案:

Spring Boot 利用 Spring Framework 的事务管理能力,提供了声明式事务和编程式事务两种方式。在 Spring Boot 中,声明式事务是更常用和推荐的方式。

1. 声明式事务(推荐):

声明式事务通过 AOP 实现,将事务管理与业务逻辑代码解耦。主要通过 @Transactional 注解来实现。

步骤:

  • 添加依赖: 如果您使用的是 spring-boot-starter-data-jpaspring-boot-starter-jdbc,通常已经包含了事务所需的依赖。

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
  • 配置数据源和事务管理器: Spring Boot 会自动配置数据源和事务管理器(例如 DataSourceTransactionManagerJpaTransactionManager),无需手动配置。

  • 在 Service 层方法上使用 @Transactional 注解: 这是最常见的用法。将 @Transactional 注解应用到方法或类上,Spring 会在方法执行前后自动管理事务。

    Java

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class OrderService {
    
        @Autowired
        private OrderRepository orderRepository; // 假设是JPA Repository
    
        @Autowired
        private StockService stockService; // 假设另一个服务
    
        @Transactional // 声明式事务
        public void createOrderAndReduceStock(Order order, int quantity) {
            // 1. 创建订单
            orderRepository.save(order);
    
            // 2. 扣减库存 (如果stockService的reduceStock方法抛异常,整个事务会回滚)
            stockService.reduceStock(order.getProductId(), quantity);
    
            // 模拟一个异常,触发事务回滚
            // if (quantity > 100) {
            //     throw new RuntimeException("Quantity too large, transaction will rollback!");
            // }
        }
    
        @Transactional(readOnly = true) // 只读事务,优化性能
        public Order getOrderById(Long id) {
            return orderRepository.findById(id).orElse(null);
        }
    }
    

@Transactional 常用属性:

  • propagation 事务的传播行为。定义当一个事务方法被另一个事务方法调用时,如何参与事务。

    • REQUIRED (默认): 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    • REQUIRES_NEW 总是创建一个新事务,如果当前存在事务,则将当前事务挂起。
    • SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
    • NOT_SUPPORTED 总是以非事务方式执行,如果当前存在事务,则将当前事务挂起。
    • MANDATORY 必须在现有事务中运行,否则抛出异常。
    • NEVER 必须不在事务中运行,否则抛出异常。
    • NESTED 如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务。
  • isolation 事务的隔离级别。定义一个事务对其他并发事务的影响程度。

    • DEFAULT (默认): 使用底层数据库的默认隔离级别。
    • READ_UNCOMMITTED 读未提交。脏读、不可重复读、幻读都可能发生。
    • READ_COMMITTED 读已提交。避免脏读,但可能出现不可重复读和幻读。
    • REPEATABLE_READ 可重复读。避免脏读和不可重复读,但可能出现幻读。
    • SERIALIZABLE 串行化。最高隔离级别,避免所有并发问题,但性能最低。
  • rollbackFor 指定哪些异常会触发事务回滚。默认情况下,Spring 只对运行时异常(RuntimeException 及其子类)和 Error 进行回滚。

  • noRollbackFor 指定哪些异常不会触发事务回滚。

  • readOnly 设置为 true 表示事务是只读的,可以优化性能。对于查询操作,建议设置为 true

  • timeout 事务的超时时间(秒)。

2. 编程式事务:

编程式事务通过 TransactionTemplate 或直接使用 PlatformTransactionManager 来实现。它将事务管理代码嵌入到业务逻辑中。

使用 TransactionTemplate

Java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class AnotherOrderService {

    private final TransactionTemplate transactionTemplate;
    // ... 其他依赖

    // 构造器注入TransactionTemplate
    public AnotherOrderService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void createOrderWithProgrammaticTransaction(Order order, int quantity) {
        transactionTemplate.execute(status -> {
            try {
                // 1. 创建订单
                // orderRepository.save(order);

                // 2. 扣减库存
                // stockService.reduceStock(order.getProductId(), quantity);

                System.out.println("Executing business logic in programmatic transaction...");
                // 模拟业务逻辑和潜在异常
                if (quantity > 100) {
                    throw new RuntimeException("Quantity too large for programmatic transaction, will rollback!");
                }
                return null; // 正常返回
            } catch (Exception e) {
                status.setRollbackOnly(); // 标记事务为回滚
                throw e; // 重新抛出异常
            }
        });
    }
}

选择哪种方式?

  • 声明式事务 (@Transactional): 强烈推荐。它更简洁、更符合 Spring 的 AOP 思想,将事务管理与业务逻辑解耦,提高了代码的可读性和维护性。适用于大多数场景。
  • 编程式事务: 适用于需要对事务进行更精细控制的复杂场景,或者在某些特殊情况下无法使用声明式事务的场景。但在大多数情况下,应该优先考虑声明式事务。

18. Spring Boot 如何优雅地关闭应用程序?

答案:

在 Spring Boot 中,优雅地关闭应用程序意味着在应用程序接收到关闭信号时,能够完成正在进行的任务、释放资源、保存状态,然后平稳地停止,而不是突然终止。这对于生产环境的稳定性和数据一致性至关重要。

Spring Boot 提供了多种机制来实现优雅关闭:

  1. 内嵌服务器的优雅关闭(Graceful Shutdown):

    • Spring Boot 2.2 以后,内嵌的 Tomcat、Jetty 和 Undertow 都支持优雅关闭。

    • 配置方式:application.propertiesapplication.yml 中配置。

      Properties

      # application.properties
      server.shutdown.graceful=true
      server.shutdown.timeout=30s # 等待的最大时间,默认30秒
      
    • 作用: 当应用程序收到关闭信号(如 SIGTERM 或通过 Actuator /actuator/shutdown),服务器会停止接收新的请求,并等待已接收的请求处理完成,直到超时或所有请求完成。

  2. Spring Boot Actuator 的 /actuator/shutdown 端点:

    • 作用: 提供一个 HTTP 端点,可以通过发送 POST 请求来触发应用程序的关闭。

    • 启用: 默认是禁用的,需要在 application.properties 中开启:

      Properties

      management.endpoints.web.exposure.include=shutdown
      management.endpoint.shutdown.enabled=true
      
    • 安全性: 非常重要! 默认情况下,该端点是无需认证即可访问的,这在生产环境是巨大的安全隐患。务必配置 Spring Security 或其他认证授权机制来保护此端点。

    • 使用方式:POST /actuator/shutdown 发送请求。

  3. 使用 ApplicationListener@PreDestroy 进行资源释放:

    • 对于自定义的资源(如线程池、消息队列连接、自定义缓存等),您可以在应用程序关闭时执行清理逻辑。

    • ApplicationListener<ContextClosedEvent> 监听 Spring 应用程序上下文关闭事件。在容器销毁 Bean 之前触发。

      Java

      import org.springframework.context.ApplicationListener;
      import org.springframework.context.event.ContextClosedEvent;
      import org.springframework.stereotype.Component;
      
      @Component
      public class AppShutdownListener implements ApplicationListener<ContextClosedEvent> {
      
          @Override
          public void onApplicationEvent(ContextClosedEvent event) {
              System.out.println("Application Context is closing. Performing cleanup...");
              // 执行资源释放、数据保存等操作
              // 例如:关闭自定义线程池、清空缓存、发送通知等
              System.out.println("Cleanup completed.");
          }
      }
      
    • @PreDestroy 注解: 标注在 Bean 的方法上,在 Bean 被销毁之前执行。适用于释放特定 Bean 拥有的资源。

      Java

      import org.springframework.stereotype.Component;
      import jakarta.annotation.PreDestroy;
      
      @Component
      public class CustomResourceProcessor {
      
          public CustomResourceProcessor() {
              System.out.println("CustomResourceProcessor initialized.");
          }
      
          @PreDestroy
          public void cleanup() {
              System.out.println("CustomResourceProcessor is being destroyed. Releasing resources...");
              // 关闭数据库连接、关闭文件句柄、释放内存等
          }
      }
      
  4. SpringApplication.exit()

    • 通过编程方式调用此方法来优雅地关闭应用程序。

    Java

    import org.springframework.boot.SpringApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    
    public class ShutdownExample {
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(YourApplication.class, args);
            // 业务逻辑...
    
            // 在某个条件满足时,优雅关闭
            // context.close(); // 或者
            // SpringApplication.exit(context, () -> 0);
        }
    }
    

总结与最佳实践:

  • 内嵌服务器优雅关闭(server.shutdown.graceful=true)是基础和首要配置,它确保正在处理的 HTTP 请求能够完成。
  • 保护 /actuator/shutdown 端点,防止未经授权的关闭。
  • 对于应用程序自身的业务逻辑和自定义资源,使用 ApplicationListener<ContextClosedEvent>@PreDestroy 来执行清理工作。ApplicationListener 更适合全局的应用程序关闭逻辑,而 @PreDestroy 适用于特定 Bean 的资源释放。
  • 在容器化环境(如 Docker、Kubernetes)中,确保容器编排工具发送 SIGTERM 信号给 Spring Boot 应用程序,以便它有机会执行优雅关闭逻辑。

19. Spring Boot 中如何集成 WebSocket?

答案:

Spring Boot 提供了对 WebSocket 的良好支持,通过 Spring Framework 的 WebSocket 模块,可以轻松地实现全双工通信。主要有两种集成方式:低级 WebSocket API 和 STOMP 消息协议。通常,STOMP 方式更常用,因为它提供了更高级的、基于消息的抽象,简化了客户端和服务端的交互。

1. 集成 STOMP 消息协议(推荐):

STOMP (Simple Text Oriented Messaging Protocol) 是一种简单的文本消息协议,它定义了消息的帧格式,使得客户端和服务器之间的通信更加结构化。

步骤:

  • 添加依赖:

    XML

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
  • 配置 WebSocket 消息代理: 创建一个配置类,继承 AbstractWebSocketMessageBrokerConfigurerWebSocketMessageBrokerConfigurer,并重写必要的方法。

    Java

    import org.springframework.context.annotation.Configuration;
    import org.springframework.messaging.simp.config.MessageBrokerRegistry;
    import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
    import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
    import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
    
    @Configuration
    @EnableWebSocketMessageBroker // 启用WebSocket消息代理
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            // 配置消息代理
            // 1. 设置应用请求的前缀,所有客户端发送到服务器的消息都必须以 /app 开头
            config.setApplicationDestinationPrefixes("/app");
            // 2. 设置订阅消息的前缀,客户端订阅的路径都必须以 /topic 或 /queue 开头
            // /topic 用于广播,/queue 用于点对点
            config.enableSimpleBroker("/topic", "/queue");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            // 注册一个STOMP端点,客户端将通过这个端点连接WebSocket
            // `withSockJS()` 启用SockJS支持,当浏览器不支持WebSocket时,会回退到其他传输方式
            registry.addEndpoint("/ws").withSockJS();
        }
    }
    
  • 创建 STOMP 消息控制器: 使用 @MessageMapping@SendTo 注解来处理客户端发送的消息和向客户端发送消息。

    Java

    import org.springframework.messaging.handler.annotation.MessageMapping;
    import org.springframework.messaging.handler.annotation.SendTo;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.util.HtmlUtils;
    
    @Controller
    public class GreetingController {
    
        @MessageMapping("/hello") // 客户端发送消息到 /app/hello
        @SendTo("/topic/greetings") // 将消息发送到 /topic/greetings 订阅的客户端
        public Greeting greeting(HelloMessage message) throws Exception {
            Thread.sleep(1000); // 模拟处理延迟
            return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
        }
    
        // 可以通过SimpMessagingTemplate注入来主动发送消息
        // @Autowired
        // private SimpMessagingTemplate messagingTemplate;
        // public void sendToUser(String user, String message) {
        //     messagingTemplate.convertAndSendToUser(user, "/queue/messages", message);
        // }
    }
    
    • HelloMessage.java (消息入参):

      Java

      public class HelloMessage {
          private String name;
          // Getters and Setters, constructor
      }
      
    • Greeting.java (消息出参):

      Java

      public class Greeting {
          private String content;
          // Getters and Setters, constructor
      }
      
  • 前端(JavaScript)连接和发送/接收消息: 使用 SockJS 和 Stomp.js 库进行连接。

    HTML

    <!DOCTYPE html>
    <html>
    <head>
        <title>WebSocket Chat</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
        <script type="text/javascript">
            var stompClient = null;
    
            function connect() {
                var socket = new SockJS('/ws'); // 连接我们注册的STOMP端点
                stompClient = Stomp.over(socket);
                stompClient.connect({}, function (frame) {
                    console.log('Connected: ' + frame);
                    // 订阅消息
                    stompClient.subscribe('/topic/greetings', function (greeting) {
                        showGreeting(JSON.parse(greeting.body).content);
                    });
                });
            }
    
            function disconnect() {
                if (stompClient !== null) {
                    stompClient.disconnect();
                }
                console.log("Disconnected");
            }
    
            function sendName() {
                stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
            }
    
            function showGreeting(message) {
                $("#greetings").append("<tr><td>" + message + "</td></tr>");
            }
    
            $(function () {
                $("form").on('submit', function (e) {
                    e.preventDefault();
                });
                $("#connect").click(function() { connect(); });
                $("#disconnect").click(function() { disconnect(); });
                $("#send").click(function() { sendName(); });
            });
        </script>
    </head>
    <body>
        <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript to work.</h2></noscript>
        <div>
            <button id="connect">Connect</button>
            <button id="disconnect">Disconnect</button>
            <br>
            <label>What is your name?</label><input type="text" id="name" placeholder="Your name here...">
            <button id="send">Send</button>
            <br>
            <table id="greetings">
                <thead>
                    <tr>
                        <th>Greetings</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
    </body>
    </html>
    

2. 低级 WebSocket API (不常用,除非有特殊需求):

这种方式直接使用 WebSocket 协议,不涉及 STOMP。需要实现 WebSocketHandler 接口。

  • 添加依赖: 同上,spring-boot-starter-websocket

  • 配置 WebSocket:

    Java

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.config.annotation.EnableWebSocket;
    import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
    
    @Configuration
    @EnableWebSocket // 启用WebSocket
    public class MyWebSocketConfig implements WebSocketConfigurer {
    
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(new MyTextWebSocketHandler(), "/my-websocket")
                    .withSockJS(); // 同样可以启用SockJS
        }
    }
    
  • 创建 WebSocket Handler:

    Java

    import org.springframework.web.socket.CloseStatus;
    import org.springframework.web.socket.TextMessage;
    import org.springframework.web.socket.WebSocketSession;
    import org.springframework.web.socket.handler.TextWebSocketHandler;
    
    public class MyTextWebSocketHandler extends TextWebSocketHandler {
    
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
            System.out.println("WebSocket connection established: " + session.getId());
            session.sendMessage(new TextMessage("Hello from server!"));
        }
    
        @Override
        protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
            System.out.println("Received message: " + message.getPayload() + " from " + session.getId());
            session.sendMessage(new TextMessage("Echo: " + message.getPayload()));
        }
    
        @Override
        public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
            System.out.println("WebSocket connection closed: " + session.getId() + " status: " + status);
        }
    }
    

总结:

  • **对于大多数实时通信场景,特别是需要消息路由、订阅、点对点发送等功能时,强烈推荐使用 STOMP 方式。**它提供了更高级别的抽象,使得开发更加简单和直观。
  • 低级 WebSocket API 更适合那些需要完全控制 WebSocket 帧和协议细节的特殊用例。

20. Spring Boot 打包成可执行 Jar 包的原理是什么?

答案:

Spring Boot 应用程序能够打包成一个独立的、可执行的 JAR 包,这被称为“胖 Jar”(Fat Jar)或“可执行 Jar”,其核心原理依赖于 Maven 或 Gradle 的插件以及特殊的类加载机制。

主要原理和机制:

  1. Maven/Gradle 插件支持:

    • Maven: 主要依赖于 spring-boot-maven-plugin
    • Gradle: 主要依赖于 spring-boot-gradle-plugin
    • 这些插件在项目打包(mvn packagegradle bootJar)时,会将应用程序的所有依赖(包括 Spring Boot 自身的核心库、所有 Starter 依赖以及您的项目代码)都打包到一个单独的 JAR 文件中。
  2. 特殊的 JAR 结构: 生成的 JAR 包不再是传统的扁平结构,而是一个特殊的嵌套 JAR 结构。它的内部通常包含:

    • META-INF/MANIFEST.MF 清单文件。其中包含了一个特殊的 Start-Class 属性,指向您的 Spring Boot 应用程序的启动类(通常是带有 main 方法和 @SpringBootApplication 注解的类),以及一个 Spring-Boot-Classes 属性,指向应用程序的类路径。
    • BOOT-INF/classes/ 存放您自己的编译后的类文件(.class)和资源文件。
    • BOOT-INF/lib/ 存放所有依赖的 JAR 包。这些依赖不再是直接在 JAR 包的根目录下,而是嵌套在这个目录中。
    • org/springframework/boot/loader/ 这是一个非常关键的目录,包含了 Spring Boot 自己的类加载器代码。
  3. 自定义类加载器(LaunchedURLClassLoader):

    • 当您使用 java -jar your-app.jar 命令执行这个胖 Jar 时,Java 虚拟机会首先加载 JAR 包中的 META-INF/MANIFEST.MF 文件,并找到 Main-Class 属性。
    • Main-Class 属性通常指向 Spring Boot 提供的 org.springframework.boot.loader.JarLauncher 或其子类(例如 org.springframework.boot.loader.WarLauncher)。
    • JarLauncher 不会直接加载您的应用程序类,而是会启动它自己内部的自定义类加载器(通常是 LaunchedURLClassLoader)。
    • 这个自定义类加载器能够识别并加载 BOOT-INF/classes/ 目录下的您自己的类,以及 BOOT-INF/lib/ 目录下嵌套的依赖 JAR 包。
    • 它解决了 Java 标准类加载器无法直接加载嵌套 JAR 包的问题。
  4. 内嵌 Web 服务器:

    • spring-boot-starter-web 被引入时,Spring Boot 会自动配置内嵌的 Tomcat、Jetty 或 Undertow 服务器。
    • 这些服务器的 JAR 包也被打包到了 BOOT-INF/lib/ 目录中。
    • 在应用程序启动时,Spring Boot 会启动这个内嵌的 Web 服务器,使其监听指定端口并处理 HTTP 请求,从而无需额外安装和配置外部 Web 服务器。

总结流程:

  1. 打包阶段: spring-boot-maven-plugin(或 Gradle 插件)负责收集所有项目代码和依赖,将它们按照特定的目录结构打包到一个 JAR 文件中,并生成特殊的 MANIFEST.MF 文件。
  2. 运行阶段: java -jar 命令执行后,JarLauncher 作为入口点被加载,它会创建并启动一个自定义的 LaunchedURLClassLoader
  3. 类加载: LaunchedURLClassLoader 负责从 JAR 包内部的 BOOT-INF/classes/BOOT-INF/lib/ 目录中加载应用程序的类和所有依赖的类。
  4. 启动应用: 最终,通过反射机制调用 MANIFEST.MFStart-Class 指定的应用程序主类的 main 方法,启动 Spring 上下文和内嵌的 Web 服务器。