当前位置: 首页 > 科技观察

SpringBoot异步请求和异步调用,一篇搞定!

时间:2023-03-12 02:58:22 科技观察

一、SpringBoot中异步请求的使用1、异步请求和同步请求的特点:可以先释放容器分配给请求的线程和相关资源,减轻系统负担,释放容器分配的线程的请求。Response会被Delayed,可以在耗时处理完成(比如长操作)时再响应客户端。一句话:增加服务端对客户端请求的吞吐量(实际上我们在生产中用的比较少,如果并发请求量大的话,我们会使用nginx将请求加载到集群服务的各个节点来分担请求压力,当然你也可以通过消息队列缓冲请求)。2、异步请求的实现方法一:实现异步请求的Servlet方法@RequestMapping(value="/email/servletReq",method=GET)publicvoidservletReq(HttpServletRequestrequest,HttpServletResponseresponse){AsyncContextasyncContext=request.startAsync();//设置监听器:可以设置其启动、完成、异常、超时等事件的回调处理asyncContext.addListener(newAsyncListener(){@OverridepublicvoidonTimeout(AsyncEventevent)throwsIOException{System.out.println("Timedout.....发生错误:"+event.getThrowable());}@OverridepublicvoidonComplete(AsyncEventevent)throwsIOException{System.out.println("Executioncomplete");//这里可以做一些清理资源的操作...}});//设置超时asyncContext.setTimeout(20000);asyncContext.start(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(10000);System.out.println("内部线程:"+Thread.currentThread().getName());异步上下文.getResponse().setCharacterEncoding("utf-8");asyncContext.getResponse().setContentType("text/html;charset=UTF-8");asyncContext.getResponse().getWriter().println("这是异步请求return");}catch(Exceptione){System.out.println("Exception:"+e);}//异步请求完成通知//此时整个请求完成asyncContext.complete();}});//此时请求的线程连接已经释放System.out.println("主线程:"+Thread.currentThread().getName());}方法二:很简单使用和直接返回参数只是包裹了一层callable,可以继承WebMvcConfigurerAdapter类设置默认线程池和超时处理@RequestMapping(value="/email/callableReq",method=GET)@ResponseBodypublicCallablecallableReq(){System.out.println("外线程:"+Thread.currentThread().getName());returnnewCallable(){@OverridepublicStringcall()throwsException{Thread.sleep(10000);System.out.println("内部线程:"+Thread.currentThread().getName());返回“可调用!”;}};}@ConfigurationpublicclassRequestAsyncPoolConfigextendsWebMvcConfigurerAdapter{@ResourceprivateThreadPoolTask??ExecutormyThreadPoolTask??Executor;@OverridepublicvoidconfigureAsyncSupport(finalAsyncSupportConfigurerconfigurer){//处理callable超时configurer.setDefaultTimeout(60*1000);configurer.setTaskExecutor(myThreadPoolTask??Executor);configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());}@BeanpublicTimeoutCallableProcessingInterceptortimeoutCallableProcessingInterceptor(){returnnewTimeoutCallableProcessingInterceptor();}}方法三:与方法二类似,在Callable外包层,为WebAsyncTask设置超时回调,实现超时处理@RequestMapping(value="/email/webAsyncReq",method=GET)@ResponseBodypublicWebAsyncTaskwebAsyncReq(){System.out.println("外线程:"+Thread.currentThread().getName());Callableresult=()->{System.out.println("内部线程开始:"+Thread.currentThread().getName());try{TimeUnit.SECONDS.sleep(4);}catch(Exceptione){//TODO:handleexception}logger.info("副线程返回");System.out.println("内部线程返回:"+Thread.currentThread().getName());返回“成功”;};WebAsyncTaskwat=newWebAsyncTask(3000L,result);wat.onTimeout(newCallable(){@OverridepublicStringcall()throwsException{//TODOAuto-generatedmethodstubreturn"timeout";}});returnwat;}方法四:DeferredResult可以处理一些比较复杂的业务逻辑,最重要的是在另一个线程中进行业务处理和返回,可以在两个完整的通信中完成无关线程之间@RequestMapping(value="/email/deferredResultReq",method=GET)@ResponseBodypublicDeferredResultdeferredResultReq(){System.out.println("外线程:"+Thread.currentThread().getName());//设置超时时间DeferredResultresult=newDeferredResult(60*1000L);//使用委托机制处理超时事件result.onTimeout(newRunnable(){@Overridepublicvoidrun(){System.out.println("DeferredResulttimeout");result.setResult("超时!");}});result.onCompletion(newRunnable(){@Overridepublicvoidrun(){//完成后System.out.println("调用完成");}});myThreadPoolTask??Executor.execute(newRunnable(){@Overridepublicvoidrun(){//处理业务逻辑System.out.println("内部线程:"+Thread.currentThread().getName());//返回结果result.setResult("DeferredResult!!");}});returnresult;}二、SpringBoot中异步调用的使用1.介绍异步请求的处理。除了异步请求,一般我们用的比较多的是异步调用。通常在开发过程中,会遇到与实际业务无关、没有亲近性的方法。比如记录日志信息等服务。这时候就正常起一个新线程做一些业务处理,让主线程异步执行其他业务。2、使用方法(基于spring)需要在启动类中添加@EnableAsync,使异步调用@Async注解生效。在需要异步执行的方法上加上这个注解。@Async("threadPool"),threadPool是一个自定义的线程池。代码省略。..就两个标签,大家可以自己试试3.注意事项默认情况下,在没有设置TaskExecutor的情况下,默认使用线程池SimpleAsyncTaskExecutor,但是这个线程并不是真正意义上的线程池,因为没有复用线程。每次调用都会创建一个新线程。从控制台日志输出可以看出,每次输出线程名称都会递增。所以我们最好定义一个线程池。调用的异步方法不能是同一个类(包括同一个类的内部类)的方法。简单的说,因为Spring在开始扫描的时候会为它创建一个代理类,调用同一个类的时候,还是调用自己的代理类,所以和正常调用一样。@Cache等其他注解也是如此。说白了就是Spring的代理机制导致的。因此,在开发中,最好将异步服务分离成一个类进行管理。下面将重点介绍。.4、@Async异步方法在什么情况下会失败?调用同一个类有@Async异步方法:在spring中,@Async、@Transactional、cache等注解使用动态代理。其实在Spring容器初始化的时候,Spring容器会把AOP注解的类对象“替换”成代理对象(简单理解),那么注解失效的原因就很明显了,因为方法是由对象本身而不是代理对象调用的,因为不经过Spring容器,那么解决方案就按照这个思路解决。static(静态)方法被调用,(private)私有方法被调用。5、4中第1题的解法(其他2、3题可以自己关注)。将要异步执行的方法单独提取到一个类中。其原理是,当你把异步方法抽离到一个单独的类中时,这个类必须由Spring来管理,其他的Spring组件在需要调用的时候肯定会被注入进去。这个时候代理类才真正注入。实际上,我们的注入对象是从Spring容器中赋值给当前Spring组件的成员变量。由于有些类使用了AOP注解,所以真正存在于Spring容器中的是它的代理对象。然后我们就可以通过上下文获取自己的代理对象来调用异步方法了。@Controller@RequestMapping("/app")publicclassEmailController{//获取ApplicationContext对象的方式有很多种,这个是最简单的,其他的,可以了解一下@AutowiredprivateApplicationContextapplicationContext;@RequestMapping(value="/email/asyncCall",method=GET)@ResponseBodypublicMapasyncCall(){MapresMap=newHashMap();试试{//这种方式调用同类别下的异步方法是不行的//this.testAsyncTask();//通过上下文获取自己的代理对象调用异步方法EmailControllerremailController=(EmailController)applicationContext.getBean(EmailController.class);emailController.testAsyncTask();resMap.put("code",200);}catch(Exceptione){resMap.put("code",400);logger.error("error!",e);}returnresMap;}//注意必须是public非静态方法@AsyncpublicvoidtestAsyncTask()throwsInterruptedException{Thread.sleep(10000);System.out.println("异步任务执行完成!");}}开启cglib代理,手动获取Spring代理类,调用同类别下的异步方法。首先,在启动类中添加@EnableAspectJAutoProxy(exposeProxy=true)注解。代码实现如下:@Service@Transactional(value="transactionManager",readOnly=false,propagation=Propagation.REQUIRED,rollbackFor=Throwable.class)publicclassEmailService{@AutowiredprivateApplicationContextapplicationContext;@AsyncpublicvoidtestSyncTask()throwsInterruptedException{Thread.sleep(10000);System.out.println("异步任务执行完成!");}publicvoidasyncCallTwo()throwsInterruptedException{//this.testSyncTask();//EmailServiceemailService=(EmailService)applicationContext.getBean(EmailService.class);//emailService.测试同步任务();booleanisAop=AopUtils.isAopProxy(EmailController.class);//是否为代理对象;booleanisCglib=AopUtils.isCglibProxy(EmailController.class);//是否为CGLIB代理对象;booleanisJdk=AopUtils.isJdkDynamicProxy(EmailController.class);//是否是JDK动态代理方法的代理对象;//下面是重点!!!EmailServiceemailService=(EmailService)applicationContext.getBean(EmailService.class);EmailServiceproxy=(EmailService)AopContext.currentProxy();System.out.println(emailService==proxy?true:false);proxy.testSyncTask();System.out.println("end!!!");}}3、异步请求和异步调用的区别。其带来的压力可以提高请求的吞吐量;而异步调用用于做一些非主线流程,不需要实时计算和响应的任务,比如同步日志到Kafka进行日志分析等异步请求会一直等待响应,对应的结果需要是返回给客户;对于异步调用,我们往往会立即将响应返回给客户端,完成整个请求。至于异步调用的任务,只是在后台慢慢运行,客户端不会关心。4.小结异步请求和异步调用的使用在这里基本相同。如果有什么问题,希望大家指出。这里的文章提到了动态代理,spring中Aop的实现原理就是动态代理。后面会详细讲解动态代理,希望大家多多支持~