@Async导致controller 404及失效原因解决分析

目录
  • 前言
  • 排查过程
  • 排查结果分析
    • 1、接口404
    • 2、为何controller里面加了@Asyn异步就失效了
  • 总结

前言

事情的起因是微服务A通过feign调用微服务B的某个接口,报了形如下的异常

feign.FeignException$NotFound: [404] during [GET] to [http://feign-provider/test/async] [AyncTestServiceClient#testAsync()]: [{"timestamp":"2022-05-28T01:16:36.283+0000","status":404,"error":"Not Found","message":"No message available","path":"/test/async"}]

负责微服务A的工程师小张就找到负责提供该接口的工程师小李,问小李是不是改动了接口,小李一脸无辜说他最近没对这个接口做任何改动,不过小李还是说道他排查一下。

排查过程

小李排查的过程如下,他先通过swagger查看他提供给A服务接口是否存在,他一查发现他在swagger上看不到他提供给A服务的接口。于是他怀疑是不是有人动了他的代码,他就去查找最近的git提交记录,发现没人动他的代码,因为项目还没发布,都在测试阶段,他就根据项目集成的git-commit-id-maven-plugin插件定位到测试目前发布具体是哪个版本。(ps:对
git-commit-id-maven-plugin感兴趣的朋友,可以查看之前的文章聊聊如何验证线上的版本是符合预期的版本)。然后他将该版本的代码下到本地进行调试,他发现代码中提供给A的接口还在,target下的class也有提供给A的接口class,但诡异的是swagger就是没显示他提供出去的接口,他一度以为是swagger出了问题,于是他用postman直接请求他提供A的接口,发现报了404。然后他就叫负责同个微服务B的同事小王,也帮忙试一下,发现结果就是404。后面没招,小李就去求助他们项目资深同事小林。

小林的排查思路如下,他先走查一下小李的接口代码,发现他提供的接口实现层的方法上加了一个@Async,示例形如下

@RestController
@RequestMapping(AsyncTestService.INTER_NAME)
public class AsyncTestServiceImpl implements AsyncTestService{
    @GetMapping("async")
    @Override
    public String testAsync() {
        System.out.println("testAsync start....");
        this.doAsynBiz();
        System.out.println("testAsync end....");
        return "hello async";
    }
    @Async
    public void doAsynBiz(){
            System.out.println("doAsynBiz.....");
        }
    }

小林凭多年的经验直觉告诉小李说,应该是@Async引起。小李很斩钉截铁的说不可能啊,他@Async很早就加了,之前接口都可以访问的,小林一看小李说得那么肯定,他也不好打击小李。于是他接下来做了如下操作,先在项目中yml配置如下参数,开启springweb日志

logging:
  level:
    org.springframework.web: trace

然后在项目中加了形如下代码,来跟踪接口bean的类型

for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
            if(beanDefinitionName.toLowerCase().startsWith("AsyncTestService".toLowerCase())){
                System.err.println(beanDefinitionName + "=" + applicationContext.getBean(beanDefinitionName).getClass());
            }
        }

启动控制台,看日志形如下

c.d.f.c.ConfigController:
    {GET /config/test}: test()
09:15:04 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.c.ConfigController:
    {GET /config/test}: test()
2022-05-28 09:15:04.564 TRACE 10120 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.i.UserServiceImpl:
    {GET /user/{id}}: getUserById(Long)
09:15:04 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.i.UserServiceImpl:
    {GET /user/{id}}: getUserById(Long)
2022-05-28 09:15:04.577 TRACE 10120 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    s.d.s.w.ApiResourceController:
    { /swagger-resources/configuration/ui}: uiConfiguration()
    { /swagger-resources}: swaggerResources()
    { /swagger-resources/configuration/security}: securityConfiguration()
09:15:04 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    s.d.s.w.ApiResourceController:
    { /swagger-resources/configuration/ui}: uiConfiguration()
    { /swagger-resources}: swaggerResources()
    { /swagger-resources/configuration/security}: securityConfiguration()
