最近在开发黑马点评项目时,遇到了一个令人头疼的P80空指针异常。这个异常出现在用户提交评论时,系统突然抛出NullPointerException,导致整个评论功能瘫痪。作为一个高频使用功能,这个问题直接影响用户体验和业务指标。
通过日志分析,我们发现异常堆栈指向CommentServiceImpl的第80行代码。具体报错信息显示是在处理评论点赞数统计时,获取到的用户DTO对象为null。有趣的是,这个问题并非每次都会出现,而是在特定用户操作序列下才会复现。
让我们先看看出问题的代码段:
java复制public Result queryCommentLikes(Long commentId) {
// P80行代码
UserDTO user = UserHolder.getUser();
if (user == null) {
return Result.fail("用户未登录");
}
// 后续统计逻辑...
}
问题根源在于UserHolder的实现方式。这个工具类使用了ThreadLocal存储用户信息:
java复制public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
在异步场景下,当请求经过线程池处理后,原始线程的用户信息不会自动传递到新线程。如果统计任务被提交到线程池执行,就会因为线程切换导致UserHolder中的用户信息丢失。
我们考虑了三种解决方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 同步执行 | 实现简单 | 影响性能 | 低并发系统 |
| 参数传递 | 线程安全 | 代码侵入性强 | 简单异步场景 |
| 上下文传递 | 解耦性好 | 实现复杂 | 复杂异步系统 |
最终选择上下文传递方案,因为:
java复制public class RequestContext {
public static final String ATTRIBUTE = "request_context";
private final UserDTO user;
// 构造方法、getter省略...
}
java复制@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
UserDTO user = ... // 原有认证逻辑
request.setAttribute(ATTRIBUTE, new RequestContext(user));
return true;
}
java复制public class ContextAwareExecutor extends ThreadPoolTaskExecutor {
@Override
public <T> Future<T> submit(Callable<T> task) {
RequestContext context = getCurrentContext();
return super.submit(() -> {
try {
if (context != null) {
RequestHolder.setContext(context);
}
return task.call();
} finally {
RequestHolder.clear();
}
});
}
}
xml复制<bean id="taskExecutor" class="com.hmdp.config.ContextAwareExecutor">
<!-- 原有配置保持不变 -->
</bean>
java复制public Result queryCommentLikes(Long commentId) {
UserDTO user = RequestHolder.getUser();
if (user == null) {
return Result.fail("用户未登录");
}
// 后续逻辑不变
}
java复制@Test
public void testAsyncCommentLike() throws Exception {
// 模拟异步场景
Future<Result> future = taskExecutor.submit(() ->
commentService.queryCommentLikes(1L));
Result result = future.get();
assertFalse(result.isSuccess());
assertEquals("用户未登录", result.getMessage());
}
在这次问题排查中,我们遇到了几个典型问题:
java复制// 好的实践
public void process(UserDTO user) {
Objects.requireNonNull(user, "用户信息不能为空");
// ...
}
// 更好的实践
public void process(@NonNull UserDTO user) {
// ...
}
这个问题引发我们对系统架构的重新思考。后续可以考虑:
特别是在微服务架构下,上下文传递变得更加重要。我们可以借鉴OpenTelemetry的标准,建立统一的上下文传递机制。