ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Spring Async 最佳实践(3):完结篇

2021-05-30 10:54:43  阅读:195  来源: 互联网

标签:完结篇 name Thread Spring org request springframework Async import


在之前的文章中,我们讨论了 Spring Async 概念以及如何把它用好。如果想要重温之前的文章,请查看下面链接:


[1]:Spring Aysnc 最佳实践(1):原理与限制

[2]:Spring Async 最佳实践(2):ExceptionHandler


在这一篇中,我们将讨论 Spring Async 如何在 Web 应用中工作。


很高兴能和大家分享关于 Spring Async 和 `HttpRequest` 的使用经验。在最近参与的项目中遇到了一件有趣的事情,相信我的经历可以为你在将来节省一些宝贵的时间。


让我试着描述一下当时的场景:


目标


需要把数据从 UI 传给后端 Controller,接着 Controller 将执行一些操作,最终调用异步邮件服务发送邮件。


一位初级工程师编写了这部分代码。下面是我根据功能复现的代码,你能找出中间的问题吗?


Controller


Controller 通过接收 HTTP Servelet 请求从 UI 收集信息,接着执行一些操作,并将请求转给异步邮件服务。


```java
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetController {
   @Autowired
   private AsyncMailTrigger greeter;

   @RequestMapping(value = "/greet", method = RequestMethod.GET)
   public String greet(HttpServletRequest request) throws Exception {
       String name = request.getParameter("name");
       greeter.asyncGreet(request);
       System.out.println(Thread.currentThread() + " Says Name is " + name);
       System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
       return name;
   }
}
```


异步邮件服务 `AsyncMailTrigger` 类加上了 `@Component` 注解,你也可以改成 `@Service`。其中包含了 `asyncGreet` 方法,接受 `HttpRequest` 输入,从中获取信息并发送邮件(简单起见,这一部分被略过)。**注意:** 这里有一条 `Thread.sleep()` 语句,稍后我会讨论它的作用。


```java
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncMailTrigger {
   @Async
   public void asyncGreet(HttpServletRequest request) throws Exception {
       System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
       System.out.println(Thread.currentThread().getName() + " greets before sleep" + request.getParameter("name"));
       Thread.sleep(1000);
       System.out.println(Thread.currentThread().getName() + " greets" + request.getParameter("name"));
       System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
   }
}
```


下面是 main class:


```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringAsyncWebApplication {
   public static void main(String[] args) {
       SpringApplication.run(SpringAsyncWebApplication.class, args);
   }
}
```


运行程序,输出结果如下:


```
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 821691136
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets null task-1 Hashcode 821691136
```


仔细查看输出会发现:在 `sleep()` 调用前 `request` 信息正确,但调用 `sleep()` 后 `request` 信息就神奇地消失了。很奇怪,对吧?但从 hashcode 可以证明它们是同一个 request 对象。


到底发生了什么?`request` 信息消失的原因是什么?我们的初级工程师遇到了这样的情况,收件人信息、收件人的姓名从 `request` 中消失了,邮件也没有发送成功。


让我们仔细调查这个问题


`request` 出现问题很正常。要理解这个问题,首先要了解 `request` 的生命周期。


在调用 Servlet 方法前,Servlet 容器会创建 `request` 对象。Spring 通过 Dispatcher Servlet 传递 `request` ,根据映射找到对应的 Controller 并调用相应的方法。当 `request` 得到响应时,Servlet 容器要么删除要么重置 `request` 对象的状态(完全取决于容器的实现,这里实际上维护了一个 request pool)。然而,这里不打算深入探讨关于容器如何维护 `request` 对象这个话题。


"但是请记住:" 一旦`request` 得到响应时,容器就会删除或者重置 `request` 对象。


现在,让我们思考 Spring Async 代码。Async 的工作是从线程池中分配一个线程让它执行任务。上面的例子中,我们把 `request` 对象传递给异步线程,并在 `asyncGreet` 方法中,试图直接从 `request` 对象提取信息。


然而,由于这里的操作是异步的,主线程(即 Controller 部分)不会等待线程完成。它会直接执行 `print` 语句,返回 `response`,并刷新 `request` 对象的状态。


这里的问题在于,我们直接把 `request` 对象传给了异步线程。为了证明上面的推断,这里加上了一条 `sleep` 语句。当主线程在 `sleep` 结束前返回 `response`,就能复现之前问题中的现象。


从这个实验中可以学到什么?


使用 Async 时,**不要**直接传 `request` 对象或任何与 `Request/Response` 相关的对象。因为永远不知道什么时候会提交 `response` 并刷新状态。如果这样做,可能会遇到偶发性错误。


有什么解决办法?


如果需要传递 `request` 中的信息,可以创建一个 `value` 对象。为对象设置信息后,把 `value` 对象传给 Spring Async。通过这种方式,可以解决上面的问题:


RequestVO 对象


```java
package com.example.demo;

public class RequestVO {
   String name;
   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }
}
```


异步邮件服务


```java
package com.example.demo;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncMailTrigger {
   @Async
   public void asyncGreet(RequestVO reqVO) throws Exception {
       System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
       System.out.println(Thread.currentThread().getName() + " greets before sleep" + reqVO.getName());
       Thread.sleep(1000);
       System.out.println(Thread.currentThread().getName() + " greets" + reqVO.getName());
   }
}
```


Greet Controller


```java
package com.example.demo;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetController {
   @Autowired
   private AsyncMailTrigger greeter;

   @RequestMapping(value = "/greet", method = RequestMethod.GET)
   public String greet(HttpServletRequest request) throws Exception {
       String name = request.getParameter("name");
       RequestVO vo = new RequestVO();
       vo.setName(name);
       //greeter.asyncGreet(request);
       greeter.asyncGreet(vo);
       System.out.println(Thread.currentThread() + " Says Name is " + name);
       System.out.println(Thread.currentThread().getName() + " Hashcode" + request.hashCode());
       return name;
   }
}
```


输出


```
Thread[http-nio-8080-exec-1,5,main] Says Name is Shamik
http-nio-8080-exec-1 Hashcode 1669579896
Trigger mail in a New Thread:: task-1
task-1 greets before sleep Shamik
task-1 greets Shamik
```


总结 


希望你喜欢这篇文章。如果有任何问题,欢迎在文后留言。


标签:完结篇,name,Thread,Spring,org,request,springframework,Async,import
来源: https://blog.51cto.com/u_15127686/2832738

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有