服务端5月30日 21:29
Spring Boot 如何正确实现 CSRF 防护?Spring Boot 里的 CSRF 防护不要一上来就关掉。只要项目还依赖浏览器 Cookie 维持登录态,POST、PUT、DELETE 这类改数据请求就可能被第三方页面借用户身份发出去。正确做法是保留 Spring Security 的 CSRF 校验:服务端生成 Token,前端在表单或请求头里带回,服务端再验证它是否属于当前会话。
## 追问
### CookieCsrfTokenRepository 为什么要 withHttpOnlyFalse?
SPA 需要从 Cookie 读取 Token,再写入 `X-CSRF-TOKEN` 请求头。代价是脚本也能读到它,所以站点存在 XSS 时风险会被放大。
### CSRF Token 和 SameSite 是二选一吗?
不是。SameSite 是浏览器层面的减风险措施,Token 是服务端验证用户意图。敏感业务最好两者都用。
### AJAX 一直 403 查哪里?
先看 header 名称是否正确,Spring 常用 `X-CSRF-TOKEN`。再看 Token 是否来自同一会话,登录刷新、多标签旧页面提交都可能不匹配。
### 什么时候可以忽略 CSRF?
真正无状态 REST API 使用 Authorization Bearer,浏览器不会自动带这个头,CSRF 风险较低;但 Session Cookie API 不能直接忽略。
## 写段配置
```java
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
```标签
Spring Boot
Spring Boot 是一个开源的 Java 基础框架,旨在简化 Spring 应用的创建和开发过程。它由 Pivotal 团队(现为 VMware)开发,是 Spring 平台和第三方库的集成,提供了一个快速且广泛接受的方式来构建 Spring 应用。Spring Boot 使得设置和配置 Spring 应用变得简单,主要通过约定优于配置的原则,减少了项目的样板代码。

