Spring 異步接口返回結果的四種方式

1. 需求

開發中我們經常遇到異步接口需要執行一些耗時的操作,並且接口要有返回結果。

使用場景:用戶綁定郵箱、手機號,將郵箱、手機號保存入庫後發送郵件或短信通知
接口要求:數據入庫後給前臺返回成功通知,後臺異步執行發郵件、短信通知操作

一般的話在企業中會借用消息隊列來實現發送,業務量大的話有一個統一消費、管理的地方。但有時項目中沒有引用mq相關組件,這時為瞭實現一個功能去引用、維護一個消息組件有點大材小用,下面介紹幾種不引用消息隊列情況下的解決方式

定義線程池:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
 * @description: 公共配置
 * @author: yh
 * @date: 2022/8/26
 */
@EnableAsync
@Configuration
public class CommonConfig {
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 設置核心線程數
        executor.setCorePoolSize(50);
        // 設置最大線程數
        executor.setMaxPoolSize(200);
        // 設置隊列容量
        executor.setQueueCapacity(200);
        // 設置線程活躍時間(秒)
        executor.setKeepAliveSeconds(800);
        // 設置默認線程名稱
        executor.setThreadNamePrefix("task-");
        // 設置拒絕策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任務結束後再關閉線程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        return executor;
    }
}

2. 解決方案

2.1 @Async

定義異步任務,如發送郵件、短信等

@Service
public class ExampleServiceImpl implements ExampleService {
    @Async("taskExecutor")
    @Override
    public void sendMail(String email) {
        try {
            Thread.sleep(3000);
            System.out.println("異步任務執行完成, " + email + " 當前線程:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Controller

@RequestMapping(value = "/api")
@RestController
public class ExampleController {
    @Resource
    private ExampleService exampleService;

    @RequestMapping(value = "/bind",method = RequestMethod.GET)
    public String bind(@RequestParam("email") String email) {
        long startTime = System.currentTimeMillis();
        try {
            // 綁定郵箱....業務
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //模擬異步任務(發郵件通知、短信等)
        exampleService.sendMail(email);

        long endTime = System.currentTimeMillis();
        System.out.println("方法執行完成返回,耗時:" + (endTime - startTime));
        return "ok";
    }
}

運行結果:

2.2 TaskExecutor

@RequestMapping(value = "/api")
@RestController
public class ExampleController {
    @Resource
    private ExampleService exampleService;
    @Resource
    private TaskExecutor taskExecutor;

    @RequestMapping(value = "/bind", method = RequestMethod.GET)
    public String bind(@RequestParam("email") String email) {
        long startTime = System.currentTimeMillis();
        try {
            // 綁定郵箱....業務
            Thread.sleep(2000);

            // 將發送郵件交給線程池去執行
            taskExecutor.execute(() -> {
                exampleService.sendMail(email);
            });
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("方法執行完成返回,耗時:" + (endTime - startTime));
        return "ok";
    }
}

運行結果:

2.3 Future

首先去掉Service方法中的@Async("taskExecutor"),此時執行就會變成同步,總計需要5s才能完成接口返回。這次我們使用jdk1.8中的CompletableFuture來實現異步任務

@RequestMapping(value = "/api")
@RestController
public class ExampleController {
    @Resource
    private ExampleService exampleService;
    @Resource
    private TaskExecutor taskExecutor;

    @RequestMapping(value = "/bind", method = RequestMethod.GET)
    public String bind(@RequestParam("email") String email) {
        long startTime = System.currentTimeMillis();
        try {
            // 綁定郵箱....業務
            Thread.sleep(2000);

            // 將發送郵件交給異步任務Future,需要記錄返回值用supplyAsync
            CompletableFuture.runAsync(() -> {
                exampleService.sendMail(email);
            }, taskExecutor);

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("方法執行完成返回,耗時:" + (endTime - startTime));
        return "ok";
    }
}

運行結果:

2.4 @EventListener

Spring為我們提供的一個事件監聽、訂閱的實現,內部實現原理是觀察者設計模式;為的就是業務系統邏輯的解耦,提高可擴展性以及可維護性。事件發佈者並不需要考慮誰去監聽,監聽具體的實現內容是什麼,發佈者的工作隻是為瞭發佈事件而已。

2.4.1 定義event事件模型

public class NoticeEvent extends ApplicationEvent {
    private String email;
    private String phone;
    public NoticeEvent(Object source, String email, String phone) {
        super(source);
        this.email = email;
        this.phone = phone;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
}

2.4.2 事件監聽

@Component
public class ComplaintEventListener {

    /**
     * 隻監聽NoticeEvent事件
     * @author: yh
     * @date: 2022/8/27
     */
    @Async
    @EventListener(value = NoticeEvent.class)
//    @Order(1) 指定事件執行順序
    public void sendEmail(NoticeEvent noticeEvent) {
        //發郵件
        try {
            Thread.sleep(3000);
            System.out.println("發送郵件任務執行完成, " + noticeEvent.getEmail() + " 當前線程:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Async
    @EventListener(value = NoticeEvent.class)
//    @Order(2) 指定事件執行順序
    public void sendMsg(NoticeEvent noticeEvent) {
        //發短信
        try {
            Thread.sleep(3000);
            System.out.println("發送短信步任務執行完成, " + noticeEvent.getPhone() + " 當前線程:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

2.4.5 事件發佈

@RequestMapping(value = "/api")
@RestController
public class ExampleController {
    /**
     * 用於事件推送
     * @author:  yh
     * @date:  2022/8/27
     */
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @RequestMapping(value = "/bind", method = RequestMethod.GET)
    public String bind(@RequestParam("email") String email) {
        long startTime = System.currentTimeMillis();
        try {
            // 綁定郵箱....業務
            Thread.sleep(2000);

            // 發佈事件,這裡偷個懶手機號寫死
            applicationEventPublisher.publishEvent(new NoticeEvent(this, email, "13211112222"));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("方法執行完成返回,耗時:" + (endTime - startTime));
        return "ok";
    }
}

運行結果:

3. 總結

通過@Async、子線程、Future異步任務、Spring自帶ApplicationEvent事件監聽都可以完成以上描述的需求。

到此這篇關於Spring 異步接口返回結果的四種方式的文章就介紹到這瞭,更多相關Spring 異步接口內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: