Spring Aysnc 最佳实践

1. @Async 的工作机制

首先为方法添加 `Async` 注解。接着,Spring 会基于 `proxyTargetClass` 属性,为包含 `Async` 定义的对象创建代理(JDK Proxy/CGlib)。最后,Spring 会尝试搜索与当前上下文相关的线程池,把该方法作为独立的执行路径提交。确切地说,Spring 会搜索唯一的 `TaskExecutor` bean 或者名为 `taskExecutor` 的 bean。如果找不到,则使用默认的 `SimpleAsyncTaskExecutor`。

要完成上面的过程,使用中需要注意几个限制,否则会出现 `Async` 不起作用的情况。

2. @Async 的限制

1. 必须在标记 `@ComponentScan` 或 `@configuration` 的类中使用 `@Async`。

2.1 在类中使用 Async 注解

package com.example.ask2shamik.springAsync.demo;

import java.util.Map;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncMailTrigger {
    @Async
    public void senMail(Map<String,String> properties) {
        System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
        properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V));
    }
}

2.2 Caller 类

package com.example.ask2shamik.springAsync.demo;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AsyncCaller {
    @Autowired
    AsyncMailTrigger asyncMailTriggerObject;

    public void rightWayToCall() {
        System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
        asyncMailTriggerObject.senMail(populateMap());
    }
    
    public void wrongWayToCall() {
        System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName());
        AsyncMailTrigger asyncMailTriggerObject = new AsyncMailTrigger();
        asyncMailTriggerObject.senMail(populateMap());
    }

    private Map<String,String> populateMap(){
        Map<String,String> mailMap= new HashMap<String,String>();
        mailMap.put("body", "A Ask2Shamik Article");
        return mailMap;
    }
}

上面的例子中,使用了 `@Autowired` 的 `AsyncMailTrigger` 受 `@ComponentScan` 管理,因而会创建新线程执行。而 `WrongWayToCall` 方法中创建的局部对象,不受 `@ComponentScan` 管理,不会创建新线程。

 2.3 输出

Calling From rightWayToCall Thread main
2019-03-09 14:08:28.893  INFO 8468 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
Trigger mail in a New Thread :: task-1
Key::body Value ::A Ask2Shamik Article
++++++++++++++++
Calling From wrongWayToCall Thread main
Trigger mail in a New Thread :: main
Key::body Value ::A Ask2Shamik Article

2. 不要在 `private` 方法上使用 `@Async` 注解。由于在运行时不能创建代理,所以不起作用。

@Async
private void senMail() {
    System.out.println("A proxy on Private method "  + Thread.currentThread().getName());
}

3. 调用 `methodAsync` 的 `caller` 方法与 `@Async` 方法应该在不同的类中定义。否则,尽管创建了代理对象,但 `caller` 会绕过代理直接调用方法,不会创建新线程。

2.4 示例

package com.example.ask2shamik.springAsync.demo;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class AsyncCaller {
    @Autowired
    AsyncMailTrigger asyncMailTriggerObject;

    public void rightWayToCall() {
        System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
        asyncMailTriggerObject.senMail(populateMap());
    }

    public void wrongWayToCall() {
        System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName());
        this.senMail(populateMap());
    }

    private Map<String,String> populateMap(){
        Map<String,String> mailMap= new HashMap<String,String>();
        mailMap.put("body", "A Ask2Shamik Article");
        return mailMap;
    }

    @Async
    public void senMail(Map<String,String> properties) {
        System.out.println("Trigger mail in a New Thread :: "  + Thread.currentThread().getName());
        properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V));
    }
}

最后,在执行的时候应当使用 `@EnableAsync` 注解。它的作用是让 Spring 在后台线程池中提交 `@Async` 方法。要自定义 `Executor` 可自己实现 bean。在接下来的文章中会给出具体的示例。

package com.example.ask2shamik.springAsync;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import com.example.ask2shamik.springAsync.demo.AsyncCaller;

@SpringBootApplication
@EnableAsync
public class DemoApplication {
    @Autowired
    AsyncCaller caller;

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
        caller.rightWayToCall();
        Thread.sleep(1000);
        System.out.println("++++++++++++++++");
        Thread.sleep(1000);
        caller.wrongWayToCall();
        };
    }
}

 

ExceptionHandler

从主线程 fork 新线程时,有两种情况:

1. "Fire-and-forget":fork 线程,然后为这个线程分配任务,接下来什么也不用管。不需要关心任务执行结果,其他业务逻辑的执行也不依赖该结果。通常任务的返回类型是 `void`。让我们通过例子帮助理解:假设你在为员工发薪水,需要给每个员工发送一份工资单邮件,你可以异步执行该任务。发邮件显然不是核心业务逻辑,而是一个横切关注点。然而,发邮件很好,而且在某些情况下是必须的。这时候需要制定失败重试或者定时机制。

