@Async導(dǎo)致controller?404及失效原因解決分析
前言
事情的起因是微服務(wù)A通過feign調(diào)用微服務(wù)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"}]
負責微服務(wù)A的工程師小張就找到負責提供該接口的工程師小李,問小李是不是改動了接口,小李一臉無辜說他最近沒對這個接口做任何改動,不過小李還是說道他排查一下。
排查過程
小李排查的過程如下,他先通過swagger查看他提供給A服務(wù)接口是否存在,他一查發(fā)現(xiàn)他在swagger上看不到他提供給A服務(wù)的接口。于是他懷疑是不是有人動了他的代碼,他就去查找最近的git提交記錄,發(fā)現(xiàn)沒人動他的代碼,因為項目還沒發(fā)布,都在測試階段,他就根據(jù)項目集成的git-commit-id-maven-plugin插件定位到測試目前發(fā)布具體是哪個版本。(ps:對
git-commit-id-maven-plugin感興趣的朋友,可以查看之前的文章聊聊如何驗證線上的版本是符合預(yù)期的版本)。然后他將該版本的代碼下到本地進行調(diào)試,他發(fā)現(xiàn)代碼中提供給A的接口還在,target下的class也有提供給A的接口class,但詭異的是swagger就是沒顯示他提供出去的接口,他一度以為是swagger出了問題,于是他用postman直接請求他提供A的接口,發(fā)現(xiàn)報了404。然后他就叫負責同個微服務(wù)B的同事小王,也幫忙試一下,發(fā)現(xiàn)結(jié)果就是404。后面沒招,小李就去求助他們項目資深同事小林。
小林的排查思路如下,他先走查一下小李的接口代碼,發(fā)現(xiàn)他提供的接口實現(xiàn)層的方法上加了一個@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....."); } }
小林憑多年的經(jīng)驗直覺告訴小李說,應(yīng)該是@Async引起。小李很斬釘截鐵的說不可能啊,他@Async很早就加了,之前接口都可以訪問的,小林一看小李說得那么肯定,他也不好打擊小李。于是他接下來做了如下操作,先在項目中yml配置如下參數(shù),開啟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)
發(fā)現(xiàn)確實沒打印出相關(guān)requestMapping映射信息,這可以說明一點就是小李那個接口沒有綁定到springmvc映射,也就是出現(xiàn)404的原因。接著觀察控制臺打印的bean,內(nèi)容形如下
asyncTestServiceImpl=class com.sun.proxy.$Proxy127
這很明顯這個接口bean已經(jīng)被jdk動態(tài)代理給替換。小李看到控制臺打印的信息,若有所思,然后說,我把@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
通過控制臺可以發(fā)現(xiàn),此時接口已經(jīng)綁定到springmvc映射,而且打印出bean類型是真實對象bean。小李看到這個現(xiàn)象,也百思不得其解,他說道他之前確實是加了@Async,接口也能正常訪問。于是小林就問一句,你確定你加了@Async,異步生效了嗎,小李說開啟spring異步,不都是加@Async嗎。小林又問了一句,你在項目中開啟異步,除了加@Async,還有做什么處理嗎,小李說沒了,他之前在項目使用異步就都是加了@Async,也能用了好好的,小林一聽,基本上知道為什么小李之前@Async,接口還能正常訪問了,小林為了驗證想法,就問同負責該項目的小王,說你最近有加什么異步操作嗎,小王說有,小林進一步問,你是怎么做的,小王說,他先加@EnabledAsyn,開啟異步,然后在業(yè)務(wù)邏輯層上的方法上加@Async注解。小李一聽,說原來使用@Async還要配合@EnabledAsyn啊,他之前都不知道
接著小李說那在controller是不是就不能使用@Async注解了?,小林說最好是把加@Async的邏輯挪到service層去處理,不過也不是controller就不能使用@Async注解了,接著小林為了驗證這個想法,他把原來實現(xiàn)的接口類去掉,形如下
@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
訪問接口,打印內(nèi)容如下
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....
從控制臺可以發(fā)現(xiàn),都是http-nio-8080-exec-1線程觸發(fā),說明異步?jīng)]生效,即@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....."); } }
訪問接口,打印內(nèi)容如下
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"; } }
訪問接口,打印內(nèi)容
http-nio-8080-exec-1-----testAsync start....
http-nio-8080-exec-1-----testAsync end....
task-1-----doAsynBiz.....
說明異步生效
排查結(jié)果分析
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的特性加斷點調(diào)試,比如通過afterPropertiesSet這一啟動擴展點調(diào)試起,就會發(fā)現(xiàn)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); }); } }
這個里面就是做了實際注冊。而執(zhí)行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發(fā)現(xiàn)
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController {
他本質(zhì)就是@Controller。但我們通過反射查找注解,正常只會查找一層,比如
AsynTestController.class.getAnnotation(RestController.class)
他找到@RestController這一層,而不會找繼續(xù)再找@RestController里面的@Controller,而AnnotatedElementUtils.hasAnnotation,這個注解方法就不一樣,他是可以找到合并注解,即使是使用
@RestController,他還會繼續(xù)找到里面的@Controller。因此這個方法對于找復(fù)合型注解很有用
當我們使用jdk動態(tài)代理時,因為父類上沒加@Controller或者@RequestMapping,因此他不會被mvc進行映射處理,導(dǎo)致404。而使用cglib時,因為他是作為子類繼承了目標類,因此他會繼承目標類上的注解,因此當為cglib代理時,他會正常被mvc進行映射處理
2、為何controller里面加了@Asyn異步就失效了
這是因為加了@Async后,controller變成代理了,而當要異步處理方法,用this時,他使用的是目標對象,而非代理對象。這跟現(xiàn)在面試事務(wù)為啥事務(wù)失效的八股文基本是一個套路
總結(jié)
本文主要講@Async導(dǎo)致controller 404,同時也使@Async失效的原因。解決的推薦方法就是將@Async抽離出controller,新建一個service類進行處理,更多關(guān)于@Async導(dǎo)致controller 404的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
帶你了解Java數(shù)據(jù)結(jié)構(gòu)和算法之棧
這篇文章主要為大家介紹了Java數(shù)據(jù)結(jié)構(gòu)和算法之棧 ,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-01-01利用Java異常機制實現(xiàn)模擬借書系統(tǒng)
這篇文章主要給大家介紹了利用Java異常機制實現(xiàn)模擬借書系統(tǒng)的相關(guān)資料,文中先對java異常機制進行了簡單介紹,而后通過示例代碼介紹了java語言是如何實現(xiàn)一個控制臺版的模擬借書系統(tǒng),需要的朋友可以參考學習,一起來看看吧。2017-04-04SpringBoot生成License的實現(xiàn)示例
License指的是版權(quán)許可證,那么對于SpringBoot項目,如何增加License呢?本文就來介紹一下,感興趣的可以了解一下2021-06-06Java單元測試Powermockito和Mockito使用總結(jié)
公司單元測試框架選用了Junit 4.12,Mock框架選用了Mockito和PowerMock,本文主要介紹了Java單元測試Powermockito和Mockito使用總結(jié),感興趣的可以了解一下2021-09-09