服务端5月30日 00:10
Spring Boot 的启动流程是怎样的?关键阶段有哪些?Spring Boot 启动本质上是 `SpringApplication.run()` 创建并刷新 Spring 容器的过程。主线可以记成:创建 SpringApplication、准备 Environment、创建 ApplicationContext、刷新容器、启动 WebServer、执行 Runner、发布 ready 事件。真正初始化 Bean、自动配置和启动内嵌 Tomcat 的核心都发生在 `refreshContext()` 里的 `ApplicationContext.refresh()`。
## 追问
### SpringApplication 实例化时做了什么?
它会推断应用类型:Servlet、Reactive 或普通应用;再加载 `ApplicationContextInitializer`、`ApplicationListener`,并推断 main 方法所在主类。这一步还没创建业务 Bean,只是在准备启动所需的元信息。
### Environment 是什么时候准备的?
在创建 ApplicationContext 之前。Spring Boot 会合并命令行参数、系统属性、环境变量、application.yml/properties 和 profile 配置,再发布 `ApplicationEnvironmentPreparedEvent`。
### refresh() 为什么最关键?
因为它会加载 BeanDefinition、执行 BeanFactoryPostProcessor、注册 BeanPostProcessor、实例化非懒加载单例 Bean。Servlet 应用的内嵌 Tomcat/Jetty 通常也在刷新过程的 `onRefresh()` 阶段创建并启动。
### Runner 在什么时候执行?
`ApplicationRunner` 和 `CommandLineRunner` 在容器刷新完成、`ApplicationStartedEvent` 发布后执行。它们适合做启动后的初始化任务,但不要放耗时阻塞逻辑,否则应用迟迟到不了 ready 状态。
### 启动失败会发生什么?
Spring Boot 会发布 `ApplicationFailedEvent`,销毁已创建的 Bean,并把异常继续抛出。排查启动问题时,重点看 Environment、自动配置条件和 Bean 创建异常。
## 写段代码
```java
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
```服务端5月29日 01:38
Spring Boot 中如何实现异步编程?Spring Boot 通过 `@EnableAsync` + `@Async` 实现声明式异步编程。在配置类上标注 `@EnableAsync` 开启支持,在方法上标注 `@Async` 即可在独立线程执行;默认使用 `SimpleAsyncTaskExecutor`(每次新建线程),生产环境应自定义 `ThreadPoolTaskExecutor` 并通过 `@Async("executorName")` 指定线程池。有返回值的方法返回 `CompletableFuture`,调用方可通过 `future.get()` 或 `CompletableFuture.allOf()` 组合多个异步结果。异常处理方面,返回 `CompletableFuture` 的方法异常会传播到 future,void 方法需实现 `AsyncUncaughtExceptionHandler`。注意同类内部调用 `@Async` 方法不生效(绕过代理)。
## 追问
- **ThreadPoolTaskExecutor 的核心参数如何设置?** IO 密集型核心线程数可设为 CPU 核数 * 2,队列容量适当放大;CPU 密集型核心线程数为 CPU 核数 + 1,队列不宜过大;拒绝策略推荐 `CallerRunsPolicy` 由提交线程执行降速。
- **@Async 方法为什么同类调用不生效?如何解决?** `@Async` 依赖 AOP 代理,`this.method()` 绕过代理直接调用原始方法;解决方案是注入自身代理 `@Lazy private XxxService self` 后通过 `self.method()` 调用。
- **CompletableFuture 的 thenCombine 和 thenCompose 有何区别?** `thenCombine` 将两个独立异步结果合并(BiFunction),`thenCompose` 用于异步结果串联(flatMap 语义,前一步结果决定后一步操作)。
- **@Async 和 @Transactional 一起使用有什么陷阱?** 事务上下文绑定 ThreadLocal,异步方法在新线程执行导致事务不传播;应在异步方法内部调用另一个 Bean 的事务方法,而非叠加注解。
- **如何将 RequestContext 传播到异步线程?** 使用 `TaskDecorator` 在任务提交时捕获主线程的 RequestAttributes,在异步线程执行前设置,执行后清除。
## 写段代码
```java
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(5);
ex.setMaxPoolSize(20);
ex.setQueueCapacity(100);
ex.setThreadNamePrefix("async-");
ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
ex.initialize();
return ex;
}
}
```服务端5月29日 01:38
Spring Boot 中如何实现多环境配置?Spring Boot 通过 Profile 机制实现多环境配置。核心做法是创建 `application-{profile}.yml` 文件(如 `application-dev.yml`、`application-prod.yml`),公共配置放 `application.yml`,环境特有配置放对应 Profile 文件,同名属性 Profile 文件覆盖默认值。激活方式包括配置文件中 `spring.profiles.active`、启动参数 `--spring.profiles.active=prod`、环境变量 `SPRING_PROFILES_ACTIVE`。代码层面用 `@Profile` 注解按环境条件注册 Bean,Spring Boot 2.4+ 还支持 `spring.profiles.group` 将多个 Profile 组合激活。配置优先级为:命令行参数 > 环境变量 > Profile 文件 > 默认文件。
## 追问
- **application.yml 和 application-dev.yml 同名属性如何取舍?** Profile 文件优先,覆盖默认文件中的同名配置;若同时激活多个 Profile,后激活的覆盖先激活的。
- **@Profile 注解标注在类和方法上有什么区别?** 标注在 `@Configuration` 类上整个配置类按条件加载,标注在 `@Bean` 方法上只控制单个 Bean 的注册。
- **单文件多文档块(---分隔)和多文件方式如何选择?** 多文档块适合配置较少的项目,管理简单;多文件适合配置量大、环境差异明显的项目,避免单文件过长且减少合并冲突。
- **生产环境敏感信息如何保护?** 使用环境变量 `${DB_PASSWORD}` 引用,或通过 Jasypt 对配置值加密存储为 `ENC(密文)`,运行时由加密器解密。
- **Profile 分组解决了什么问题?** 一个环境可能需要同时激活多个维度(如 prod + prod-db + prod-mq),分组将它们聚合为一个名称,简化启动参数。
## 写段代码
```java
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDs() {
return new HikariDataSource(new HikariConfig() {{ setJdbcUrl("jdbc:mysql://localhost/dev"); }});
}
@Bean
@Profile("prod")
public DataSource prodDs() {
HikariConfig c = new HikariConfig();
c.setJdbcUrl("jdbc:mysql://prod-db/prod");
c.setUsername(System.getenv("DB_USER"));
return new HikariDataSource(c);
}
}
```服务端5月29日 01:37
Spring Boot 中如何实现全局异常处理?Spring Boot 通过 `@RestControllerAdvice` + `@ExceptionHandler` 实现全局异常处理。前者是 AOP 组件,拦截所有 Controller 抛出的异常;后者按异常类型匹配处理方法,返回统一的响应体。典型做法是定义自定义 `BusinessException` 携带错误码和消息,在异常处理类中分别处理业务异常、参数校验异常(`MethodArgumentNotValidException`)和兜底异常(`Exception`),对外返回结构化的错误响应,对内记录日志并隐藏堆栈细节。RFC 7807 的 `application/problem+json` 是业界推荐的错误响应格式。
## 追问
- **@RestControllerAdvice 和 @ControllerAdvice 有何区别?** 前者是后者的 `@ResponseBody` 组合注解,方法返回值直接序列化为 JSON;后者适用于返回 ModelAndView 的传统 MVC 场景。
- **多个 @ExceptionHandler 匹配同一个异常时如何选择?** Spring 选择异常类型最具体的那个方法,即继承链中最靠近抛出异常类型的处理器优先。
- **如何处理 404 异常?** 默认 Spring Boot 不抛出 404 异常而是返回白标签页,需设置 `throw-exception-if-no-handler-found=true` 并关闭 `add-mappings=false`,再用 `@ExceptionHandler(NoHandlerFoundException.class)` 捕获。
- **BusinessException 为什么用 RuntimeException 而非 Checked Exception?** 避免 `try-catch` 侵入业务代码,全局处理器统一兜底,保持代码整洁。
- **参数校验异常 MethodArgumentNotValidException 和 ConstraintViolationException 分别在何时触发?** 前者发生在 `@RequestBody` + `@Valid` 校验请求体时,后者发生在 `@RequestParam` + `@Validated` 校验单参数时。
## 写段代码
```java
@RestControllerAdvice
public class GlobalExHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> onBiz(BusinessException e) {
return ResponseEntity.badRequest().body(Result.error(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> onEx(Exception e) {
return ResponseEntity.status(500).body(Result.error(500, "系统繁忙"));
}
}
```服务端5月29日 01:37
Spring Boot 如何整合 MyBatis 进行数据库操作?引入 mybatis-spring-boot-starter 后,通过 @MapperScan 扫描接口、XML 或注解编写 SQL 即可完成整合。核心三要素:Mapper 接口(定义方法签名)、映射文件(编写 SQL 与 ResultMap)、自动配置(驼峰映射、数据源)。简单 CRUD 用注解(@Select/@Insert),复杂动态 SQL 用 XML(where/set/foreach)。PageHelper.startPage 即可实现分页,@Transactional 管理事务。
## 追问
**@Mapper 和 @MapperScan 有什么区别?**
@Mapper 逐个标注接口,适合 Mapper 少的场景;@MapperScan 在启动类统一指定包路径,批量注册,推荐使用。两者二选一,不可重复注册。
**#{} 和 ${} 的区别是什么?**
#{} 使用预编译参数(PreparedStatement 占位符),防止 SQL 注入;${} 直接字符串拼接,有注入风险,仅用于表名、列名等不可预编译的位置。
**ResultMap 和 ResultType 怎么选?**
字段名与属性一致时用 resultType 自动映射(配合 map-underscore-to-camel-case);不一致或有关联查询(association/collection)时必须用 resultMap 手动映射。
**动态 SQL 的 where 标签解决了什么问题?**
自动去除首个多余 AND/OR,并在条件全空时不生成 WHERE 子句,避免语法错误。set 标签同理,自动去除末尾多余逗号。
**PageHelper 分页的原理和坑是什么?**
PageHelper 基于 MyBatis Interceptor 拦截 SQL,自动拼接 LIMIT。坑:startPage 必须紧挨查询语句,中间不能有其他查询,否则分页错乱;只对紧跟的第一条查询生效。
## 写段代码
```java
@SpringBootApplication
@MapperScan("com.example.mapper")
public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
// XML 动态查询
<select id="selectByCondition" resultMap="BaseMap">
SELECT * FROM user
<where>
<if test="name != null">AND name LIKE CONCAT('%',#{name},'%')</if>
</where>
</select>
```服务端5月29日 01:37
Spring Boot 中如何实现安全认证?Spring Boot 通过 `spring-boot-starter-security` 集成 Spring Security,核心流程为:请求进入 `SecurityFilterChain`,依次经过认证过滤器(如 `UsernamePasswordAuthenticationFilter` 或自定义 JWT Filter),由 `AuthenticationManager` 委托 `UserDetailsService` 加载用户并校验凭证,认证成功后将 `Authentication` 存入 `SecurityContextHolder`;授权阶段通过 `@PreAuthorize` 或 URL 规则判断权限。JWT 场景下需自定义 Filter 从 Header 提取 Token 并验证,同时将 `SessionCreationPolicy` 设为 `STATELESS`。
## 追问
- **SecurityFilterChain 的执行顺序如何控制?** 通过 `http.addFilterBefore()/after()` 指定过滤器位置,Spring Security 内置过滤器有固定顺序(如 `UsernamePasswordAuthenticationFilter` 位于 `BasicAuthenticationFilter` 之前)。
- **UserDetailsService 和 UserDetails 的职责分别是什么?** `UserDetailsService` 负责从数据源加载用户信息,`UserDetails` 是用户信息的载体接口,包含密码、权限和账户状态。
- **JWT 无状态方案下如何实现 Token 注销?** 可维护黑名单(Redis 存储已注销 Token 直至过期)或采用短期 Token + Refresh Token 轮换机制。
- **@PreAuthorize 和 URL 授权规则各自适合什么场景?** URL 规则适合粗粒度的路径级控制,`@PreAuthorize` 适合方法级细粒度控制,支持 SpEL 表达式如 `@userSecurity.canAccess(authentication, #id)`。
- **为什么密码必须用 BCrypt 而非 MD5?** BCrypt 内置盐值且计算成本可调,抗彩虹表和暴力破解;MD5 是快速哈希,易被暴力穷举。
## 写段代码
```java
@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.csrf(c -> c.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
```服务端5月29日 01:37
Spring Boot 微服务如何实现服务注册与发现?服务注册与发现是微服务架构的基础设施:服务启动时将自身地址注册到注册中心,消费方从注册中心拉取可用实例列表并调用。主流方案有 Eureka(AP、已停维)、Nacos(AP/CP 可切换、国产生态)、Consul(CP、云原生友好)。核心流程三步:注册(实例启动时上报 IP/端口)、心跳(定时续约保活)、拉取(客户端缓存实例列表并定时刷新)。Nacos 是当前新项目首选,临时实例走客户端心跳,持久实例走服务端探活,还内置配置中心。
## 追问
**Eureka 自我保护机制触发后,实例下线能否被感知?**
不能立即感知。自我保护模式下 Eureka 不再剔除过期实例,客户端可能拿到已下线的地址,需配合 Ribbon 重试或熔断兜底。
**Nacos 临时实例和持久实例的区别是什么?**
临时实例(ephemeral=true)由客户端发心跳,不续约则自动剔除,适合微服务;持久实例由服务端主动探活,不会自动删除,适合数据库等基础设施。
**客户端发现与服务端发现有什么区别?**
客户端发现:消费方从注册中心拉取列表,本地负载均衡(Eureka/Nacos);服务端发现:请求先到网关/代理,由代理侧转发(Consul + Nginx)。前者少一跳,后者解耦消费方。
**注册中心选型时 CAP 如何取舍?**
Eureka 牺牲一致性保可用(AP),适合网络分区时仍需可用的场景;Consul/ZK 保一致性(CP),适合不能容忍脏数据的场景;Nacos 支持按实例切换 AP/CP。
**优雅下线如何避免请求打到已注销实例?**
先从注册中心注销,再等待正在处理的请求完成(如 5s),最后关闭服务。Spring Boot 可监听 ContextClosedEvent 触发注销,配合 sleep 等待在途请求。
## 写段代码
```java
@SpringBootApplication
@EnableDiscoveryClient
public class OrderApp { public static void main(String[] args) { SpringApplication.run(OrderApp.class, args); } }
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}") User getById(@PathVariable Long id);
}
```服务端5月29日 01:37
Spring Boot Starter 的作用和原理是什么?Starter 是 Spring Boot 提供的依赖聚合与自动配置的组合机制。它将某项功能所需的所有依赖打包成一个 POM 依赖描述符,同时通过 `@EnableAutoConfiguration` 扫描 `META-INF/spring.factories`(或 2.7+ 的 `AutoConfiguration.imports`)中注册的自动配置类,配合 `@ConditionalOnClass`、`@ConditionalOnMissingBean` 等条件注解,实现按需装配 Bean。因此引入一个 starter 即可获得完整的依赖集合和零配置的功能启用。
## 追问
- **@ConditionalOnClass 和 @ConditionalOnMissingBean 分别解决什么问题?** 前者在 classpath 存在指定类时才装配,避免缺少依赖导致启动失败;后者在容器中无同名 Bean 时才创建,允许用户覆盖默认配置。
- **官方 starter 和第三方 starter 的命名规范有何不同?** 官方为 `spring-boot-starter-*`,第三方为 `*-spring-boot-starter`,反过来便于区分来源。
- **Spring Boot 2.7 后为什么用 AutoConfiguration.imports 替代 spring.factories?** spring.factories 职责过重,AutoConfiguration.imports 专用于自动配置注册,加载更高效且语义更清晰。
- **自定义 starter 时 @ConfigurationProperties 有什么作用?** 将 `application.yml` 中以指定前缀开头的属性映射到 Java 对象,实现外部化配置的类型安全绑定。
- **Starter 依赖传递和 BOM 版本管理是什么关系?** Starter 聚合依赖但不管理版本,版本由 `spring-boot-dependencies` BOM 统一控制,确保兼容性。
## 写段代码
```java
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(MyProps.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyProps p) {
return new MyService(p.getHost(), p.getPort());
}
}
```服务端5月28日 01:31
什么是 Spring Boot 的自动配置原理?## Spring Boot 的自动配置原理是什么?
Spring Boot 的自动配置,简单说就是:根据你引入的依赖和已有的配置,自动帮你把该配的 Bean 都配好。你不用手写一堆 XML,也不用挨个注册 Bean,Spring Boot 帮你搞定。
这个能力背后靠的是三个核心机制:**SPI 发现配置类 → 条件注解过滤 → 属性绑定定制**。下面逐一拆解。
## 入口:@SpringBootApplication 做了什么?
启动类上的 `@SpringBootApplication` 是个复合注解,拆开来看:
```java
@SpringBootConfiguration // 标记当前类是配置类
@EnableAutoConfiguration // 开启自动配置(核心)
@ComponentScan // 扫描当前包及子包下的组件
public @interface SpringBootApplication {}
```
其中 `@EnableAutoConfiguration` 是关键,它又引入了 `AutoConfigurationImportSelector`,这就是整个自动配置的调度中心。
## 自动配置的三步核心流程
### 第一步:发现候选配置类
Spring Boot 启动时,`AutoConfigurationImportSelector` 会去类路径下查找自动配置类的注册信息。这里有个重要的版本差异:
**Spring Boot 2.7 之前**,读取的是 `META-INF/spring.factories`:
```properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
```
**Spring Boot 2.7+ / 3.x**,新增了 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件:
```
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
```
每行一个全限定类名,格式更简洁。Spring Boot 3.x 中 `spring.factories` 已被废弃,统一使用 imports 文件。这个变化面试中经常被追问,务必记住。
### 第二步:条件注解过滤
扫描到的配置类不是全部生效,Spring Boot 用 `@Conditional` 系列注解做条件判断:
| 条件注解 | 生效条件 | 典型场景 |
|---------|---------|----------|
| `@ConditionalOnClass` | 类路径存在指定类 | 引了 mysql 驱动才配 DataSource |
| `@ConditionalOnMissingBean` | 容器中不存在该 Bean | 用户没自定义才给默认实现 |
| `@ConditionalOnProperty` | 配置项满足指定值 | 开关控制是否启用 |
| `@ConditionalOnWebApplication` | 是 Web 应用 | 只在 Web 环境配 MVC |
| `@ConditionalOnMissingClass` | 类路径不存在指定类 | 互斥依赖场景 |
其中 `@ConditionalOnMissingBean` 最常被问——它保证了"用户自定义优先,框架兜底默认"的设计原则。你手动声明了一个 Bean,自动配置就不会再创建同类型的。
### 第三步:属性绑定定制默认值
自动配置类通过 `@ConfigurationProperties` 把 `application.yml` 中的属性绑定到 Java 对象:
```java
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DataSourceProperties properties) {
return DataSourceBuilder.create()
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build();
}
}
```
`DataSourceProperties` 通过 `@ConfigurationProperties(prefix = "spring.datasource")` 绑定配置,这样你在 yml 里写的 `spring.datasource.url` 就能自动注入。框架提供合理的默认值,你想改就改,不想改直接用。
## 执行时机:自动配置在什么时候生效?
```
SpringApplication.run()
└── refreshContext()
└── invokeBeanFactoryPostProcessors()
└── AutoConfigurationImportSelector.selectImports()
├── 读取 spring.factories / imports 文件
├── 条件注解过滤
└── 返回满足条件的配置类全限定名数组
```
自动配置发生在 Spring 容器刷新的早期阶段,在普通 Bean 实例化之前,这样自动配置产生的 Bean 就能被后续流程正常使用。
## 如何自定义一个 Starter?
理解了原理,写一个自定义 Starter 就是照猫画虎:
**1)配置类 + 条件注解**
```java
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyProperties properties) {
return new MyService(properties.getName(), properties.isEnabled());
}
}
```
**2)属性类**
```java
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
private String name = "default";
private boolean enabled = true;
// getter/setter
}
```
**3)注册配置类**
Spring Boot 3.x 在 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 中添加:
```
com.example.MyAutoConfiguration
```
Spring Boot 2.x 则在 `META-INF/spring.factories` 中注册。
## 如何排除不需要的自动配置?
注解方式:
```java
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {}
```
配置文件方式:
```yaml
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
```
还可以用 `@ConditionalOnProperty` 做开关,通过配置项控制是否启用某个自动配置,比硬编码 exclude 更灵活。
## 一句话总结
Spring Boot 自动配置的本质:**SPI 机制发现候选类 → @Conditional 条件过滤 → @ConfigurationProperties 属性绑定**,三条线串起来,实现了"约定优于配置"。你引了依赖它就帮你配,你想改就改,不改也有合理默认。
---
**追问方向**:spring.factories 和 imports 文件的区别是什么?@ConditionalOnMissingBean 如何保证用户自定义优先?自动配置的加载顺序怎么控制(@AutoConfigureBefore/After)?AutoConfigurationImportSelector 的过滤去重逻辑是怎样的?服务端5月28日 01:31
Spring Boot 中如何实现缓存?## 核心回答
Spring Boot 通过 **Spring Cache Abstraction** 提供统一的缓存抽象,开发者只需添加 `@EnableCaching` 注解和对应缓存实现依赖,即可用 `@Cacheable`、`@CachePut`、`@CacheEvict` 等注解实现声明式缓存。常用实现方案有三种:
- **ConcurrentMapCache**:基于 `ConcurrentHashMap`,零依赖,适合单机开发测试
- **Caffeine**:高性能本地缓存,支持过期策略和容量限制,适合单机生产环境
- **Redis**:分布式缓存,支持持久化和集群,适合多实例部署
选择依据:单机选 Caffeine,分布式选 Redis,开发调试用 ConcurrentMapCache。
## 缓存注解用法
### @Cacheable —— 查询时缓存
方法执行前先查缓存,命中则直接返回,未命中才执行方法并缓存结果:
```java
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
```
- `key`:支持 SpEL 表达式,如 `#id`、`#user.name`
- `condition`:满足条件才缓存(方法执行前判断)
- `unless`:满足条件则不缓存(方法执行后判断)
### @CachePut —— 更新缓存
方法一定执行,执行后用返回值更新缓存:
```java
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
```
### @CacheEvict —— 删除缓存
```java
// 删除指定 key
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) { ... }
// 清空整个缓存区域
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() { }
```
`beforeInvocation = true` 可在方法执行前删缓存,防止方法异常导致缓存未清除。
### @Caching —— 组合操作
```java
@Caching(
put = { @CachePut(value = "users", key = "#user.id") },
evict = { @CacheEvict(value = "userList", allEntries = true) }
)
public User saveUser(User user) {
return userRepository.save(user);
}
```
## Caffeine 本地缓存配置
```xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
```
```java
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return manager;
}
}
```
YAML 简写方式(Spring Boot 2.7+):
```yaml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
```
不同缓存区域可用 `registerCustomCache` 分别配置过期时间和容量。
## Redis 分布式缓存配置
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
```yaml
spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 600000
cache-null-values: false
key-prefix: "myapp:"
```
```java
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> configs = new HashMap<>();
configs.put("users", defaults.entryTtl(Duration.ofMinutes(30)));
configs.put("products", defaults.entryTtl(Duration.ofMinutes(5)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(configs)
.transactionAware()
.build();
}
}
```
## 缓存穿透、击穿、雪崩
| 问题 | 原因 | 解法 |
|------|------|------|
| 穿透 | 查询不存在的数据,缓存和DB都没有 | 缓存空值(短TTL)或布隆过滤器 |
| 击穿 | 热点key过期,大量请求同时打到DB | 互斥锁或逻辑过期 |
| 雪崩 | 大量key同时过期 | 随机过期时间 + 多级缓存 |
互斥锁示例(基于 Redisson):
```java
public User getUserWithLock(Long id) {
String key = "users::" + id;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) return JSON.parseObject(cached, User.class);
RLock lock = redissonClient.getLock("lock:users:" + id);
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
try {
cached = redisTemplate.opsForValue().get(key);
if (cached != null) return JSON.parseObject(cached, User.class);
User user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
}
return user;
} finally { lock.unlock(); }
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
```
## 面试追问
**@Cacheable 和 @CachePut 有什么区别?**
@Cacheable 会先查缓存,命中则跳过方法执行;@CachePut 不查缓存,方法一定执行后用返回值更新缓存。更新场景必须用 @CachePut,否则缓存不会刷新。
**缓存和事务一起用要注意什么?**
Spring 缓存注解基于 AOP 代理,在事务边界之外执行。如果事务回滚,缓存可能已经写入脏数据。建议写操作用 @CachePut 且在事务提交后再更新,或用 `TransactionSynchronizationManager.registerSynchronization` 在事务提交后操作缓存。
**多级缓存怎么实现?**
L1 用 Caffeine(本地,毫秒级),L2 用 Redis(分布式,5-10ms)。读取时先查 L1,未命中查 L2,再未命中查 DB 并回写 L1 和 L2。更新时先更新 DB,再删 L1 和 L2 缓存。可用 `CompositeCacheManager` 组合多个 CacheManager。
**Spring Cache 的 key 生成规则是什么?**
默认用 `SimpleKeyGenerator`:无参用 `SimpleKey.EMPTY`,一个参数直接用该参数,多个参数用 `SimpleKey(params)`。自定义可实现 `KeyGenerator` 接口,通过 `keyGenerator` 属性引用。
## 方案选型总结
| 缓存类型 | 适用场景 | 优点 | 缺点 |
|---------|---------|------|------|
| ConcurrentMapCache | 单机开发测试 | 零配置、无额外依赖 | 不支持过期和分布式 |
| Caffeine | 单机生产环境 | 高性能、支持过期淘汰 | 不支持多实例共享 |
| Redis | 分布式生产环境 | 支持集群和持久化 | 网络开销、需要运维 |