2. "Fire-with-callback":在主线程中 fork 一个线程,为该线程分配任务并关联 `Callback`。接下来,主线程会继续执行其他任务并持续检查 `Callback` 结果。主线程需要子线程 `Callback` 执行结果进行下一步工作。

假设你正在做一份员工报告,员工信息根据各自数据类型存储在不同的后端。General Service 存储员工通用数据,比如姓名、生日、性别、地址等;Financial Service 存储薪资、税金以及其他 PF 相关数据。因此,你会创建两个并行线程,分别调用 General Service 与 Financial Service。这两组数据最终都要在报告中体现,因此需要进行数据组合,在主线程中表现为子线程 Callback 结果。一般会用 `CompletebleFuture` 实现。

在上面描述的场景中,如果一切顺利是最理想的结果。但如果执行中发生异常,该如何进行异常处理?

第二种情况下,由于回调执行后能够返回成功或失败,因此处理异常非常容易。失败的时候,异常会被封装在 `CompltebleFuture` 里,在主线程中可以检查异常并处理。处理异常的 Java 代码很简单,这里直接略过。

然而,第一种情况的异常处理非常棘手:创建的线程会执行业务逻辑,但如何确保业务执行成功?或者说,执行失败该如何进行调试,如何追踪是什么地方出现了问题?

解决方案很简单:注入自己的 exception handler。这样,当 `Async` 方法执行过程中发生异常,会把程序控制转交给 handler。你的 handler 知道接下来该如何处理。很简单,不是吗?

要做到这一点,需要执行以下步骤:

1. `AsyncConfigurer`:`AsyncConfigurere` 是一个 Spring 提供的接口,包含两个方法。一个可以重载 `TaskExecutor`(线程池),另一个是 exception handler。Exception handler 支持注入用来捕捉 unCaught 异常,也可以自己定义 class 直接实现。这里我不会直接实现,而是用 Spring 提供的 `AsyncConfigurerSupport` 类,通过 `@Configuration` 和 `@EnableAsync` 注解提供默认实现。

package com.example.ask2shamik.springAsync;


import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;


@Configuration
@EnableAsync
public class CustomConfiguration extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        return new SimpleAsyncTaskExecutor();
    }


    @Override
    @Nullable
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, obj) -> {
            System.out.println("Exception Caught in Thread - " + Thread.currentThread().getName());
            System.out.println("Exception message - " + throwable.getMessage());
            System.out.println("Method name - " + method.getName());
            for (Object param : obj) {
                System.out.println("Parameter value - " + param);
            }
        };
    }
}

请注意,因为不想使用自定义 task executor,在 `getAsyncExecutor` 方法中,没有创建任何新的 executor。因此,我将使用 Spring 默认的 `SimpleAsyncExecutor`。

但是,我需要自己定义 uncaught exception handler 处理 uncaught 异常。因此,我写了一条继承 `AsyncUncaughtExceptionHandler` 类的 lambda 表达式并覆盖 `handleuncaughtexception` 方法。

这样,会让 Spring 加载与应用匹配的 `AsyncConfugurer(CustomConfiguration)` 并用 lambda 表达式进行异常处理。

新建一个 `@Async` 方法抛出异常:

package com.example.ask2shamik.springAsync;

import java.util.Map;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


@Component
public class AsyncMailTrigger {
    @Async
    public void senMailwithException() throws Exception {
        throw new Exception("SMTP Server not found :: orginated from Thread :: " + Thread.currentThread().getName());
    }
}

现在,创建调用方法。

package com.example.ask2shamik.springAsync;


import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;


@Component
public class AsyncCaller {
    @Autowired
    AsyncMailTrigger asyncMailTriggerObject;


    public void rightWayToCall() throws Exception {
        System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
        asyncMailTriggerObject.senMailwithException();
    }
}

接下来让我们启动 Spring Boot 应用,看它如何捕捉 `sendMailwithException` 方法引发的异常。

package com.example.ask2shamik.springAsync;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import com.example.ask2shamik.springAsync.demo.AsyncCaller;


@SpringBootApplication
public class DemoApplication {
    @Autowired
    AsyncCaller caller;


    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }


    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            caller.rightWayToCall();
        };
    }
}

结果如下:

```shell
Calling From rightWayToCall Thread main
Exception Caught in Thread - SimpleAsyncTaskExecutor-1
Exception message - SMTP Server not found:: originated from Thread:: SimpleAsyncTaskExecutor-1
Method name - senMailwithException
```

Spring Async 如何在 Web 应用中工作

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

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

目标

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

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

Controller

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

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()` 语句,稍后我会讨论它的作用。

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:

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 对象

package com.example.demo;

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

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

异步邮件服务

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


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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值