2022-05-28 09:15:04.590 TRACE 10120 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    o.s.b.a.w.s.e.BasicErrorController:
    { /error}: error(HttpServletRequest)
    { /error, produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse)
09:15:04 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    o.s.b.a.w.s.e.BasicErrorController:
    { /error}: error(HttpServletRequest)
    { /error, produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse)

发现确实没打印出相关requestMapping映射信息,这可以说明一点就是小李那个接口没有绑定到springmvc映射,也就是出现404的原因。接着观察控制台打印的bean,内容形如下

asyncTestServiceImpl=class com.sun.proxy.$Proxy127

这很明显这个接口bean已经被jdk动态代理给替换。小李看到控制台打印的信息,若有所思,然后说,我把@Async去掉试下。小李把@Async去掉后,再观察下控制台

2022-05-28 10:09:40.814 TRACE 13028 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
10:09:40 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
2022-05-28 10:09:40.817 TRACE 13028 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.c.ConfigController:
    {GET /config/test}: test()
10:09:40 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.c.ConfigController:
    {GET /config/test}: test()
2022-05-28 10:09:40.820 TRACE 13028 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.i.UserServiceImpl:
    {GET /user/{id}}: getUserById(Long)
asyncTestServiceImpl=class com.demo.feign.controller.AsyncTestServiceImpl

通过控制台可以发现,此时接口已经绑定到springmvc映射,而且打印出bean类型是真实对象bean。小李看到这个现象,也百思不得其解,他说道他之前确实是加了@Async,接口也能正常访问。于是小林就问一句,你确定你加了@Async,异步生效了吗,小李说开启spring异步,不都是加@Async吗。小林又问了一句,你在项目中开启异步,除了加@Async,还有做什么处理吗,小李说没了,他之前在项目使用异步就都是加了@Async,也能用了好好的,小林一听,基本上知道为什么小李之前@Async,接口还能正常访问了,小林为了验证想法,就问同负责该项目的小王,说你最近有加什么异步操作吗,小王说有,小林进一步问,你是怎么做的,小王说,他先加@EnabledAsyn,开启异步,然后在业务逻辑层上的方法上加@Async注解。小李一听,说原来使用@Async还要配合@EnabledAsyn啊,他之前都不知道

接着小李说那在controller是不是就不能使用@Async注解了?,小林说最好是把加@Async的逻辑挪到service层去处理,不过也不是controller就不能使用@Async注解了,接着小林为了验证这个想法,他把原来实现的接口类去掉,形如下

@RestController
@RequestMapping(AsyncTestService.INTER_NAME)
public class AsyncTestServiceImpl{
    @GetMapping("async")
    public String testAsync() {
        System.out.println(Thread.currentThread().toString() + "-----testAsync start....");
        this.doAsynBiz();
        System.out.println(Thread.currentThread().toString() + "-----testAsync end....");
        return "hello async";
    }
    @Async
    public void doAsynBiz(){
            System.out.println(Thread.currentThread().toString() + "-----doAsynBiz.....");
        }
    }

启动后,查看控制台

2022-05-28 10:41:31.624 TRACE 5068 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
10:41:31 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
2022-05-28 10:41:31.627 TRACE 5068 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.c.ConfigController:
    {GET /config/test}: test()
10:41:31 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -

此时bean的类型如下

asyncTestServiceImpl=class com.demo.feign.controller.AsyncTestServiceImpl$$EnhancerBySpringCGLIB$$a285a21c

访问接口,打印内容如下

Thread[http-nio-8080-exec-1,5,main]-----testAsync start....
Thread[http-nio-8080-exec-1,5,main]-----doAsynBiz.....
Thread[http-nio-8080-exec-1,5,main]-----testAsync end....

从控制台可以发现,都是http-nio-8080-exec-1线程触发,说明异步没生效,即@Async失效。后面对controller做了如下改造

