使用Feign出现空指针异常
  TEZNKK3IfmPf 14天前 16 0

说明:本文记录一次偶然出现的空指针异常,在微服务架构中,一个服务在调用另一个服务时,出现了空指针异常。

业务描述:在做订单超时功能时,大家都知道,可以使用RabbitMQ延迟队列,下单的同时给队列发送一个延迟消息(消息的内容是订单号),比如延迟10分钟。10分钟之后,该消息被消费者监听到,会根据该订单ID查询数据库,看该订单的状态是否为已支付,是则忽略,否则取消该订单,恢复商品库存等等其他操作,然而此时出现了空指针异常,消息未被消费,被路由到死信队列中。

(微服务调用报空指针异常)

使用Feign出现空指针异常

(消息被路由到死信队列)

使用Feign出现空指针异常

如下图的第三步:

使用Feign出现空指针异常

分析

首先排除FeignClient的问题,因为下单减少库存,取消订单恢复库存,我使用的是同一个接口,只是修改了商品数量的正负数,不可能出现下单时可以,取消订单时再使用就报错的情况。

(controller层代码)

    /** * 根据ID更新商品库存 * @param id * @param num */
    @PutMapping("/update/{id}/{num}")
    public void updateStockById(@PathVariable("id") Long id, @PathVariable("num") Integer num){
     
       
        itemService.updateStockById(id,num);
    }

(service层代码)

    @Override
    public void updateStockById(Long id, Integer num) {
     
       
        if (!ObjectUtil.isAllNotEmpty(id, num)) {
     
       
            System.out.println("参数不能为空");
        }

        if (id < 0 || num < 0) {
     
       
            System.out.println("参数非法");
        }

        update().setSql("stock = stock + " + num).eq("id", id).update();
    }

其次,再思考会不会不是因为Feign的调用报错,而是微服务之间有什业务产生的报错。于是,我找到了拦截器

为了保证用户登录后,经过Gateway(网关)后,信息可以被下游服务获取到,我的代码中是使用MVC拦截器+Feign拦截器实现的,如下图:

使用Feign出现空指针异常

每个服务会有两个拦截器,分别把服务接收到的请求,发出的请求拦截到,然后分别解析用户信息,添加用户信息到请求头,以此达到参数透传,用户信息可在微服务之间流传。

MVC拦截器代码(获取请求头中用户的ID,存到ThreadLocal中)

public class AuthorizationInterceptor implements HandlerInterceptor {
     
       
    /** * 收到请求会执行的方法 * @param request * @param response * @param handler * @return * @throws Exception */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     
       
        String id = request.getHeader("authorization");

        if (id != null && id != ""){
     
       
            long l = Long.parseLong(id);
            TokenThreadLocal.set(l);
        }else {
     
       
            responseHandler(response);
            return false;
        }

        // 放行
        return true;
    }
    ……
}

Feign拦截器(将本服务中的ThreadLocal中的用户ID再设置到请求头上)

/** * 发送请求拦截器 */
@Slf4j
public class AuthorizationRequestInterceptor implements RequestInterceptor {
     
       

    @Override
    public void apply(RequestTemplate requestTemplate) {
     
       
        requestTemplate.header("authorization",TokenThreadLocal.get().toString());
    }
}

排查

给这两个地方分别打上断点,等订单超时后进入拦截器的代码,排查一下;

断点来到Feign拦截器,选中这行代码一看,原来是这里报了空指针异常

使用Feign出现空指针异常

再一看,原来是TokenThreadLocal.get().toString()这里是空的;

使用Feign出现空指针异常

然后恍然大悟,MQ发送消息是异步请求,ThreadLocal本地线程池对象,自然为空;

解决

很自然的想到一种很简单的解决方法,发送消息的时候把ThreadLocal中的值(用户ID)也给发到延迟队列中,然后在消费者监听的代码里面,再使用ThreadLocal的set()方法,把用户ID设置到线程池中;

把订单ID、用户ID封装成一个Map,转为json格式发送到延迟队列里;

使用Feign出现空指针异常

消费者代码这边,使用ThreadLocal的set()方法,把用户ID再设置进去;

使用Feign出现空指针异常

启动,测试下单,等待订单超时,清理超时订单,进入断点,问题解决!

使用Feign出现空指针异常

总结

这是一个非常隐蔽的异常,因为设置了死信队列,未被成功消费的消息会被路由到死信队列中,程序并不会报错,并且因为订单表的内容大部分是在订单服务中,此异常仅仅会影响订单被取消后,调用商品服务恢复商品库存数量这一个很小的功能未能执行,要排除出来是非常困难的。

而问题原因,概括来说,是因为ThreadLocal的值不能在RabbitMQ的消息中传递,导致在使用拦截器获取ThreadLocal值的时候报了空指针异常

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 14天前 0

暂无评论

推荐阅读
TEZNKK3IfmPf