我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

前言

事情的起因是微服务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的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

BaseAdapter导致notifyDataSetChanged()无效的三个原因及解决方法

BaseAdapter导致notifyDataSetChanged()无效的三个原因及解决方法:1. 数据源没有改变:notifyDataSetChanged()只有在数据源改变时才会刷新列表,如果数据源没有改变,调用该方法是无效的。解决方
2023-09-12

粘性定位的失效原因及解决方案深入分析

粘性定位为何会失去效果?探讨原因与解决方案引言:在现代网页设计中,粘性定位(Sticky Positioning)被广泛应用于提升用户的交互体验。它可以使元素在滚动过程中“粘”在页面的某个位置,起到固定的效果。然而,在某些情况下,粘性定位
粘性定位的失效原因及解决方案深入分析
2024-01-29

Discuz通信失败的原因分析及解决方法

《Discuz通信失败的原因分析及解决方法》Discuz作为一个知名的开源论坛系统,在网站建设中被广泛应用。然而,有时候在使用Discuz时会出现通信失败的情况,导致网站无法正常运行。本文将从通信失败的原因分析入手,并提供一些解决方法,同
Discuz通信失败的原因分析及解决方法
2024-03-10

分析Git认证失败的原因及解决办法

前言Git是目前最主流的代码管理和版本控制工具之一,在企业和开源社区广泛使用。在使用 Git 进行代码管理时,可能会遇到 Git 认证失败的问题。本文将分析Git认证失败的原因及可能的解决方法。一、Git认证失败的原因1.凭证缓存失效在使用
2023-10-22

Springboot中yml文件不生效原因分析及解决

Springboot中yml文件不生效常见原因包括:文件路径错误、语法错误、配置文件未加载、属性冲突、环境变量覆盖、注解覆盖等。解决方案涉及检查文件路径、修复语法错误、检查配置文件加载、解决属性冲突、忽略环境变量、检查注解覆盖等。同时,可以利用@ConfigurationProperties、@Value、SpringApplication.setDefaultProperties方法进行配置。若上述方法无效,可使用IDE调试、启用debug日志、检查日志、寻求社区或专业帮助等方式解决问题。
Springboot中yml文件不生效原因分析及解决
2024-04-02

解决windows7系统服务运行失败及原因分析

单击开始菜单,在搜索程序和文件填入:文件夹选项 会弹出文件夹选项,选择图片所示。 重置 应用保存后,问题得到解决。 出现这种情况有几种原因:系统安装错编程客栈误,丢失explohttp://www.cppcns.comrer.exe文
2023-05-30

Android中EditText+Button组合导致输入板无法收起的原因分析及解决办法

在Android开发中,录入信息是最基本的操作,使用非常广泛。但是Android对输入法弹出/收起的支持,并不是很好。对弹出,提供了force方式和implicit方式,对输入却没有提供force方式。可想而知,想弹能弹,想收不能收,这是多
2022-06-06

@ComponentScan在spring中无效的原因分析以及解决方法

这篇文章将为大家详细讲解有关@ComponentScan在spring中无效的原因分析以及解决方法,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。@ComponentScan在spring中无
2023-06-25

阿里云网络服务器搭建失败原因分析及解决方法

阿里云网络服务器搭建过程中,如果遇到失败的情况,可能会让人感到困扰。本文将分析阿里云网络服务器搭建失败的常见原因,并提供相应的解决方法。一、常见原因及解决方法网络问题阿里云网络服务器搭建过程中,网络问题是最常见的问题之一。可能是网络线路问题,也可能是网络设置问题。解决方法如下:检查网络线路是否正常,如果线路有问题
阿里云网络服务器搭建失败原因分析及解决方法
2023-11-20

阿里云服务器发送邮件失败原因分析及解决方法

本文主要针对阿里云服务器发送邮件失败的问题进行深入分析,并提出相应的解决方法。邮件发送是很多企业日常运营的重要环节,而阿里云服务器作为企业级云服务提供商,其稳定性和可靠性备受企业关注。然而,由于各种原因,阿里云服务器可能会出现发送邮件失败的情况。本文将从阿里云服务器的邮件服务、邮件客户端设置、网络环境等方面进行深
阿里云服务器发送邮件失败原因分析及解决方法
2023-12-10

阿里云服务器内容审查失败原因分析及解决策略

随着互联网的飞速发展,网站内容审查成为了网站运营的重要环节。阿里云服务器作为国内领先的云计算服务提供商,其服务器内容审查功能在很多网站中得到了广泛的应用。然而,最近有用户反映阿里云服务器内容审查失败的问题,这究竟是什么原因呢?本文将对此问题进行详细分析,并提供相应的解决策略。一、阿里云服务器内容审查失败的原因分析
阿里云服务器内容审查失败原因分析及解决策略
2023-11-09

手机端登录阿里云服务器失败原因分析及解决方案

本文将详细讨论手机端登录阿里云服务器失败的问题,并分析其原因,同时提出相应的解决方案。在当今数字化时代,越来越多的企业和个人用户开始使用阿里云服务器。然而,随着手机端应用的普及,越来越多的用户开始使用手机端来登录阿里云服务器。然而,也有些用户在手机端登录阿里云服务器时遇到了困难,无法正常登录。那么,手机端登录阿里
手机端登录阿里云服务器失败原因分析及解决方案
2023-10-29

手机打开阿里云服务器失败的原因分析及解决方案

手机无法打开阿里云服务器是一个常见的问题,可能会导致您的业务受到影响。本文将详细分析手机打开阿里云服务器失败的原因,并提供相应的解决方案。一、手机打开阿里云服务器失败的原因网络问题:手机无法连接到互联网,或者网络不稳定,这都可能导致无法打开阿里云服务器。服务器问题:阿里云服务器可能出现故障或者被暂停使用,此时手机
手机打开阿里云服务器失败的原因分析及解决方案
2023-12-15

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录