1 scene

Call multiple level services and return the first valid data according to service priority.

Specific case: a page may have many pop-up windows, and the pop-up windows have priorities. Only the first popup window with data needs to be returned each time. But I also hope that the data acquisition between all pop-up windows is asynchronous. How to implement this scenario using Reactor?

2 Create service

2.1 Create basic interface and entity class

 public interface TestServiceI {
    Mono request();
}

Provides a request method that returns a Mono object.

 @Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TestUser {
    private String name;
}

2.2 Create service implementation

 @Slf4j
public class TestServiceImpl1 implements TestServiceI {
    @Override
    public Mono request() {
        log.info("execute.test.service1");
        return Mono.fromSupplier(() -> {
                    try {
                        System.out.println("service1.threadName=" + Thread.currentThread().getName());
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return "";
                })
                .map(name -> {
                    return new TestUser(name);
                });
    }
}

The first service execution takes 500ms. return empty object;

Creating the second service takes 1000ms to execute. Returns an empty object; the code is as above, just change the sleep time.

Continue to create a third service execution takes 1000ms. Return name3. The code is as above, change the sleep time, and return to name3.

3 main method

 public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        TestServiceI testServiceImpl4 = new TestServiceImpl4();
        TestServiceI testServiceImpl5 = new TestServiceImpl5();
        TestServiceI testServiceImpl6 = new TestServiceImpl6();
        List<TestServiceI> serviceIList = new ArrayList<>();
        serviceIList.add(testServiceImpl4);
        serviceIList.add(testServiceImpl5);
        serviceIList.add(testServiceImpl6);

    // 执行 service 列表,这样有多少个 service 都可以
        Flux<Mono<TestUser>> monoFlux = Flux.fromIterable(serviceIList)
                .map(service -> {
                    return service.request();
                });

    // flatMap(或者flatMapSequential) + map 实现异常继续下一个执行
        Flux flux = monoFlux.flatMapSequential(mono -> {
            return mono.map(user -> {
                        TestUser testUser = JsonUtil.parseJson(JsonUtil.toJson(user), TestUser.class);
                        if (Objects.nonNull(testUser) && StringUtils.isNotBlank(testUser.getName())) {
                            return testUser;
                        }
            // null 在 reactor 中是异常数据。
                        return null;
                    })
                    .onErrorContinue((err, i) -> {
                        log.info("onErrorContinue={}", i);
                    });
        });
        Mono mono = flux.elementAt(0, Mono.just(""));
        Object block = mono.block();
        System.out.println(block + "blockFirst 执行耗时ms:" + (System.currentTimeMillis() - startTime));
    }

1. Flux.fromIterable executes the service list, and you can add or delete service services at will.

2. flatMap (or flatMapSequential) + map + onErrorContinue implements an exception and continues to the next execution. Specific reference:

Reactor's onErrorContinue and onErrorResume

3. Mono mono = flux.elementAt(0, Mono.just("")); Return the first normal data.

Execution output:

 20:54:26.512 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
20:54:26.553 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1
service1.threadName=main
20:54:27.237 [main] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
20:54:27.237 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
service5.threadName=main
20:54:28.246 [main] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
20:54:28.246 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service6.threadName=main
TestUser(name=name3)blockFirst 执行耗时ms:2895

1. Since service1 and service2 return empty, continue to the next one, and finally return name3.

2. View the total time: 2895ms. Service1 takes 500, service2 takes 1000, and service3 takes 1000. The discovery time is basically equal to service1 + service2 + service3. What's going on here? View the threads that return to execution, all of which are main.

Summary: This implementation returns the first normal data in order. But the execution is not asynchronous. Next step: how to implement asynchrony?

4 Implementing asynchrony

4.1 SubscribeOn implements asynchronous

Modify the service implementation. Added .subscribeOn(Schedulers.boundedElastic())

as follows:

 @Slf4j
public class TestServiceImpl1 implements TestServiceI {
    @Override
    public Mono request() {
        log.info("execute.test.service1");
        return Mono.fromSupplier(() -> {
                    try {
                        System.out.println("service1.threadName=" + Thread.currentThread().getName());
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return "";
                })
                //增加subscribeOn
                .subscribeOn(Schedulers.boundedElastic())
                .map(name -> {
                    return new TestUser(name);
                });
    }
}

The output of executing it again is as follows:

 21:02:04.213 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
21:02:04.265 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1
service4.threadName=boundedElastic-1
21:02:04.300 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
21:02:04.302 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service2.threadName=boundedElastic-2
service3.threadName=boundedElastic-3
21:02:04.987 [boundedElastic-1] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
21:02:05.307 [boundedElastic-2] INFO com.geniu.reactor.TestReactorOrderV2 - onErrorContinue=TestUser(name=)
TestUser(name=name6)blockFirst 执行耗时ms:1242

1. It is found that the thread that implements sleep is not the main thread, but boundedElastic ;

2. The final execution time is 1242ms, which is only 1000ms longer than service2 and service3 which have the longest execution time. The proof is asynchronous.

4.2 CompletableFuture implements asynchronous

Modify the service implementation and use CompletableFuture to perform time-consuming operations (sleep here, which may be external interface calls, DB operations, etc. in the project); then use Mono.fromFuture to return the Mono object.

 @Slf4j
public class TestServiceImpl1 implements TestServiceI{
    @Override
    public Mono request() {
        log.info("execute.test.service1");
        CompletableFuture<String> uCompletableFuture = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println("service1.threadName=" + Thread.currentThread().getName());
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return "testname1";
        });

        return Mono.fromFuture(uCompletableFuture).map(name -> {
            return new TestUser(name);
        });
    }
}

Execution returns as follows:

 21:09:59.465 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
21:09:59.510 [main] INFO com.geniu.reactor.TestServiceImpl2 - execute.test.service2
service2.threadName=ForkJoinPool.commonPool-worker-1
21:09:59.526 [main] INFO com.geniu.reactor.TestServiceImpl3 - execute.test.service3
service3.threadName=ForkJoinPool.commonPool-worker-2
21:09:59.526 [main] INFO com.geniu.reactor.TestServiceImpl1 - execute.test.service1 
service1.threadName=ForkJoinPool.commonPool-worker-3
21:10:00.526 [ForkJoinPool.commonPool-worker-1] INFO com.geniu.reactor.TestReactorOrder - onErrorContinue=TestUser(name=)
21:10:00.538 [ForkJoinPool.commonPool-worker-2] INFO com.geniu.reactor.TestReactorOrder - onErrorContinue=TestUser(name=)
TestUser(name=testname1)blockFirst 执行耗时ms:1238

1. Time-consuming operations are performed using threads in the ForkJoinPool thread pool.

2. The final time is basically the same as that of method 1.

Let's all try it~

Related Links:

Reactor's onErrorContinue and onErrorResume

Detailed explanation of Reactor's flatMap vs map


程序员伍六七
201 声望597 粉丝