1Spring Boot 是什么

约定优于配置,让 Java 企业开发像"自动挡汽车"

Spring Boot 是基于 Spring Framework 的快速开发框架,它通过自动配置(Auto-Configuration)、起步依赖(Starter)和内嵌服务器(Tomcat),让开发者只需极少的配置就能搭建生产级应用。核心特点:IoC 容器管理 Bean、AOP 面向切面、约定优于配置、内嵌 Tomcat 一键启动。

🔑 一句话理解 IoC

你不用 new 对象了。类上加 @Service/@Component,容器帮你创建、注入、管理。用的时候 @Autowired 自动拿。结果:松耦合、可替换、好测试

架构图:分层 + IoC 容器

Spring Boot Architecture
HTTP 请求
浏览器 / App / curl
内嵌 Tomcat
接收 HTTP
DispatcherServlet
分发请求
IoC 容器 (ApplicationContext)
管理所有 Bean 的生命周期
@Controller @Service @Repository @Component
Auto-Configuration
自动装配 Bean
DataSource / Redis / MQ
application.yml
外部化配置
多环境 Profile

Spring Boot vs Spring Framework

维度Spring FrameworkSpring Boot
配置方式XML / Java Config,手动配置多自动配置 + application.yml
服务器需要打包成 WAR 部署到 Tomcat内嵌 Tomcat,java -jar 启动
依赖管理手动指定每个依赖的版本号Starter 自动管理版本,parent POM
启动方式配置 ContextLoaderListenerSpringApplication.run() 一行启动
生产监控需额外集成Actuator 内置健康检查/指标/审计
适用场景需要精细控制的老项目微服务、新项目(业界标配)
🔒 面试要点

Spring Boot 不是替代 Spring Framework 的新框架,而是在 Spring 之上做了一层封装。核心是 @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan。理解 Boot 的关键在于理解自动配置原理:spring.factories 中声明了 xxxAutoConfiguration,根据 @Conditional 决定是否加载。

自动配置原理

// Spring Boot 启动入口
@SpringBootApplication  // = 3个注解的组合
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

// 自动配置核心:spring.factories 中声明的 AutoConfiguration
// 例如 DataSourceAutoConfiguration:
@Configuration
@ConditionalOnClass(DataSource.class)           // classpath 有 DataSource 才生效
@ConditionalOnProperty(name = "spring.datasource.url")  // 配置了 URL 才生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean  // 你没手动定义才自动创建
    public DataSource dataSource(DataSourceProperties props) {
        return props.initializeDataSourceBuilder().build();
    }
}

2IoC 容器与 Bean 生命周期 核心中的核心!

理解 Bean 生命周期 = 理解 Spring 的灵魂

🔑 Bean 生命周期 5 步

实例化(调构造方法)→ 属性注入(@Autowired)→ 初始化(@PostConstruct → InitializingBean → BeanPostProcessor)→ 使用销毁(@PreDestroy)
其中 BeanPostProcessor 是 AOP 实现的关键钩子。三级缓存(singletonObjects / earlySingletonObjects / singletonFactories)用来解决循环依赖。

Bean 生命周期 5 阶段详解

Bean Lifecycle 5 Phases
Bean 生命周期
Bean Lifecycle
↻ 容器管理
① 实例化
createBeanInstance
构造方法 new 出对象
② 属性填充
populateBean
@Autowired 注入
③ 初始化
initializeBean
@PostConstruct
④ 正常使用
Bean Ready
🔥 服务请求
⑤ 销毁
destroyBean
@PreDestroy
Aware 回调
BeanNameAware 等
BeanPostProcessor
postProcessBeforeInitialization
AOP 代理
postProcessAfterInitialization

↑ 初始化阶段内部还有 3 个子步骤:Aware 注入 → 前置处理 → AOP 代理生成

生命周期代码演示

@Component
public class MyBean implements InitializingBean, DisposableBean {

    @Autowired
    private Dependency dep;  // ② 属性填充阶段注入

    public MyBean() {
        System.out.println("① 构造方法: 实例化");
    }

    @PostConstruct
    public void init() {
        System.out.println("③ @PostConstruct: 初始化");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("③ InitializingBean: 属性设置完成后");
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("⑤ @PreDestroy: 销毁前清理");
    }

    @Override
    public void destroy() {
        System.out.println("⑤ DisposableBean: 销毁");
    }
}

Bean 作用域(Scope)

作用域说明使用场景
singleton(默认)容器中只有一个实例无状态 Service、DAO
prototype每次获取都创建新实例有状态对象、需要隔离的数据
request每个 HTTP 请求一个实例Web 请求上下文信息
session每个 HTTP Session 一个实例用户登录信息
application每个 ServletContext 一个全局应用级缓存
⚠️ singleton 注入 prototype 的陷阱

singleton Bean 中 @Autowired 一个 prototype Bean,注入只发生一次,之后永远是同一个对象。解决方案:@Lookup 注解或 ObjectFactory

// ❌ 错误: prototype 失效
@Service
public class OrderService {
    @Autowired
    private PriceCalculator calculator; // 只注入一次, 后续永远是同一个!

    public BigDecimal calculate(Order order) {
        calculator.reset(); // 多线程下有线程安全问题
        return calculator.calc(order);
    }
}

// ✅ 正确: 使用 @Lookup
@Service
public abstract class OrderService {
    @Lookup  // 每次调用都从容器获取新实例
    protected abstract PriceCalculator getCalculator();

    public BigDecimal calculate(Order order) {
        PriceCalculator calc = getCalculator(); // 每次新的
        return calc.calc(order);
    }
}

循环依赖与三级缓存

📚 三级缓存原理

Spring 用三级缓存解决 singleton Bean 的循环依赖(构造器循环依赖无法解决):

// DefaultSingletonBeanRegistry 中的三级缓存
Map<String, Object> singletonObjects = new ConcurrentHashMap<>();       // 一级: 完整 Bean
Map<String, Object> earlySingletonObjects = new HashMap<>();           // 二级: 提前曝光的半成品
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();  // 三级: ObjectFactory

// 循环依赖解决流程 (A 依赖 B, B 依赖 A):
// 1. 创建 A → 实例化 A → 放入三级缓存
// 2. 填充 A 的属性, 发现需要 B → 创建 B
// 3. 创建 B → 实例化 B → 填充 B 的属性, 发现需要 A
// 4. 从三级缓存拿到 A 的 ObjectFactory → 提前暴露 A → 放入二级缓存
// 5. B 拿到 A 的引用, B 完成 → 放入一级缓存
// 6. A 继续完成初始化 → 放入一级缓存

// ⚠️ 为什么需要三级缓存而不是两级?
// 如果 A 被 AOP 代理, 三级缓存的 ObjectFactory 可以提前生成代理对象
// 保证 B 注入的是 A 的代理, 而不是原始对象

生产案例:Bean 注入失败

🚨 线上事故:服务启动直接失败 (NoSuchBeanDefinitionException)

症状:新同事提交代码后,应用启动直接报 NoSuchBeanDefinitionException: No qualifying bean of type 'UserService',CI/CD 流水线红灯。

❌ 错误原因

// UserService 没有加 @Service 注解!
public class UserService {  // 缺少 @Service
    public User getUser(Long id) { ... }
}

// 或者: @ComponentScan 没扫到这个包
@SpringBootApplication
@ComponentScan("com.examplewrong")  // 包名写错了
public class MyApp { ... }

✅ 正确做法

@Service  // 必须加注解, 或者在 @Configuration 中 @Bean
public class UserService { ... }

