文章目录
-
- [一、 三种方案的代码演练](#一、 三种方案的代码演练)
-
- [1. AOP(Aspect 面向切面)方案 ------ 针对 Controller 方法(最推荐)](#1. AOP(Aspect 面向切面)方案 —— 针对 Controller 方法(最推荐))
- [2. Interceptor(Spring 拦截器)方案 ------ 针对 Spring MVC 链路](#2. Interceptor(Spring 拦截器)方案 —— 针对 Spring MVC 链路)
- [3. Filter(Servlet 过滤器)方案 ------ 针对最外层 HTTP 请求](#3. Filter(Servlet 过滤器)方案 —— 针对最外层 HTTP 请求)
- [二、 面试官角度:三种方案的本质区别(核心考点)](#二、 面试官角度:三种方案的本质区别(核心考点))
- [三、 复盘:你在面试中的回答到底对不对?](#三、 复盘:你在面试中的回答到底对不对?)
-
- [1. 得分点(说得对的地方)](#1. 得分点(说得对的地方))
- [四、 下一次面试,如何给出"满分回答"?](#四、 下一次面试,如何给出“满分回答”?)
根据你提供的面试复盘音频记录,面试官在 18:29 问到了你这个问题:"如果说在代码中,如果要实现接口的参数,耗时,接口花了多长时间,日志的打印,你会去怎么做?"
首先给你打个气,你提到了 AOP(面向切面编程) 并在前后获取时间戳相减,这个核心思路是完全正确的! 但是,你在细节表达上有些含糊(比如提到了"ConcurrentMedia"这个不存在的词 ,可能是想说 System.currentTimeMillis() 或者是 ProceedingJoinPoint),而且没有展开具体的落地方案。面试官最后也建议你:"可以调整一下自己的思路,写出来比说要好一点" 。
为了帮你完美复盘并准备接下来的面试,下面我用 Spring Boot 的三种主流拦截机制(AOP、Interceptor、Filter) 分别为你演示怎么实现这个功能,并客观评估你当时的面试回答。
一、 三种方案的代码演练
在 Spring Boot 中,统计接口参数和耗时通常有以下三种做法。我们由浅入深,从你提到的 AOP 开始:
1. AOP(Aspect 面向切面)方案 ------ 针对 Controller 方法(最推荐)
AOP 能够直接获取到 Spring 解析后的 Java 对象入参,最适合用来做复杂的日志审计和耗时统计。
java
@Aspect
@Component
@Slf4j
public class LogAspect {
@Resource
private ObjectMapper objectMapper; // 用于序列化参数
// 拦截所有 controller 包下的方法
@Pointcut("execution(public * com.example.demo.controller..*.*(..))")
public void controllerLog() {}
@Around("controllerLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis(); // 记录开始时间
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 获取入参对象数组
Object[] args = joinPoint.getArgs();
log.info("【AOP日志】进入方法: {}, 参数: {}", methodName, objectMapper.writeValueAsString(args));
// 执行核心业务方法
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime; // 计算耗时
log.info("【AOP日志】退出方法: {}, 耗时: {}ms", methodName, executionTime);
return result;
}
}
2. Interceptor(Spring 拦截器)方案 ------ 针对 Spring MVC 链路
拦截器属于 Spring MVC 级别,可以在请求到达 Controller 之前和之后进行拦截,非常适合配合 ThreadLocal 统计耗时。
java
@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
// 使用 ThreadLocal 保证多线程环境下线程安全地记录各自请求的开始时间
private static final ThreadLocal<Long> TIME_THREAD_LOCAL = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
TIME_THREAD_LOCAL.set(startTime); // 存入当前线程
// 打印基础请求参数(URL参数)
log.info("【拦截器】请求 URI: {}, Method: {}, 参数: {}", request.getRequestURI(), request.getMethod(), request.getParameterMap());
return true; // 放行
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Long startTime = TIME_THREAD_LOCAL.get();
if (startTime != null) {
long endTime = System.currentTimeMillis();
log.info("【拦截器】请求 URI: {} 处理完成,总耗时: {}ms", request.getRequestURI(), (endTime - startTime));
TIME_THREAD_LOCAL.remove(); // 极其重要:用完必须释放,防止内存泄漏
}
}
}
3. Filter(Servlet 过滤器)方案 ------ 针对最外层 HTTP 请求
过滤器属于标准 Servlet 容器级别(如 Tomcat),是全系统最外层的防线。它的生命周期甚至比 Spring 还要早。
java
@Component
@WebFilter(urlPatterns = "/*")
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest context, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) context;
long startTime = System.currentTimeMillis();
log.info("【过滤器】开始拦截请求: {}", request.getRequestURI());
// 放行,让请求继续往后走(走拦截器、Controller等)
chain.doFilter(context, response);
long executionTime = System.currentTimeMillis() - startTime;
log.info("【过滤器】请求: {} 结束,耗时: {}ms", request.getRequestURI(), executionTime);
}
}
二、 面试官角度:三种方案的本质区别(核心考点)
如果你能在面试中主动说出下面这张表对比,面试官会觉得你的计算机网络和 Spring 底层功底极其扎实:
| 维度 | Filter (过滤器) | Interceptor (拦截器) | AOP (切面) |
|---|---|---|---|
| 所属框架 | Servlet 容器规范(Tomcat) | Spring MVC 框架 | Spring Core 框架 |
| 拦截边界 | HTTP 请求的最外层入口 | 进入 SpringMVC 但在 Controller 之前 | 具体的某一个方法(Method) |
| 获取入参的便利度 | 极其困难 。只能拿到原始 Http 字节流,如果读了 RequestBody 后面业务就读不到了。 |
中等 。能拿到 request.getParameter(),但同样存在 Body 只能读一次的问题。 |
极其轻松 。Spring 已经帮我们把参数反序列化成了 Java 对象 (joinPoint.getArgs())。 |
| 核心应用场景 | 全局跨域(CORS)、安全过滤(XSS防注入)、全局解密。 | 权限校验(鉴权)、TraceId 注入。 | 性能监控(统计耗时)、事务控制、操作审计日志。 |
三、 复盘:你在面试中的回答到底对不对?
针对你当时的现场表现,我为你做一个客观的"诊断报告":
1. 得分点(说得对的地方)
思路切中要害 :你第一时间回答了"可以用 AOP 去做" ,并且提到了 around(环绕通知),还知道是在方法执行前后分别获取时间做减法 。在逻辑闭环上是完全成立的。
拓展知识面广 :你顺带提到了可以使用 Arthas(阿尔萨斯) ,说明你具备线上排查慢接口的实际工具经验。
- 失分点(导致你最后"心虚"和减分的地方)
专业术语口误(致命伤) :你在描述时间戳或者连接点时,连续说了两次 "获取一个 ConcurrentMedia" 。Java 和 Spring 里完全没有这个类(你可能脑子里想的是 ConcurrentHashMap 或者 System.currentTimeMillis() 的杂交语)。这会让面试官觉得你只是背了概念,没有真正写过代码。
缺乏落地方案 :当面试官追问"有哪几种方式"以及"你会拦截哪一层"时 ,你的语言开始细碎重复("就是那回事嘛......" 这种口头禅容易显得不自信) 。你没有清晰地划分出 Filter -> Interceptor -> AOP 这一条清晰的拦截链路。
四、 下一次面试,如何给出"满分回答"?
如果下次再被问到:"Spring Boot 里怎么统计接口参数和耗时并打印日志?"
🗣️ 你应该这样优雅地回答:
"在 Spring Boot 中,通常有 Filter、Interceptor 和 AOP 三种方式可以拦截请求并统计耗时。
在实际生产中,对于'统计接口入参和耗时'这个特定需求,我个人最推荐使用 Spring AOP 结合
@Around环绕通知来实现 。因为如果使用 Filter 或 Interceptor,由于 HTTP 请求的
RequestBody输入流只能读取一次,如果我们为了打印日志强制去读取它,就会导致后续的 Controller 无法正常解析参数(即流关闭异常)。而使用 AOP ,由于它切入的是方法级别,我们能通过ProceedingJoinPoint.getArgs()直接拿到 Spring 已经反序列化好的 Java 对象入参 ,对核心业务完全做到零侵入。
我的落地做法是:在环绕通知中,先通过
System.currentTimeMillis()记录当前时间,并利用objectMapper将入参对象转为 JSON 字符串打印出来(注意要过滤掉类似HttpServletRequest这种无法序列化的内置对象)。接着调用joinPoint.proceed()执行业务,最后再次获取时间戳相减,算出方法总耗时。此外,为了方便线上监控,我通常会在代码里设定一个慢响应阈值 (比如 3 秒)。一旦耗时超过 3 秒,日志级别会从
INFO提升为WARN,并在日志头部打上【接口慢响应警告】标签,方便后续我们在 ELK 日志平台中通过脚本直接拉取和报警。"