在项目里,日志开关这个需求其实特别常见,尤其是 SpringBoot 项目跑到线上以后,经常会有人提「能不能不重启就把某个模块的日志打开」这种需求。很多人一开始想到的就是去改 logback 配置文件,但这样要么得重启,要么得写监听器,还挺麻烦的。其实用 Spring AOP 加一点点热插拔思路,就能比较优雅地搞定。
我之前遇到过一次,运维同事说线上一个接口偶尔耗时很长,让我加点日志看看。但是那会接口是个高频调用的,直接打开 DEBUG 日志简直要命,日志量暴涨,磁盘都快打满了。所以思路就是:做个开关,能随时打开或关闭某些类或方法的日志,而且不用重启。
思路设计
用 AOP 的好处就是,它本身就能在方法调用前后织入逻辑,我们只要在合适的地方判断下「这个方法是不是需要打印日志」,就能做到动态控制。剩下的就是怎么存这个「开关」信息了,常见的办法是放在内存里(比如一个ConcurrentHashMap),再暴露个接口让运维随时改。
核心代码
首先定义个注解,用来标记哪些方法可以被日志 AOP 接管:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogSwitch {
String value() default ""; // 给开关一个名字,方便控制
}
然后写一个切面类:
@Aspect
@Component
public class LogAspect {
// 用来存开关状态
private final Map<String, Boolean> logSwitchMap = new ConcurrentHashMap<>();
// 提供修改开关的方法
public void setSwitch(String key, boolean enabled) {
logSwitchMap.put(key, enabled);
}
@Around("@annotation(logSwitch)")
public Object around(ProceedingJoinPoint pjp, LogSwitch logSwitch) throws Throwable {
String key = logSwitch.value();
boolean enabled = logSwitchMap.getOrDefault(key, false);
if (enabled) {
long start = System.currentTimeMillis();
System.out.println("【日志开关已打开】调用方法: " + pjp.getSignature());
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.println("方法执行结束,耗时: " + (end - start) + "ms,返回值: " + result);
return result;
} else {
// 开关没开就直接执行
return pjp.proceed();
}
}
}
这里用了@Around环绕通知,如果开关打开就打印日志,否则就安安静静执行。
如何动态开关
再写个 Controller,把开关暴露出来:
@RestController
@RequestMapping("/log")
public class LogSwitchController {
private final LogAspect logAspect;
public LogSwitchController(LogAspect logAspect) {
this.logAspect = logAspect;
}
@PostMapping("/switch")
public String setSwitch(@RequestParam String key, @RequestParam boolean enabled) {
logAspect.setSwitch(key, enabled);
return "开关 " + key + " 已设置为: " + enabled;
}
}
这样只要调用/log/switch?key=test&enabled=true就能实时打开某个日志开关,不用重启应用。
使用示例
假设你在某个 service 方法上加了注解:
@Service
public class UserService {
@LogSwitch("userQuery")
public String getUserById(String id) {
return "用户:" + id;
}
}
一开始开关默认是关的,调用不会打印额外日志;当你通过接口打开userQuery这个开关后,再调用getUserById方法,就能看到详细日志了。
这种方案优点就是轻量级,不用改动 logback 配置,也不用重启。缺点是如果你想控制很细粒度的日志(比如某些第三方库内部),就不太适合,还是得回到日志框架本身去做。