// 或者使用 @Bean 方式注册
@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

// 💡 排查技巧:
// 1. 检查类是否在 @ComponentScan 扫描范围内
// 2. 检查是否加了 @Service/@Component/@Repository/@Controller
// 3. 启动时加 --debug 查看 ConditionEvaluationReport
// 4. 使用 Actuator /beans 端点查看所有已注册 Bean

💡 经验法则:90% 的 Bean 注入失败都是因为忘了加注解或包扫描路径不对。

3AOP 与动态代理

Spring AOP 是事务、日志、限流的底层基石

🔑 AOP = 不改业务代码,统一加一层逻辑

5 种通知:@Before(方法前)/ @After(后,无论成败)/ @AfterReturning(成功后)/ @AfterThrowing(异常后)/ @Around(环绕,最强,能控制执行、改参数和返回值)。
事务(@Transactional)、日志、权限校验都是 AOP 实现。动态代理:JDK Proxy(基于接口)或 CGLIB(基于继承生成子类)。

JDK 动态代理 vs CGLIB 代理

Spring AOP Proxy Types
JDK 动态代理
Proxy$0 (生成的代理类)
implements UserService
InvocationHandler.invoke()
事务/日志/权限
Target: UserServiceImpl
真实业务逻辑

⚠️ 目标类必须实现接口

// JDK Proxy 核心机制
Proxy.newProxyInstance(
    classLoader,
    new Class[]{UserService.class}, // 必须有接口
    (proxy, method, args) -> {
        // @Before
        Object result = method.invoke(target, args);
        // @After
        return result;
    }
);
CGLIB 代理
UserService$$EnhancerByCGLIB
extends UserServiceImpl
MethodInterceptor.intercept()
事务/日志/权限
Target: UserServiceImpl
真实业务逻辑

✅ 不需要接口,生成子类

// CGLIB 核心机制
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    // @Before
    Object result = proxy.invokeSuper(obj, args);
    // @After
    return result;
});
enhancer.create();
🔒 Spring AOP 代理选择规则

① 目标类实现了接口 → 默认使用 JDK 动态代理
② 目标类没实现接口 → 使用 CGLIB
spring.aop.proxy-target-class=true(Spring Boot 2.x 默认 true)→ 强制使用 CGLIB
④ final 类/方法无法被 CGLIB 代理(不能继承)

@Transactional 原理与失效场景

🔎 @Transactional 本质就是 AOP
// @Transactional 的本质 = 环绕通知
@Around("execution(* com.example.service.*.*(..))")
public Object manageTransaction(ProceedingJoinPoint pjp) throws Throwable {
    TransactionStatus tx = transactionManager.begin();  // @Before: 开启事务
    try {
        Object result = pjp.proceed();  // 执行业务方法
        transactionManager.commit(tx);  // @AfterReturning: 提交
        return result;
    } catch (Exception e) {
        // 默认只回滚 RuntimeException 和 Error
        transactionManager.rollback(tx); // @AfterThrowing: 回滚
        throw e;
    }
}

// 所以事务的底层代理, 执行的是:
// 1. 开启数据库连接, 设置 autoCommit=false
// 2. 执行你的方法
// 3. 方法正常结束 → commit; 抛 RuntimeException → rollback

@Transactional 失效的 5 大场景

场景原因解决方案
1. 类内部方法调用this.method() 不经过代理注入自己/AopContext.currentProxy()
2. 方法不是 publicSpring AOP 只代理 public 方法改为 public
3. 异常被 catch 吞掉代理层没看到异常就不回滚catch 后 re-throw
4. 异常类型不匹配默认只回滚 RuntimeExceptionrollbackFor = Exception.class
5. 数据库不支持事务MySQL MyISAM 引擎用 InnoDB
// ⚠️ 场景 1: 内部调用导致事务失效 (最经典的坑!)
@Service
public class OrderService {

    public void batchProcess(List<Order> orders) {
        for (Order order : orders) {
            // this.process() 是直接调用, 不经过代理!
            this.process(order);  // ❌ 事务失效!
        }
    }

    @Transactional
    public void process(Order order) {
        orderDao.update(order);
        logDao.insert(buildLog(order));
    }
}

// ✅ 修复方案 1: 注入自己
@Service
public class OrderService {
    @Autowired
    @Lazy
    private OrderService self;

    public void batchProcess(List<Order> orders) {
        for (Order order : orders) {
            self.process(order);  // ✅ 通过代理调用!
        }
    }
}

// ✅ 修复方案 2: 从 AopContext 获取代理
((OrderService) AopContext.currentProxy()).process(order);

// ⚠️ 场景 4: 异常类型不匹配
@Transactional  // 默认只回滚 RuntimeException
public void pay(Long orderId) {
    try {
        // 抛的是 checked Exception, 默认不回滚!
        throw new Exception("支付通道异常");
    } catch (Exception e) {
        throw e;
    }
}

// ✅ 修复: 指定 rollbackFor
@Transactional(rollbackFor = Exception.class)  // 所有异常都回滚
public void pay(Long orderId) { ... }

生产案例:事务失效导致脏数据

🚨 线上事故:支付成功但订单状态没更新

症状:用户支付成功(第三方回调确认),但订单状态仍为"待支付"。运营每天收到几十单客诉。

排查过程

  1. 查看代码发现 handleCallback() 方法内部调用 this.updateOrder()
  2. updateOrder()@Transactional,但 handleCallback() 没加
  3. 内部 this. 调用绕过了代理,事务注解形同虚设
  4. updateOrder 中第一步 UPDATE 成功,第二步 INSERT 日志失败抛异常——但第一步的数据已经持久化了

解决:把 @Transactional(rollbackFor = Exception.class) 加到 handleCallback() 方法上,或使用 self.updateOrder() 经过代理。

💡 经验法则:任何涉及多表写入的方法,事务注解必须加在真正被外部调用的方法上。

4Spring MVC 请求全链路

一个 HTTP 请求在 Spring Boot 中经历了什么?

🔑 Filter vs Interceptor

Filter是 Servlet 规范,在 DispatcherServlet 前后执行,不能直接用 Spring Bean。适合 CORS/编码/安全头。Interceptor是 Spring MVC 的,在 Controller 前后执行,可以 @Autowired。适合登录校验/权限/日志。
执行顺序:Filter → Interceptor.preHandle → Controller → Interceptor.postHandle → Filter。

DispatcherServlet 全链路

Spring MVC Request Pipeline
Client
HTTP Request
Tomcat
接收 HTTP
Filter Chain
过滤
CORS/编码/鉴权
DispatcherServlet
前端控制器
分发请求
Interceptor.preHandle
前置拦截
登录校验/日志
Controller
@RequestMapping
处理请求
Interceptor.postHandle
后置拦截
View/JSON
返回结果

Filter vs Interceptor 详细对比

维度FilterInterceptor
规范Servlet 规范(javax.servlet)Spring MVC(HandlerInterceptor)
执行时机DispatcherServlet 前后Controller 方法前后
能否访问 Spring Bean需要手动注入天然支持 @Autowired
能否中断请求✓ response.sendError()✓ preHandle return false
典型用途CORS、编码、安全头登录校验、权限检查、日志
执行顺序先进后出(洋葱)preHandle 顺序,postHandle 逆序

Filter 和 Interceptor 代码示例

// Filter: CORS 跨域配置
@Component
public class CorsFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "*");
        chain.doFilter(req, res);  // 放行
    }
}

// 更推荐: 用 WebMvcConfigurer 统一配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://yourdomain.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login", "/api/register");
    }
}

