SpringBoot接口异步

场景

由于从其他地方接数据,数据量较大。要进行较长时间的读写操作,前端请发出请求后,一直被挂起,现需要将接口改为异步返回。由于请求比较耗时,故还有需求:不允许五分钟内访问两次,详情请见之前的博客。SpringBoot接口限流

整体思路

使用各种方式实现接口的异步返回,需要在耗时操作开始的时候给前端返回结果,而不是执行完耗时操作以后再返回数据给前端。

实现方式

实现方式有很多,我自己使用的是CompletableFuture方式,其他方式整理起来当个笔记。

方式1:CompletableFuture类

CompletableFuture是Java 8中提供的一个用于异步编程的类,它提供了一些方法,可以在异步操作完成后执行特定的任务。CompletableFuture适用于需要执行多个异步操作的场景,可以使用CompletableFuture的方法来组合多个异步操作,提高代码的可读性和可维护性。(该方式为最后使用的方式,其他方式未进行验证使用。)

  • controller

    public CompletableFuture<String> addData(){
    		//元组返回:T1-是否可以进入耗时操作  T2-提示语
            Tuple2<Boolean, String> resTuple = addService.getLicense();
            if(resTuple.getT1()){
                return CompletableFuture.supplyAsync(()->{
               		//执行耗时操作,同时返回提示语
                    addService.timeConsuming();
                    return resTuple.getT2();
                });
            }else {
                return CompletableFuture.supplyAsync(resTuple::getT2);
            }
        }
    
  • Service

    1. getLicense获取是否可以进入耗时操作方法
    @Override
    public Tuple2<Boolean, String> getLicense() {
        String requestId = RandomUtils.uuid();
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, source, requestId, TimeUnit.MINUTES);
        if (!locked) {
            //正在更新
            return Tuples.of(false, "更新中,请稍候。");
        }
        String lastCallTime0 = redisTemplate.opsForValue().get(LAST_CALL_TIME_KEY);
        Long time = System.currentTimeMillis() - LIMIT_QUERY_TIME - 10 * 1000;
        String lastCallTimeTemp = lastCallTime0 == null ? String.valueOf(time) : lastCallTime0;
        long lastCallTime = Long.parseLong(lastCallTimeTemp);
        long now = System.currentTimeMillis();
        if (now - lastCallTime < LIMIT_QUERY_TIME) {
            redisTemplate.delete(LOCK_KEY);
            //不到5分钟
            return Tuples.of(false, "5分钟内不可重复更新,请稍后重试。");
        }
    	// 从ERP获取数据
        return Tuples.of(true, "更新中,请稍候。");
    }
    
    1. timeConsuming耗时操作所在方法
      注意要加上@Async注解,开启新的线程来执行耗时操作。
    @Async
    @Override
    public void timeConsuming() {
        // 执行耗时操作
        long now = System.currentTimeMillis();
        redisTemplate.opsForValue().set(LAST_CALL_TIME_KEY, String.valueOf(now));
        redisTemplate.delete(LOCK_KEY);
    }
    

方式2:Callable接口

Callable是Java标准库提供的一个可以在另一个线程中执行的任务,它的call方法可以返回一个结果,并且可以抛出异常。Callable适用于需要执行长时间运行的代码的场景,可以使用线程池来执行多个Callable任务,提高应用程序的并发处理能力。

  • controller

    @RestController
    public class MyController {
        
        @Autowired
        private MyService myService;
        
        @GetMapping("/async")
        public Callable<String> asyncMethod() {
            return () -> {
                String result;
                try {
                    result = myService.longRunningMethod();
                } catch (Exception e) {
                    throw new Exception("异步操作发生异常:" + e.getMessage());
                }
                return result;
            };
        }
    }
    
  • service

    @Service
    public class MyService {
       
       public String longRunningMethod() {
           // 长时间运行的代码
           return "result";
       }
    }
    

方式3:DeferredResult类

DeferredResult是Spring框架提供的一种异步处理机制,可以实现非阻塞的异步处理。它的实现原理是在请求处理的过程中,将结果的处理交由另外一个线程或者线程池来完成,这样在当前请求线程中就不需要等待结果的返回,从而实现了异步处理。客户端就可以立即返回结果,而不需要等待异步处理的完成。在异步处理完成后,DeferredResult对象会自动将结果返回给客户端。

使用DeferredResult的步骤如下:

  1. 在Controller中创建DeferredResult对象,将其返回给客户端。
  2. 在DeferredResult对象的回调函数中,实现异步处理逻辑,并将处理结果设置到DeferredResult对象中。
  3. 后台线程或线程池中处理完异步逻辑后,将结果设置到DeferredResult对象中。

需要注意的是,使用DeferredResult可能会带来一些问题,比如长时间的等待、线程池资源的消耗等,需要根据具体的业务场景进行合理的使用。