@RestController
@RequestMapping(AsyncTestService.INTER_NAME)
public class AsyncTestServiceImpl{
    @Autowired
    private ObjectProvider<AsyncTestServiceImpl> asyncTestServices;
    @GetMapping("async")
    public String testAsync() {
        System.out.println(Thread.currentThread().toString() + "-----testAsync start....");
        asyncTestServices.getIfAvailable().doAsynBiz();
        System.out.println(Thread.currentThread().toString() + "-----testAsync end....");
        return "hello async";
    }
    @Async
    public void doAsynBiz(){
            System.out.println(Thread.currentThread().toString() + "-----doAsynBiz.....");
        }
    }

访问接口,打印内容如下

Thread[http-nio-8080-exec-2,5,main]-----testAsync start....
Thread[http-nio-8080-exec-2,5,main]-----testAsync end....
Thread[task-1,5,main]-----doAsynBiz.....

这说明在controller其实也是可以用@Async,只是要额外做处理。所以建议是把@Async从controller中抽离出去,在新类中进行处理,示例如下

@Service
public class AysncService {
    @Async
    public void doAsynBiz(){
        System.out.println(Thread.currentThread().getName() + "-----doAsynBiz.....");
    }
}
@RestController
@RequestMapping(AsyncTestService.INTER_NAME)
@RequiredArgsConstructor
public class AsyncTestServiceImpl implements AsyncTestService {
    private final AysncService aysncService;
    @Override
    public String testAsync() {
        System.out.println(Thread.currentThread().getName() + "-----testAsync start....");
        aysncService.doAsynBiz();
        System.out.println(Thread.currentThread().getName() + "-----testAsync end....");
        return "hello async";
    }
}

访问接口,打印内容

http-nio-8080-exec-1-----testAsync start....
http-nio-8080-exec-1-----testAsync end....
task-1-----doAsynBiz.....

说明异步生效

排查结果分析

1、接口404

从mvc日志

2022-05-28 10:59:50.394 TRACE 14152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
10:59:50 [main] TRACE o.s.w.s.m.m.a.RequestMappingHandlerMapping -
    c.d.f.c.AsyncTestServiceImpl:
    {GET /test/async}: testAsync()
2022-05-28 10:59:50.397 TRACE 14152 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping :

我们可以知道,controller映射处理是在RequestMappingHandlerMapping 这个类中,但具体是哪个方法进行处理呢,我们可以通过日志打印的信息,进行倒推,也可以基于spring的特性加断点调试,比如通过afterPropertiesSet这一启动扩展点调试起,就会发现RequestMappingHandlerMapping的映射处理是在

protected void initHandlerMethods() {
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                processCandidateBean(beanName);
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }

进行处理,具体是通过processCandidateBean进行处理

protected void processCandidateBean(String beanName) {
        Class<?> beanType = null;
        try {
            beanType = obtainApplicationContext().getType(beanName);
        }
        catch (Throwable ex) {
            // An unresolvable bean type, probably from a lazy bean - let's ignore it.
            if (logger.isTraceEnabled()) {
                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
            }
        }
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }

最终是通过detectHandlerMethods进行处理

protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        try {
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            if (logger.isTraceEnabled()) {
                logger.trace(formatMappings(userType, methods));
            }
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }

这个里面就是做了实际注册。而执行detectHandlerMethods的前提是

beanType != null && isHandler(beanType)
@Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }

即只有加了@Controller或者@RequestMapping的类会进行处理,而@RestController为啥也处理,点击
@RestController发现

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

他本质就是@Controller。但我们通过反射查找注解,正常只会查找一层,比如

AsynTestController.class.getAnnotation(RestController.class)

他找到@RestController这一层,而不会找继续再找@RestController里面的@Controller,而AnnotatedElementUtils.hasAnnotation,这个注解方法就不一样,他是可以找到合并注解,即使是使用
@RestController,他还会继续找到里面的@Controller。因此这个方法对于找复合型注解很有用

当我们使用jdk动态代理时,因为父类上没加@Controller或者@RequestMapping,因此他不会被mvc进行映射处理,导致404。而使用cglib时,因为他是作为子类继承了目标类,因此他会继承目标类上的注解,因此当为cglib代理时,他会正常被mvc进行映射处理

2、为何controller里面加了@Asyn异步就失效了