// Interceptor: 登录校验
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        String token = req.getHeader("Authorization");
        if (token == null || !tokenService.isValid(token)) {
            res.setStatus(401);
            return false;  // 中断请求
        }
        return true;  // 放行
    }

    @Override
    public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView mv) {
        // Controller 执行后, 视图渲染前
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
        // 视图渲染后 (总是执行, 用于清理资源)
    }
}

5线程池与异步编程

Java 并发是 Spring Boot 高性能的底层保障

🔑 线程池执行流程

任务来了 → 核心线程不满就新建执行 → 满了进队列排队 → 队列满了开临时线程(到 maxPoolSize)→ 连临时都满了执行拒绝策略
阿里规约:禁用 Executors(无界队列/无限线程会 OOM),必须手动 new ThreadPoolExecutor + 有界队列。

ThreadPoolExecutor 7 大参数

ThreadPoolExecutor Architecture
Task 1
Task 2
Task 3
Task N
▼ ▼ ▼ ▼
ThreadPoolExecutor
corePoolSize=10 } maxPoolSize=20 } keepAlive=60s
Thread 1
核心线程
Thread 2
核心线程
...
10个核心
核心全忙 →
BlockingQueue
LinkedBlockingQueue (容量=500)
任务排队等待空闲线程
队列满 →
Thread 11
临时线程
Thread 12
临时线程
...
扩到20个
线程+队列都满 →
RejectedExecutionHandler
AbortPolicy: 抛 RejectedExecutionException
CallerRunsPolicy: 调用者线程自己跑
DiscardPolicy: 丢弃 | DiscardOldestPolicy: 丢最老的

线程池配置示例

@Configuration
@EnableAsync
public class ThreadPoolConfig {

    // CPU 密集型: corePoolSize = CPU 核心数 + 1
    // IO 密集型: corePoolSize = CPU 核心数 * 2 (或用公式: N * (1 + W/C))
    // 实际项目中必须压测调优!

    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);       // 核心线程
        executor.setMaxPoolSize(20);        // 最大线程
        executor.setQueueCapacity(500);     // 队列容量
        executor.setKeepAliveSeconds(60);   // 临时线程空闲存活时间
        executor.setThreadNamePrefix("biz-task-");
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略: 调用者自己跑
        );
        executor.setWaitForTasksToCompleteOnShutdown(true);  // 优雅停机
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}
🚨 生产案例:Executors.newFixedThreadPool 导致 OOM

症状:线上服务突然 OOM,jmap 发现堆积了大量 Runnable 等待对象。

原因Executors.newFixedThreadPool() 底层用的是 无界队列 LinkedBlockingQueue(Integer.MAX_VALUE),任务无限堆积直到 OOM。

// ❌ 危险! 无界队列, 可能 OOM
ExecutorService pool = Executors.newFixedThreadPool(10);

// ✅ 正确! 手动指定有界队列
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    10, 20, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(500),  // 有界队列!
    new ThreadPoolExecutor.CallerRunsPolicy()
);

// ⚠️ 同理: Executors.newCachedThreadPool() 最大线程数是 Integer.MAX_VALUE
// 也会导致创建大量线程而 OOM

@Async 与 CompletableFuture

方式说明适用场景
@Async注解方式, 方法异步执行简单异步(发邮件、写日志)
CompletableFuture编程式异步, 可编排多任务编排、并行查询
@Async + 返回 CompletableFuture两者结合需要编排的异步任务
// @Async 基础用法
@Async("taskExecutor")  // 指定线程池
public void sendEmail(String to, String content) {
    // 在线程池中异步执行, 不阻塞主线程
    mailSender.send(to, "Notification", content);
}

// CompletableFuture: 并行查询提高性能
// 场景:查用户详情需要查 3 张表(用户/订单/积分)
// 串行:50ms + 30ms + 40ms = 120ms
// 并行:三个同时跑,总耗时 = 最慢的那个 = 50ms

📝 简洁版
public UserDetail getUserDetail(Long userId) {
    CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(
            () -> userService.getById(userId),
            taskExecutor);

    CompletableFuture<List<Order>> orderFuture =
        CompletableFuture.supplyAsync(
            () -> orderService.getByUserId(userId),
            taskExecutor);

    CompletableFuture<Integer> pointsFuture =
        CompletableFuture.supplyAsync(
            () -> pointsService.getByUserId(userId),
            taskExecutor);

    CompletableFuture.allOf(
        userFuture, orderFuture, pointsFuture
    ).join();

    return new UserDetail(
        userFuture.join(),
        orderFuture.join(),
        pointsFuture.join()
    );
}
🔍 详尽版(逐行解释)
public UserDetail getUserDetail(Long userId) {

    // ━━━ 第1步:发起3个异步任务 ━━━
    // supplyAsync 写下去就立刻在子线程开始执行
    // 返回的 xxxFuture 是"取件条",不是结果本身

    CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.getById(userId), taskExecutor);
    // ↑ 线程1 立刻查用户,主线程不等,拿取件条继续走

    CompletableFuture<List<Order>> orderFuture =
        CompletableFuture.supplyAsync(() -> orderService.getByUserId(userId), taskExecutor);
    // ↑ 线程2 同时查订单

    CompletableFuture<Integer> pointsFuture =
        CompletableFuture.supplyAsync(() -> pointsService.getByUserId(userId), taskExecutor);
    // ↑ 线程3 同时查积分。到这里3个并行跑着

    // ━━━ 第2步:等全部跑完 ━━━
    // 主线程在这里阻塞,直到3个都完成
    CompletableFuture.allOf(userFuture, orderFuture, pointsFuture).join();
    // ↑ .join() 就是"等"

    // ━━━ 第3步:取出结果 ━━━
    // 已跑完,.join() 瞬间返回,不会阻塞
    return new UserDetail(
        userFuture.join(),       // 取出 User
        orderFuture.join(),      // 取出 List<Order>
        pointsFuture.join()      // 取出 Integer
    );
}

// ━━━━ 关键认知 ━━━━
// supplyAsync(...)  = 发起+立刻执行(不等,返回凭证)
// .join()           = 等结果(阻塞直到完成)
// allOf(...).join() = 等全部完成
// xxxFuture         = 不是结果,是"取件条"(快递单号)
关键认知:supplyAsync 不是"声明一个以后执行的任务",而是现在就扔到线程池开始跑,主线程不等、拿一个 Future(取件条)继续走。.join() 才是"等结果"。所以三个 supplyAsync 几乎同时发起,并行执行。

6JVM 内存与 GC 调优

OOM 和 GC 停顿是 Java 生产环境最常见的问题

JVM 堆内存结构

JVM Memory Model
堆内存 (Heap)
-Xms / -Xmx
+
非堆 (Metaspace/Stack)
-XX:MetaspaceSize
+
直接内存
-XX:MaxDirectMemorySize
堆内存分代结构
Young Generation
Eden + S0 + S1
新创建的对象
Minor GC (快, ~10ms)
Old Generation
经过多次 GC 存活
长期存活的对象
Major GC / Full GC (慢)
Metaspace
类元数据
Class / Method 信息
OutOfMemoryError: Metaspace

JDK 8+: PermGen 已移除, 改为 Metaspace (使用本地内存)

常用 GC 参数

