查看原文
其他

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

ImportNew ImportNew 2019-10-04

(给ImportNew加星标,提高Java技能)


编译:唐尤华

链接: dzone.com/articles/effective-advice-on-spring-async-part-1


据我观察,无论是初级新手还是高级开发者都流行用 Spring Boot 构建自己的程序。Spring Boot “约定优于配置”的风格让大家在开发时能专注于业务逻辑。需要的时候,查阅 Spring Boot 教程就可以很方便地了解 Spring 的工作机制。尽管大多数时候只要添加几个注解就可以搞定,但有时候还是需要了解它背后的运行机制,这样才能更专业地使用 Spring Boot。


本文将尝试介绍如何在 Spring 中进行异步处理。


异步处理适用那些与业务逻辑(横切关注点)不直接相关或者不作为其他业务逻辑输入的部分,也可在分布式系统中解耦。


*译注:横切关注点(cross-cutting concerns)指一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效模块化的一类特殊关注点。*


Spring 中,`@Async`注解可以标记异步操作。然而,使用`@Async`时有一些限制,仅仅把它加在方法上并不能确保方法会在独立的线程中执行。如果你只是偶尔用到 `@Async`,需要格外当心。


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 注解


```java
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 类


```java
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 输出


```shell
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` 注解。由于在运行时不能创建代理,所以不起作用。


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



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


(https://dzone.com/storage/temp/11422746-springasync.jpg)


2.4 示例


```java
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。在接下来的文章中会给出具体的示例。


```java
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();
};
}
}
```


 3. 总结


希望通过本文的讲解,可以帮助理解 Async 的内部机制及使用中的限制。下一篇会探讨 Async 中的异常处理。敬请期待!


推荐阅读

(点击标题可跳转阅读)

Java 开发者应该改掉的 3 种不良习惯

在 Java 中应用骨架实现

Spring 中获取 request 的几种方法,及其线程安全性分析


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

喜欢就点一下「好看」呗~

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存