这是因为加了@Async后,controller变成代理了,而当要异步处理方法,用this时,他使用的是目标对象,而非代理对象。这跟现在面试事务为啥事务失效的八股文基本是一个套路

总结

本文主要讲@Async导致controller 404,同时也使@Async失效的原因。解决的推荐方法就是将@Async抽离出controller,新建一个service类进行处理,更多关于@Async导致controller 404的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java Spring注解之@Async的基本用法和示例

    目录 背景 异步调用 @Async介绍 在Spring中启用@Async 示例一:基本使用方式 示例二:在同一个类中调用异步方法 示例三:异步方法是static方法 示例四:在方法级别上修改默认的执行器 补充:Java中异步注解@Async的陷阱 总结 背景 通常,在Java中的方法调用都是同步调用,比如在A方法中调用了B方法,则在A调用B方法之后,必须等待B方法执行并返回后,A方法才可以继续往下执行.这样容易出现的一个问题就是如果B方法执行时间较长,则可能会导致调用A的请求响应迟缓,为了解决这

  • 深度解析SpringBoot中@Async引起的循环依赖

    目录 事故时间线 猜想 什么是循环依赖 什么是@Async 啊,昨晚发版又出现了让有头大的循环依赖问题,按理说Spring会为我们解决循环依赖,但是为什么还会出现这个问题呢?为什么在本地.UAT以及PRE环境都没有出现这个问题,但是到了PROD环境就出现了这个问题呢?本文将从事故时间线.及时止损.复盘分析等几个方面为大家带来详细的分析,干货满满! 事故时间线 本着"先止损.后复盘分析"的原则,我们来看一下这次发版事故的时间线. 2021年11月16日晚23点00分00秒开始发版,此时集

  • 关于SpringBoot使用@Async的总结

    目录 SpringBoot使用@Async总结 如下方式会使@Async失效 SpringBoot实现异步(Async)接口 1. 启动类引入@EnableAsync注解 2. 建立异步任务类 3. 建立测试接口 SpringBoot使用@Async总结 注意事项: 如下方式会使@Async失效 1. 异步方法使用static修饰 2. 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类 3. 异步方法不能与异步方法在同一个类中 4. 类中需要使用@Autowi

  • java SpringBoot注解@Async不生效的解决方法

    目录 问题描述: 解决方案: 总结: SpringBoot 注解@Async不生效的解决方法 问题描述: 这里虽然加了@EnableAsync和@Async,但是异步请求依然没有生效 解决方案: 方法一: 同一个类中调用需要先获取代理对象,也就是手动获取对象 @Service @EnableAsync public class DemoService { public void add(){ DemoService bean = SpringUtil.getBean(DemoService.cl

  • @Async导致controller 404及失效原因解决分析

    目录 前言 排查过程 排查结果分析 1.接口404 2.为何controller里面加了@Asyn异步就失效了 总结 前言 事情的起因是微服务A通过feign调用微服务B的某个接口,报了形如下的异常 feign.FeignException$NotFound: [404] during [GET] to [http://feign-provider/test/async] [AyncTestServiceClient#testAsync()]: [{"timestamp":"

  • Spring处理@Async导致的循环依赖失败问题的方案详解

    目录 简介 问题复现 原因分析 解决方案 方案1:懒加载 方案2:不让@Async的类有循环依赖 方案3:allowRawInjectionDespiteWrapping设置为true 为什么@Transactional不会导致失败 简介 说明 本文介绍SpringBoot中的@Async导致循环依赖失败的原因及其解决方案. 概述 我们知道,Spring解决了循环依赖问题,但Spring的异步(@Async)会使得循环依赖失败.本文将用实例来介绍其原因和解决方案. 问题复现 启动类 启动类添加@

  • SpringBoot使用Async注解失效原因分析及解决(spring异步回调)

    目录 Async注解失效原因分析及解决(spring异步回调) Spring中@Async 有时候在使用的过程中@Async注解会失效 解决方式一 解决方式二 springboot @Async 失效可能原因 Async注解失效原因分析及解决(spring异步回调) Spring中@Async 在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的:但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在spring 3.x之后,就已

  • jQuery Ajax async=>false异步改为同步时,解决导致浏览器假死的问题

    今天做一个需求遇到了这么个情况,就是用户个人中心有个功能,点击按钮,可以刷新用户当前的积分,这个肯定需要使用到ajax的同步请求了,当时喀喀喀三下五除二写玩了,大概代码如下: /** * 异步当前用户积分 by zgw 20161216 * @return {[type]} [description] */ function flushIntegralSum() { //点击按钮刷新前修改按钮的文案,已经去掉点击事情,防止多次点击 $("#flushbutton").replaceWi

  • php多次include后导致全局变量global失效的解决方法

    本文实例讲述了php多次include后导致全局变量global失效的解决方法.分享给大家供大家参考.具体分析如下: 在多个文件中,文件一个接一个include,但最后一个文件里的函数使用global后却无法引用全局变量.例如: a.php文件: <?php $aa = 1; ?> b.php文件: <?php include a.php function show(){ global $aa; var_dump($aa); } ?> 显示:null; 这种失效是由于多种原因造成的

  • 解决mybatis分页插件PageHelper导致自定义拦截器失效

    目录 问题背景 mybatis拦截器使用 使用方法: 注解参数介绍: setProperties方法 bug内容: 自定义拦截器部分代码 PageInterceptor源码: 解决方法: 解决方案一 调整执行顺序 解决方案二 修改拦截器注解定义 问题背景 在最近的项目开发中遇到一个需求 需要对mysql做一些慢查询.大结果集等异常指标进行收集监控,从运维角度并没有对mysql进行统一的指标搜集,所以需要通过代码层面对指标进行收集,我采用的方法是通过mybatis的Interceptor拦截器进行

  • 当master down掉后,pt-heartbeat不断重试会导致内存缓慢增长的原因及解决办法

    最近同事反映,在使用pt-heartbeat监控主从复制延迟的过程中,如果master down掉了,则pt-heartbeat则会连接失败,但会不断重试. 重试本无可厚非,毕竟从使用者的角度来说,希望pt-heartbeat能不断重试,直到重新连接上数据库.但是,他们发现,不断的重试会带来内存的缓慢增长. 重现 环境: pt-heartbeat v2.2.19,MySQL社区版 v5.6.31,Perl v5.10.1,RHEL 6.7,内存500M 为了避免数据库启停对pt-heartbea

  • 导致MyEclipse内存不足的原因分析及解决办法

    1.修改eclipse.ini 在Myeclipse安装目录下G:\MyEclipse8.5\Genuitec\MyEclipse 8.5有一个myeclipse.ini配置文件,设置如下: -vmargs -Xmx512m -XX:MaxPermSize=256m -XX:ReservedCodeCacheSize=64m 2.设置Default VM Arguments 在myEclipse中,打开Windows-> Preferences->Java->Installed JREs

  • Kotlin遍历集合导致并发修改异常的原因和解决方法

    各位android 老司机们,对于android 遍历结合的时候,发生并发修改异常一定毫不陌生: 之前看到过一篇文章, 在阿里巴巴Java开发手册中,有这样一条规定: 其实,增强for循环也是Java给我们提供的一个语法糖,如果将以上代码编译后的class文件进行反编译(使用jad工具)的话,可以得到以下代码: 1.原因:(其实我都不想在各位老司机面前再赘述这个了.-_-||) 这个异常产生的原因是,迭代器依赖于集合而存在,在判断成功后,集合中添加了新的元素,而迭代器并不知道,所有就报错了.其实

  • MySQL隐式类型转换导致索引失效的解决

    目录 问题 复现 隐式转换 总结 参考 问题 在工作中发现,有一个接口只执行一条SQL查询语句,并且SQL明明使用了主键列,但是速度很慢. 在MySQL中EXPLAINN后发现,执行时并没有使用主键索引,而是进行了全表扫描. 复现 数据表DDL如下,使用 user_id 作为主键索引: CREATE TABLE `user_message` ( `user_id` varchar(50) NOT NULL COMMENT '用户ID', `msg_id` int(11) NOT NULL COM

随机推荐