# JVM 启动推荐参数 (Spring Boot 生产环境)
java \
  -Xms2g -Xmx2g \                              # 初始/最大堆大小 (建议相等, 避免动态扩缩)
  -XX:NewRatio=2 \                              # 老年代:新生代 = 2:1
  -XX:+UseG1GC \                                # 使用 G1 收集器 (JDK 9+ 默认)
  -XX:MaxGCPauseMillis=200 \                    # 目标停顿时间 200ms
  -XX:+HeapDumpOnOutOfMemoryError \             # OOM 时自动 dump
  -XX:HeapDumpPath=/tmp/oom-dump.hprof \        # dump 文件路径
  -XX:+PrintGCDetails -Xlog:gc*:/var/log/gc.log \ # GC 日志
  -jar myapp.jar

OOM 排查实战

🔍 Java 内存泄漏常见模式

1. 静态集合无限增长

// ❌ static Map 只进不出, 永远不清理
public class UserSession {
    private static final Map<String, User> sessions = new ConcurrentHashMap<>();

    public static void put(String token, User user) {
        sessions.put(token, user);  // 放进去就再也不删了!
    }
}

// ✅ 使用 Caffeine 缓存, 设置 TTL 自动淘汰
private final Cache<String, User> sessions = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterAccess(30, TimeUnit.MINUTES)
    .build();

2. ThreadLocal 未清理

// ❌ 线程池中 ThreadLocal 不 remove, 内存泄漏
public class RequestContext {
    private static final ThreadLocal<UserContext> context = new ThreadLocal<>();
}

// 在 Filter/Interceptor 中使用后必须清理
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object h, Exception e) {
    RequestContext.context.remove();  // ✅ 关键! 线程池复用线程, 不清理会泄漏
}

3. 数据库连接 / 流未关闭

// ❌ 连接泄露
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
// 忘记 close! 连接池耗尽, 最终 OOM

// ✅ try-with-resources 自动关闭
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT ...")) {
    // 使用 rs
}  // 自动 close, 无需 finally

OOM 排查工具链

# 1. 查看进程概况
jps -l                          # 列出 Java 进程
jstat -gcutil <pid> 1000 10     # 每秒打印 GC 概况, 共 10 次

# 2. 导出堆内存快照
jmap -dump:format=b,file=heap.hprof <pid>  # 手动 dump
# 或启动时加 -XX:+HeapDumpOnOutOfMemoryError 自动 dump

# 3. 查看对象统计
jmap -histo:live <pid> | head -20  # 存活对象 Top 20

# 4. 使用 Eclipse MAT 分析 dump 文件
# MAT 会自动检测泄漏嫌疑 (Leak Suspects Report)
# 查看 Dominator Tree, 找到占用最大的对象

# 5. 使用 Arthas 在线诊断 (不需要重启!)
$ java -jar arthas-boot.jar
[arthas@1234]$ dashboard         # 实时面板
[arthas@1234]$ thread -n 3       # CPU 最高的 3 个线程
[arthas@1234]$ heapdump /tmp/dump.hprof  # 在线 dump
[arthas@1234]$ vmtool --action getInstances --className java.util.HashMap --limit 10  # 查看对象实例

生产案例:内存从 500MB 涨到 2GB 后 OOM Killer

🚨 线上事故:Java 进程被 Linux OOM Killer 杀死

症状:Spring Boot 应用运行 12 小时后,Linux dmesg 显示 Out of memory: Killed process 1234 (java)。重启后重复。

排查过程

  1. 启动参数已加 -XX:+HeapDumpOnOutOfMemoryError,但 JVM 还没触发 OOM 就被系统杀了
  2. 添加 process.memoryUsage 等价的 JVM 监控:Prometheus + Micrometer 暴露 jvm_memory_used_bytes
  3. 发现堆内存呈阶梯式增长,每次定时任务执行后涨一截不回落
  4. jmap -histo:live 发现 byte[]String 对象最多
  5. 用 Arthas heapdump 导出快照,MAT 分析发现 ConcurrentHashMap 中存储了 800MB 的导出数据
  6. 根因:数据导出功能把全量数据加载到内存中的 Map,导出完成后没有清理引用

解决:改为流式导出(MyBatis Cursor + SXSSFWorkbook),内存占用从 800MB → 50MB。

// ❌ 旧代码: 全量加载到内存
List<Order> allOrders = orderMapper.selectAll();  // 100万条!
Map<Long, String> cache = new HashMap<>();
allOrders.forEach(o -> cache.put(o.getId(), o.toJson()));
// 导出后 cache 没有清理, 一直占用内存

// ✅ 新代码: 流式处理
try (Cursor<Order> cursor = orderMapper.scanAll();
     SXSSFWorkbook wb = new SXSSFWorkbook(100)) {  // 滑动窗口只保留100行
    Sheet sheet = wb.createSheet("Orders");
    int rowIdx = 0;
    for (Order order : cursor) {  // 每次只读一条
        Row row = sheet.createRow(rowIdx++);
        row.createCell(0).setCellValue(order.getId());
        // 写完即释放, 内存恒定
    }
}

💡 经验法则:任何"导出全部"功能都必须用流式处理,绝对不能一次性加载到内存。

7生产实践与优雅停机

不重视优雅停机 = 滚动更新丢请求

Graceful Shutdown(优雅停机)

Graceful Shutdown Flow
🛑 收到 SIGTERM / kill -15 信号
🛑 Web Server 停止接收新请求
server.shutdown=graceful
⏳ 等待进行中的请求完成
spring.lifecycle.timeout-per-shutdown-phase=30s
🔌 关闭 DataSource / Redis / MQ 连接
@PreDestroy / DisposableBean
✅ JVM 正常退出 (exit code 0)
# application.yml - 优雅停机配置
server:
  shutdown: graceful                    # 开启优雅停机 (默认: immediate)
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s     # 最长等待 30 秒

# K8s deployment.yaml
spec:
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 10"]  # 等 LB 摘除 Pod
    readinessProbe:                           # 就绪探针
      httpGet:
        path: /actuator/health/readiness
        port: 8080
    livenessProbe:                            # 存活探针
      httpGet:
        path: /actuator/health/liveness
        port: 8080
    terminationGracePeriodSeconds: 45         # K8s 等待时间
🚨 生产案例:K8s 滚动更新导致 502 错误

症状:K8s 滚动更新时,用户偶发 502 Bad Gateway 和支付超时。

原因:Pod 收到 SIGTERM 后立即被杀死(没有配置 graceful shutdown),进行中的 HTTP 请求被中断。同时 K8s 还在往这个 Pod 发新请求(LB 没来得及摘除)。

解决:三管齐下:

  1. 开启 server.shutdown=graceful,等待请求完成
  2. K8s preStop hook sleep 10,给 LB 时间摘除
  3. 配置 readinessProbe,收到 SIGTERM 后立即返回 NOT_READY

Actuator 监控端点

端点说明生产暴露
/health健康检查(DB/Redis/MQ 状态)✓ (必须)
/info应用信息(版本/构建时间)
/metricsPrometheus 指标✓ (监控)
/beans所有 Bean 列表✗ (仅内部)
/env环境变量和配置✗ (敏感信息)
/threaddump线程快照✗ (排查时开)
/heapdump堆内存快照✗ (排查时开)
/loggers动态日志级别✗ (线上排障)
# application.yml - Actuator 配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus  # 只暴露安全的端点
        exclude: env,beans,heapdump              # 排除敏感端点
  endpoint:
    health:
      show-details: when-authorized  # 需要认证才显示详细信息
      probes:
        enabled: true                # 开启 K8s 探针
  metrics:
    tags:
      application: my-spring-boot-app

# 依赖
# implementation 'org.springframework.boot:spring-boot-starter-actuator'
# implementation 'io.micrometer:micrometer-registry-prometheus'