使用样例:
假设有一个接口 /download,客户端可以通过该接口下载一份大文件,由于文件比较大,下载可能需要很长时间,为了避免客户端等待太久,我们可以使用 DeferredResult 实现异步处理,客户端发起请求后,服务端先返回一个 DeferredResult 对象,然后在后台线程中下载文件,并将下载结果设置到 DeferredResult 对象中,当下载完成后,DeferredResult 对象会自动返回结果给客户端。

  • controller
    @RestController
    public class DownloadController {
        @Autowired
        private DownloadService downloadService;
    
        @RequestMapping("/download")
        public DeferredResult<ResponseEntity<InputStreamResource>> download() {
            DeferredResult<ResponseEntity<InputStreamResource>> deferredResult = new DeferredResult<>();
    
            // 在后台线程中进行下载操作
            downloadService.downloadFile(deferredResult);
    
            return deferredResult;
        }
    }
    
  • service
    @Service
    public class DownloadService {
        @Async
        public void downloadFile(DeferredResult<ResponseEntity<InputStreamResource>> deferredResult) {
            try {
                // 模拟文件下载,耗时5秒
                Thread.sleep(5000);
    
                // 下载完成后,将结果设置到DeferredResult对象中
                File file = new File("path/to/file");
                InputStreamResource resource = new InputStreamResource(new FileInputStream(file));
                HttpHeaders headers = new HttpHeaders();
                headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.txt");
                ResponseEntity<InputStreamResource> responseEntity = new ResponseEntity<>(resource, headers, HttpStatus.OK);
                deferredResult.setResult(responseEntity);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

方式4:Guava异步

Guava是Google公司开源的一个Java工具类库,其中包含了许多实用的工具类,如集合、字符串、并发等。其中,Guava提供了异步编程的支持,可以帮助我们更加方便地实现异步操作。

Guava的异步编程支持主要包括以下两个部分:

  1. ListenableFuture
    ListenableFuture是Guava提供的一个接口,它继承了JDK中的Future接口,同时提供了添加回调方法的功能。这样,在异步任务执行完成后,我们可以通过添加回调方法来处理异步任务的执行结果。ListenableFuture的主要作用是提供一个异步操作的结果的占位符,当异步操作执行完成后,占位符被填充上真正的结果。
  2. ListeningExecutorService
    ListeningExecutorService是Guava提供的一个接口,它扩展了JDK中的ExecutorService接口,同时提供了异步任务执行结果的回调功能。使用ListeningExecutorService可以很方便地实现异步任务的执行和结果处理。

在使用Guava的异步编程功能时,通常需要首先创建一个实现Callable接口的异步任务类,然后通过ListeningExecutorService的submit方法将异步任务提交到线程池中执行。执行完成后,通过ListenableFuture的回调方法来处理异步任务的执行结果。

总体来说,Guava的异步编程功能可以帮助我们更加方便地实现异步操作,提高代码的可读性和可维护性。

使用步骤:

  1. 添加依赖
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>30.1.1-jre</version>
    </dependency>
    
  2. controller
    @RestController
    public class UserController {
        @Autowired
        private UserService userService;
        
        @GetMapping("/users/{userId}")
        public ResponseEntity<User> getUserInfo(@PathVariable String userId) throws InterruptedException, ExecutionException {
            ListenableFuture<User> future = userService.getUserInfo(userId);
            User user = future.get();
            return ResponseEntity.ok(user);
        }
    }
    
  3. service
    @Service
    public class UserService {
        private final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
        
        public ListenableFuture<User> getUserInfo(String userId) {
            return executorService.submit(() -> {
                // 模拟耗时操作
                TimeUnit.SECONDS.sleep(3);
                // 从外部系统中获取用户信息
                User user = externalSystem.getUserInfo(userId);
                return user;
            });
        }
        
        public void processResult(ListenableFuture<User> future) {
            Futures.addCallback(future, new FutureCallback<User>() {
                @Override
                public void onSuccess(User result) {
                    // 处理异步任务执行成功的结果
                    System.out.println("获取用户信息成功:" + result.getName());
                }
                
                @Override
                public void onFailure(Throwable t) {
                    // 处理异步任务执行失败的结果
                    System.out.println("获取用户信息失败:" + t.getMessage());
                }
            }, MoreExecutors.directExecutor());
        }
    }
    

写在最后

方法众多,选择合适的用。我只实际使用第一种,其他的未进行使用和验证,如果有什么问题欢迎指正!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot中,你可以使用异步调用来提高应用程序的性能和吞吐量。Spring Boot支持两种异步方式:使用Callable和DeferredResult。 使用Callable: 1. 在Controller中定义一个方法,并使用@Async注解标记该方法。 ```java @RestController public class MyController { @Autowired MyService myService; @GetMapping("/async") public Callable<String> async() { return () -> myService.doSomething(); } } ``` 2. 在Service层中定义一个异步方法,并使用@Async注解标记该方法。 ```java @Service public class MyService { @Async public String doSomething() { // 执行耗时操作 return "result"; } } ``` 使用DeferredResult: 1. 在Controller中定义一个方法,并使用DeferredResult作为返回值。 ```java @RestController public class MyController { @Autowired MyService myService; @GetMapping("/async") public DeferredResult<String> async() { DeferredResult<String> deferredResult = new DeferredResult<>(); CompletableFuture.supplyAsync(() -> myService.doSomething()) .whenCompleteAsync((result, throwable) -> deferredResult.setResult(result)); return deferredResult; } } ``` 2. 在Service层中定义一个异步方法,并使用CompletableFuture异步执行该方法。 ```java @Service public class MyService { public String doSomething() { // 执行耗时操作 return "result"; } } ``` 注意事项: 1. 需要在Spring Boot应用程序的启动类上添加@EnableAsync注解来启用异步调用。 ```java @SpringBootApplication @EnableAsync public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } ``` 2. 异步调用的返回值应该是Future或其子类,例如Callable和DeferredResult。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值