动态日志级别调整(不重启排障)

# 查看当前日志级别
curl http://localhost:8080/actuator/loggers/com.example

# 临时调高日志级别 (排查后调回去)
curl -X POST http://localhost:8080/actuator/loggers/com.example \
  -H "Content-Type: application/json" \
  -d '{"configuredLevel": "DEBUG"}'

# 排查完毕, 调回 INFO
curl -X POST http://localhost:8080/actuator/loggers/com.example \
  -H "Content-Type: application/json" \
  -d '{"configuredLevel": "INFO"}'

8生产实战速查

面试官最爱问的"你项目里怎么做的" —— 每个都是生产级代码模板

全局异常处理(生产必备)

不写全局异常处理 = 每个接口都要 try-catch = 代码里全是重复的异常处理。用一个 @RestControllerAdvice 统一兜底。

// ✅ 生产标准写法:统一异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 业务异常(自定义异常,带错误码)
    @ExceptionHandler(BizException.class)
    public Result handleBiz(BizException e) {
        log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // 参数校验异常(@Valid 触发)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.error(400, msg);  // 前端直接展示
    }

    // 兜底:所有未捕获的异常
    @ExceptionHandler(Exception.class)
    public Result handleAll(Exception e) {
        log.error("系统异常", e);  // 记录完整堆栈
        return Result.error(500, "系统繁忙,请稍后重试");  // 不暴露内部细节
    }
}

// 自定义业务异常(带错误码,业务里直接 throw)
public class BizException extends RuntimeException {
    private final int code;
    public BizException(int code, String msg) { super(msg); this.code = code; }
    public int getCode() { return code; }
}

// 业务代码里直接抛(不用 try-catch)
public void pay(Long orderId) {
    if (order.getStatus() != PENDING) {
        throw new BizException(40001, "订单状态不允许支付");  // 全局处理器接住
    }
}
⚠️ 坑

handleAll(Exception.class) 里不要返回 e.getMessage() 给前端(会泄露 SQL/堆栈等敏感信息)。② handleValidation 的 message 要友好("手机号格式不正确" 而不是 "must match regex")。

参数校验(@Valid + JSR 303)

不用在业务方法里手写 if (name == null || name.isEmpty())。用注解一行搞定。

// 1. DTO 上加校验注解
public class OrderCreateDTO {
    @NotBlank(message = "订单名称不能为空")
    @Length(max = 50, message = "订单名称最多50字")
    private String name;

    @NotNull(message = "金额不能为空")
    @DecimalMin(value = "0.01", message = "金额必须大于0")
    private BigDecimal amount;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotNull(message = "商品列表不能为空")
    @Size(min = 1, max = 100, message = "商品数量1-100")
    @Valid  // 嵌套校验(校验列表里每个元素的注解)
    private List<OrderItemDTO> items;

    @Email(message = "邮箱格式不正确")
    private String email;
}

// 2. Controller 加 @Valid 触发校验
@PostMapping("/orders")
public Result<?> create(@Valid @RequestBody OrderCreateDTO dto) {
    // 到这里说明校验全过了,不用再 if 判空
    return Result.ok(orderService.create(dto));
}

// 3. 分组校验(同一DTO,不同场景不同校验规则)
public interface Create {}  // 创建组
public interface Update {}  // 更新组

public class UserDTO {
    @Null(groups = Create.class)  // 创建时必须为空(自动生成)
    @NotNull(groups = Update.class)  // 更新时必须不为空
    private Long id;

    @NotBlank(groups = {Create.class, Update.class})
    private String name;
}

@PutMapping("/users")
public Result<?> update(@Validated(Update.class) @RequestBody UserDTO dto) { ... }
📌 常用注解速查

@NotBlank(字符串非空) / @NotNull(非null) / @NotEmpty(集合非空) / @Size(长度) / @Min/@Max(数值范围) / @Pattern(正则) / @Email / @Valid(嵌套)

统一返回格式

// 统一返回结构(所有接口返回这个)
@Data
public class Result<T> {
    private int code;       // 0=成功,非0=失败
    private String message; // 提示信息
    private T data;         // 业务数据
    private String traceId; // 链路ID(排查用)

    public static <T> Result<T> ok(T data) { return build(0, "success", data); }
    public static <T> Result<T> error(int code, String msg) { return build(code, msg, null); }
    private static <T> Result<T> build(int c, String m, T d) {
        Result<T> r = new Result<>();
        r.code = c; r.message = m; r.data = d;
        r.traceId = MDC.get("traceId");  // 从日志上下文取
        return r;
    }
}

// 分页结果(列表查询统一用)
@Data
public class PageResult<T> {
    private List<T> list;
    private long total;
    private int page;
    private int size;
    // mybatis-plus 直接用 IPage,spring-data 用 Page
}

CORS 跨域配置

// 方式1:全局配置(推荐,一次配好所有接口)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("https://www.example.com", "http://localhost:3000")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .allowCredentials(true)  // 允许带 Cookie
            .maxAge(3600);            // 预检请求缓存1小时
    }
}

// 方式2:单个接口(@CrossOrigin,不推荐全局用)
@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class UserController { ... }

// 方式3:生产环境推荐用 Nginx 反代(同域,彻底没有跨域问题)
// location /api/ { proxy_pass http://backend:8080/; }
⚠️ 坑

allowCredentials(true)allowedOrigins 不能用 *(浏览器规范禁止),要用 allowedOriginPatterns。② Spring Security 环境下要在 SecurityConfig 里也配 CORS。

自定义 Starter(面试高频)

// 场景:封装公司内部的短信SDK,让别人引入 starter 就能用

// 1. 配置属性类
@Data
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
    private String accessKey;
    private String secretKey;
    private String signName = "默认签名";
    private int timeout = 3000;
}

// 2. 自动配置类
@Configuration
@ConditionalOnClass(SmsClient.class)           // classpath有SmsClient才生效
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // 用户没自己定义才创建
    public SmsClient smsClient(SmsProperties props) {
        return new SmsClient(props.getAccessKey(), props.getSecretKey(), props.getTimeout());
    }
}

// 3. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 3.x)
//    或 META-INF/spring.factories(Boot 2.x)
// 内容:
// com.example.sms.SmsAutoConfiguration

// 4. 使用方只需 application.yml 配置 + pom 引入 starter
// sms:
//   access-key: xxx
//   secret-key: yyy
// 然后 @Autowired SmsClient 直接用

多环境配置 & 外部化

# 目录结构
src/main/resources/
├── application.yml            # 公共配置
├── application-dev.yml        # 开发环境
├── application-test.yml       # 测试环境
└── application-prod.yml       # 生产环境

# application.yml
spring:
  profiles:
    active: @profiles.active@  # Maven filter 注入
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:3306/app  # 环境变量+默认值

# 启动时指定环境
java -jar app.jar --spring.profiles.active=prod
# 或环境变量
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
# 或 Docker
docker run -e SPRING_PROFILES_ACTIVE=prod myapp

# 敏感配置不放代码库(用环境变量/Nacos/Vault)
spring:
  datasource:
    password: ${DB_PASSWORD}  # 从环境变量读,不写死

接口文档(Knife4j / SpringDoc)

// Spring Boot 3.x 用 springdoc-openapi(替代旧版 Swagger)
// pom: springdoc-openapi-starter-webmvc-ui

// Controller 注解
@Tag(name = "订单管理", description = "订单CRUD")
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Operation(summary = "创建订单")
    @Parameters({@Parameter(name = "X-Token", description = "登录令牌", in = ParameterIn.HEADER, required = true)})
    @PostMapping
    public Result<OrderVO> create(@Valid @RequestBody OrderCreateDTO dto) { ... }
}

// DTO 字段描述
@Schema(description = "订单创建请求")
public class OrderCreateDTO {
    @Schema(description = "订单名称", example = "iPhone 15", requiredMode = REQUIRED)
    @NotBlank
    private String name;
}

// 访问 http://localhost:8080/swagger-ui.html 或 /doc.html (Knife4j)

文件上传 & 下载

// 上传(限制大小 + 类型)
@PostMapping(value = "/upload", consumes = "multipart/form-data")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
    // 1. 校验
    if (file.isEmpty()) throw new BizException(400, "文件不能为空");
    if (file.getSize() > 10 * 1024 * 1024) throw new BizException(400, "文件不能超过10MB");
    String suffix = FilenameUtils.getExtension(file.getOriginalFilename());
    if (!Arrays.asList("jpg", "png", "pdf").contains(suffix.toLowerCase()))
        throw new BizException(400, "不支持的文件类型");

    // 2. 存储到 OSS(不要存本地,容器重启就没了)
    String url = ossClient.upload(file.getInputStream(), "order/" + UUID.randomUUID() + "." + suffix);
    return Result.ok(url);
}

// application.yml 限制上传大小
// spring:
//   servlet:
//     multipart:
//       max-file-size: 10MB
//       max-request-size: 50MB

// 大文件下载(流式输出,不撑内存)
@GetMapping("/download/{id}")
public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition", "attachment; filename=\"report.xlsx\"");
    orderService.exportExcel(id, response.getOutputStream());  // 流式写
}

Redis 序列化配置(生产必改)

// Spring Boot 默认用 JdkSerializationRedisSerializer
// 问题:存进去是乱码(二进制),redis-cli 看不懂,不同语言不能共用
// 生产标配:改成 JSON 序列化

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // Key 用 String 序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());

        // Value 用 JSON 序列化(可读,跨语言)
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL);  // 保留类型信息
        jsonSerializer.setObjectMapper(om);

        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

定时任务

// 单机定时任务(简单场景够用)
@EnableScheduling
@SpringBootApplication
public class App { ... }

@Component
public class ReportTask {

    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
    public void generateDailyReport() {
        log.info("开始生成日报");
        reportService.generate();
    }

    @Scheduled(fixedDelay = 60000)  // 上次执行完1分钟后再次执行
    public void syncData() { ... }

    @Scheduled(fixedRate = 5000)  // 每5秒(不管上次有没有执行完)
    public void heartbeat() { ... }
}

// 分布式定时任务(多实例只执行一次)
// 方案1:XXL-JOB / ElasticJob(专业调度平台)
// 方案2:Redis 分布式锁(轻量)
@Scheduled(cron = "0 0 2 * * ?")
public void generateReport() {
    RLock lock = redisson.getLock("lock:report:daily");
    if (!lock.tryLock(0, 30, TimeUnit.MINUTES)) return;  // 没抢到锁就跳过
    try { reportService.generate(); }
    finally { lock.unlock(); }
}
⚠️ 坑

@Scheduled 默认单线程执行,多个任务会互相阻塞。配 spring.task.scheduling.pool.size=5 改成多线程。② 多实例部署时同一任务会被执行多次,必须加分布式锁。

9面试题精选 30 道

覆盖基础、核心机制、生产实战,每题附答案框架与追问

Q1.Easy Spring Boot 的核心注解是什么?包含哪些?
答案框架@SpringBootApplication = @SpringBootConfiguration(标记配置类)+ @EnableAutoConfiguration(开启自动配置)+ @ComponentScan(包扫描)。自动配置通过 spring.factories 中声明的 AutoConfiguration 类,配合 @Conditional 条件注解决定是否加载。
🔄 追问:如何禁用某个自动配置?@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
Q2.Easy IoC 和 DI 有什么区别?
答案框架:IoC(控制反转)是一种设计思想——对象的创建和管理交给容器。DI(依赖注入)是实现 IoC 的方式——容器自动把依赖注入到对象中。Spring 通过构造器注入、Setter 注入、字段注入(@Autowired)三种方式实现 DI。
🔄 追问:推荐哪种注入方式?(构造器注入,因为不可变、可测试、循环依赖时启动报错而不是运行时 NPE)
Q3.Easy Bean 的作用域有哪些?默认是哪个?
答案框架:默认是 singleton。还有 prototype(每次获取新建)、request(HTTP 请求级别)、session(会话级别)、application(应用级别)。
🔄 追问:singleton 中注入 prototype 会怎样?(只注入一次,prototype 失效。用 @Lookup 解决)
Q4.Easy @Autowired 和 @Resource 的区别?
答案框架:@Autowired 是 Spring 注解,默认按类型注入,找到多个时配合 @Qualifier 指定名称。@Resource 是 JSR-250 标准注解,默认按名称注入。推荐用 @Autowired + @Qualifier。
Q5.Medium Bean 的生命周期是什么?
答案框架:5 个阶段——① 实例化(构造方法)→ ② 属性填充(@Autowired)→ ③ 初始化(@PostConstruct、InitializingBean.afterPropertiesSet、BeanPostProcessor)→ ④ 正常使用 → ⑤ 销毁(@PreDestroy、DisposableBean.destroy)。其中 BeanPostProcessor 是 AOP 实现的关键钩子。
🔄 追问:BeanPostProcessor 在哪一步执行?(初始化阶段的前后:postProcessBeforeInitialization 和 postProcessAfterInitialization)
Q6.Medium Spring 如何解决循环依赖?三级缓存是什么?
答案框架:三级缓存——singletonObjects(一级:完整 Bean)、earlySingletonObjects(二级:提前曝光的半成品)、singletonFactories(三级:ObjectFactory)。流程:A 创建后放入三级 → A 填充属性需要 B → B 创建后填充属性需要 A → 从三级缓存拿到 A 的工厂 → 生成 A 的早期引用放入二级 → B 完成 → A 完成。三级缓存的意义是处理 AOP 代理。
🔄 追问:构造器循环依赖能解决吗?(不能,因为实例化阶段还没完成,无法提前暴露)
Q7.Medium @Transactional 失效有哪些场景?
答案框架:5 大场景——① 方法内部 this 调用(不经过代理)→ 用 self 或 AopContext;② 非 public 方法(Spring AOP 只代理 public);③ 异常被 catch 吞掉(代理看不到异常);④ rollbackFor 不匹配(默认只回滚 RuntimeException);⑤ 数据库引擎不支持事务(MyISAM)。
🏭 生产连接:多表写入必须加事务,且加在被外部调用的方法上。
Q8.Medium JDK 动态代理和 CGLIB 有什么区别?
答案框架:JDK 动态代理基于接口,通过 Proxy.newProxyInstance + InvocationHandler,要求目标类实现接口。CGLIB 基于继承,通过 Enhancer + MethodInterceptor 生成子类,不需要接口。Spring Boot 2.x 默认使用 CGLIB(proxy-target-class=true)。final 类/方法无法被 CGLIB 代理。
🔄 追问:CGLIB 为什么不能代理 final 方法?(final 不能被覆盖/重写)
Q9.Medium Spring Boot 自动配置的原理是什么?
答案框架:核心是 @EnableAutoConfiguration → AutoConfigurationImportSelector → 读取 META-INF/spring.factories(Boot 2.x)/ spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Boot 3.x)→ 加载所有 AutoConfiguration 类 → 每个 AutoConfiguration 通过 @ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean 等条件判断是否生效。
🔄 追问:如何自定义一个 Starter?(编写 AutoConfiguration 类 + spring.factories + 条件注解)
Q10.Hard 如何排查 Java 内存泄漏?
答案框架:1) jstat -gcutil 观察 GC 频率和堆使用趋势;2) jmap -histo:live 查看对象统计 Top N;3) jmap -dump 或 Arthas heapdump 导出堆快照;4) Eclipse MAT 分析 Leak Suspects,Dominator Tree 找到占用最大的对象;5) 检查是否 static Map/List 无限增长、ThreadLocal 未 remove、连接未关闭。
🏭 生产连接:监控 jvm_memory_used_bytes 指标,设置阈值告警。
Q11.Medium DispatcherServlet 的处理流程是什么?
答案框架:请求到达 → DispatcherServlet.doDispatch → HandlerMapping 找到 HandlerExecutionChain(Controller 方法 + Interceptor)→ HandlerAdapter 执行 → Interceptor.preHandle → Controller 方法 → Interceptor.postHandle → ViewResolver/MessageConverter 处理返回值 → Interceptor.afterCompletion。
🔄 追问:HandlerMapping 有哪些?(RequestMappingHandlerMapping、BeanNameUrlHandlerMapping 等)
Q12.Medium Filter 和 Interceptor 有什么区别?
答案框架:Filter 是 Servlet 规范,作用于 DispatcherServlet 前后,不能直接使用 Spring Bean。Interceptor 是 Spring MVC 的,作用于 Controller 方法前后,可以 @Autowired。Filter 适合 CORS/编码/安全头等通用处理,Interceptor 适合登录校验/权限/日志等业务相关处理。执行顺序:Filter → Interceptor.preHandle → Controller → Interceptor.postHandle → Filter。
🔄 追问:Spring Security 用的是 Filter 还是 Interceptor?(Filter,SpringSecurityFilterChain)
Q13.Medium 线程池的 7 个参数是什么?如何合理设置?
答案框架:corePoolSize、maxPoolSize、keepAliveTime、unit、workQueue、threadFactory、rejectedExecutionHandler。CPU 密集型:corePoolSize = N+1(N=CPU 核心数)。IO 密集型:corePoolSize = 2N 或 N*(1+W/C)。队列必须有界(防止 OOM)。拒绝策略推荐 CallerRunsPolicy(让调用者线程执行,天然限流)。
🏭 生产连接:永远不要用 Executors.newFixedThreadPool/CachedThreadPool(无界队列/无限线程),必须手动创建 ThreadPoolExecutor。
Q14.Hard @Async 的原理是什么?有什么坑?
答案框架:@Async 通过 AOP 代理实现,方法被代理后提交到异步线程池执行。坑:① 类内部调用失效(同 @Transactional);② 默认使用 SimpleAsyncTaskExecutor(每次创建新线程!),必须自定义线程池;③ 异常不会传播到调用者,需要 AsyncUncaughtExceptionHandler;④ 返回值用 CompletableFuture 或 AsyncResult。
🏭 生产连接:@Async("taskExecutor") 必须指定线程池。
Q15.Hard CompletableFuture 如何实现多任务编排?
答案框架:supplyAsync 异步执行 → thenApply/thenAccept 转换结果 → thenCombine 合并两个 → allOf/anyOf 等待全部/任一完成 → exceptionally/handle 处理异常 → join/get 获取结果。典型场景:并行调用多个服务接口,总耗时 = max(各接口耗时)。
🔄 追问:join 和 get 的区别?(join 不抛 checked exception,get 需要 try-catch InterruptedException)
Q16.Medium JVM 内存区域划分是怎样的?
答案框架:堆(Young/Old Generation,对象存储)、Metaspace(类元数据,JDK 8 替代 PermGen)、栈(方法栈帧,局部变量/操作数栈)、本地方法栈(Native 方法)、程序计数器(当前执行位置)。OOM 类型:Java heap space(堆)、Metaspace、Direct buffer memory、GC overhead limit exceeded。
🔄 追问:栈会 OOM 吗?(StackOverflowError,递归太深。也可以 OOM 但少见)
Q17.Medium G1 和 CMS 收集器有什么区别?
答案框架:CMS(已废弃 JDK 14):老年代并发标记-清除,低延迟但有碎片问题。G1(JDK 9+ 默认):Region 化内存布局,混合回收(Young + Old),可预测停顿时间(-XX:MaxGCPauseMillis)。G1 更适合大堆(>6GB),CMS 适合小堆。JDK 11+ 推荐 ZGC/Shenandoah(亚毫秒级停顿)。
🔄 追问:G1 的 Region 大小怎么计算?(1-32MB,等于堆大小/2048,2 的幂次)
Q18.Hard 如何监控 Spring Boot 应用的性能?
答案框架:1) Actuator + Micrometer + Prometheus + Grafana 监控 JVM/HTTP/自定义指标;2) 日志(Logback + ELK),结构化 JSON 日志;3) 分布式追踪(OpenTelemetry + Jaeger/Zipkin),traceId 贯穿全链路;4) APM 工具(SkyWalking/Pinpoint);5) JVM 层面:jstat/jmap/Arthas。
🏭 生产连接:设置 P99 响应时间、错误率、GC 停顿时间、堆内存水位告警。
Q19.Easy Spring Boot Profile 是什么?如何使用?
答案框架:Profile 实现多环境配置。application-dev.yml / application-prod.yml,启动时 --spring.profiles.active=prod。也可以在代码中用 @Profile("dev") 控制 Bean 的加载。@ConfigurationProperties 可以配合 Profile 实现不同环境的配置映射。
Q20.Medium @Conditional 系列注解有哪些?
答案框架:@ConditionalOnClass(classpath 有指定类)、@ConditionalOnMissingBean(容器中没有指定 Bean)、@ConditionalOnProperty(配置了指定属性)、@ConditionalOnWebApplication(Web 环境)、@ConditionalOnExpression(SpEL 表达式)。这些是自动配置的核心条件判断机制。
🔄 追问:如何自定义 Condition?(实现 Condition 接口,重写 matches 方法)
Q21.Medium 如何实现一个大文件导出功能?
答案框架:1) MyBatis Cursor 游标查询(不一次性加载到内存);2) Apache POI SXSSFWorkbook(滑动窗口写入 Excel);3) EasyExcel(Alibaba 开源,基于 SAX 流式读取);4) 超大数据异步导出——生成文件后上传 OSS,发通知给用户下载链接;5) 限制查询条数,分页导出。
🏭 生产连接:导出 100 万条数据,内存控制在 100MB 以内。
Q22.Hard Spring Boot 启动流程是什么?
答案框架:SpringApplication.run → 创建 SpringApplication 实例(推断 Web 类型、加载 ApplicationContextInitializer/Listener)→ 准备环境(读 application.yml/环境变量)→ 创建 ApplicationContext → refreshContext(核心!加载 BeanDefinition → 创建 Bean → 自动配置 → 启动内嵌 Tomcat)→ CommandLineRunner/ApplicationRunner 执行。
🔄 追问:如何在容器启动后执行代码?(CommandLineRunner / ApplicationRunner / @PostConstruct)
Q23.Easy Spring Boot Starter 是什么?如何自定义?
答案框架:Starter 是一组约定的依赖集合,引入一个 Starter 自动拉入所有需要的库。自定义步骤:1) 编写 AutoConfiguration 类(@Configuration + @Conditional + @Bean);2) 编写 @ConfigurationProperties 属性类;3) 在 META-INF/spring.factories 中声明 AutoConfiguration;4) 打包发布。
Q24.Medium 如何保证 Spring Boot 应用的高可用?
答案框架:1) 多实例部署 + 负载均衡(Nginx/K8s Service);2) 健康检查(Actuator + K8s Probe);3) 优雅停机(graceful shutdown + preStop hook);4) 限流/熔断(Resilience4j/Sentinel);5) 配置中心(Nacos/Apollo)动态配置;6) 日志/监控/告警(ELK + Prometheus + Grafana);7) 数据库主从 + Redis 集群。
🏭 生产连接:SLA 99.99% 需要多副本 + 多可用区 + 自动故障转移。
Q25.Hard 如何排查 Spring Boot 应用启动慢的问题?
答案框架:1) 开启启动日志:--debuglogging.level.org.springframework=DEBUG;2) Spring Boot 2.x+ 使用 ApplicationStartup 记录启动步骤耗时;3) 检查 @ComponentScan 范围是否过大;4) 检查是否有耗时的 @PostConstruct 方法;5) 检查数据库连接池初始化(HikariCP 初始化连接);6) jfr(Java Flight Recorder)录制启动过程分析。
🔄 追问:如何优化启动速度?(延迟初始化 spring.main.lazy-initialization=true,减少自动配置 exclude)
Q26.Hard 设计一个高并发短链接服务,Spring Boot 技术栈怎么选?
答案框架:Spring Boot WebFlux(响应式非阻塞)或 Spring MVC + 线程池调优 + Redis(短码→长URL映射,10万+ QPS)+ Bloom Filter(防重复)+ Caffeine 本地缓存(热点短码)+ ClickHouse(访问统计)。写入用 Kafka 异步。短码用 Base62 编码自增 ID(分布式 ID 用雪花算法或 Redis incr)。
🏭 生产连接:短链接服务核心指标是读 QPS,Redis Cluster + Caffeine 二级缓存可支持 50万+ QPS。
Q27.Medium 如何在不重启服务的情况下动态刷新配置?
答案框架:1) Spring Cloud Config + @RefreshScope + /actuator/refresh 端点(手动触发);2) Nacos/Apollo Config 配置中心(自动推送 + 监听变更);3) @ConfigurationProperties + @RefreshScope,配置变更后 Bean 自动重建。关键是配合 Environment 和 ConfigurableEnvironment 实现运行时属性更新。
🔄 追问:@RefreshScope 的原理是什么?(基于 CGLIB 代理,配置变更时销毁旧 Bean 重新创建)
Q28.Medium Spring Boot 中的 @Async 有哪些坑?
答案框架:1) 类内部调用失效(需要经过代理);2) 默认 SimpleAsyncTaskExecutor 每次创建新线程(必须自定义 ThreadPoolTaskExecutor);3) 异常不传播(配置 AsyncUncaughtExceptionHandler);4) 如果在 Filter/Interceptor 中调 @Async 方法,ThreadLocal/RequestContext 会丢失(需要 TaskDecorator 传递上下文);5) 不能在 private/protected 方法上使用。
🔄 追问:如何在线程间传递 MDC traceId?(实现 TaskDecorator,在 decorate 时复制 MDC context)
Q29.Hard AOP 中的通知(Advice)类型有哪些?
答案框架:5 种通知——@Before(方法执行前)、@After(方法执行后,无论成功失败)、@AfterReturning(方法成功返回后)、@AfterThrowing(方法抛异常后)、@Around(环绕,最强大,可以控制是否执行、修改参数和返回值)。@Around 的 ProceedingJoinPoint.proceed() 决定是否执行目标方法。事务、缓存都是 @Around 实现。
🔄 追问:多个 @Around 的执行顺序?(按 @Order 值小的先执行,嵌套结构像洋葱模型)
Q30.Hard 你在实际项目中遇到过哪些 Spring Boot 性能问题?如何解决的?
答案框架(参考模板)
1. OOM:全量导出加载到内存 → 改为 MyBatis Cursor + EasyExcel 流式处理
2. 事务失效:内部 this 调用 → 注入 self 或 @Transactional 加在外部方法上
3. N+1 查询:循环中逐条查 DB → 批量查询 + MyBatis foreach + 二次组装
4. 线程池耗尽:无界队列堆积 → 有界队列 + CallerRunsPolicy + 监控告警
5. GC 停顿:Young 区太小导致频繁 Minor GC → 调大 -Xmn 或换 G1

回答技巧:STAR 法则(Situation → Task → Action → Result),量化结果("响应时间从 3s 降到 200ms")。
🏭 这是开放题,提前准备 2-3 个真实案例。
Q31.Medium @ConfigurationProperties 和 @Value 的区别?
答案框架:@Value 单个字段注入,不支持复杂数据结构、不支持松散绑定。@ConfigurationProperties 批量绑定整个对象,支持松散绑定(my-app-namemyAppName)、JSR303 校验、Map/List。生产推荐 @ConfigurationProperties(结构化、可维护)。
Q32.Medium Spring Boot 如何实现全局异常处理?
答案框架@RestControllerAdvice + @ExceptionHandler。分三层处理:① 业务异常(自定义 BizException,返回业务错误码)② 参数校验异常(MethodArgumentNotValidException,返回字段+错误信息)③ 兜底异常(Exception.class,记录堆栈但不暴露给前端)。
🏭 关键:兜底异常返回 "系统繁忙" 而非 e.getMessage()(防止泄露 SQL/堆栈)。
Q33.Easy @Valid 和 @Validated 的区别?
答案框架:@Valid 是 JSR-303 标准注解,支持嵌套校验(@Valid 在集合字段上校验每个元素)。@Validated 是 Spring 扩展,支持分组校验(同一DTO在Create/Update时用不同规则)。Controller 方法参数上加 @Validated(Update.class) 即可指定校验组。
Q34.Medium Spring Boot 3.x 有什么重大变化?
答案框架:① 最低 JDK 17(不再支持 8/11)② Jakarta EE(javax.* → jakarta.*,所有 import 要改)③ 自动配置文件从 spring.factories 改为 AutoConfiguration.imports ④ 原生支持 GraalVM Native Image ⑤ Micrometer 替代 Actuator 指标。迁移注意:所有 javax.servlet.* 要改成 jakarta.servlet.*
Q35.Hard @Async 方法中如何获取 SecurityContext / RequestAttributes?
答案框架:@Async 切到新线程后,ThreadLocal 上下文(Security、RequestAttributes、MDC traceId)会丢失。解法:配置 TaskDecorator,在提交任务时复制主线程上下文到子线程:
executor.setTaskDecorator(runnable -> {
    RequestAttributes ctx = RequestContextHolder.getRequestAttributes();
    String traceId = MDC.get("traceId");
    return () -> {
        try {
            if (ctx != null) RequestContextHolder.setRequestAttributes(ctx);
            if (traceId != null) MDC.put("traceId", traceId);
            runnable.run();
        } finally {
            RequestContextHolder.resetRequestAttributes();
            MDC.clear();
        }
    };
});
Spring Security 场景用 DelegatingSecurityContextAsyncTaskExecutor 自动传递。
📚 学习路线建议
阶段内容推荐资源
入门IoC/DI/AOP 基础、Spring Boot 脚手架Spring Guides (spring.io)
进阶Bean 生命周期、AOP 原理、事务原理Spring 源码深度解析 (书)
高级JVM 调优、自动配置源码、线程池调优极客时间《深入拆解 Java 虚拟机》
生产监控告警、GC 调优、分布式架构Spring Boot Production-Ready (docs)