• 微言异步回调


    前言

    回调,顾名思义,回过头来调用,详细的说来就是用户无需关心内部实现的具体逻辑,只需要在暴露出的回调函数中放入自己的业务逻辑即可。由于回调机制解耦了框架代码和业务代码,所以可以看做是对面向对象解耦的具体实践之一。由于本文的侧重点在于讲解后端回调,所以对于前端回调甚至于类似JSONP的回调函数类的,利用本章讲解的知识进行代入的时候,请斟酌一二,毕竟后端和前端还是有一定的区别,所谓差之毫厘,可能谬以千里,慎之。所以本章对回调的讲解侧重于后端,请知悉。

    回调定义

    说到回调,其实我的理解类似于函数指针的功能,怎么讲呢?因为一个方法,一旦附加了回调入参,那么用户在进行调用的时候,这个回调入参是可以用匿名方法直接替代的。回调的使用必须和方法的签名保持一致性,下面我们来看一个JDK实现的例子:

        default boolean removeIf(Predicate<? super E> filter) {
            Objects.requireNonNull(filter);
            boolean removed = false;
            final Iterator<E> each = iterator();
            while (each.hasNext()) {
                if (filter.test(each.next())) {
                    each.remove();
                    removed = true;
                }
            }
            return removed;
        }

    在JDK中,List结构有一个removeIf的方法,其实现方式如上所示。由于附带了具体的注释讲解,我这里就不再进行过多的讲述。我们需要着重关注的是其入参:Predicate,因为他就是一个函数式接口,入参为泛型E,出参为boolean,其实和Function<? super E, boolean>是等价的。由于List是一个公共的框架代码,里面不可能糅合业务代码,所以为了解耦框架代码和业务代码,JDK使用了内置的各种函数式接口作为方法的回调,将具体的业务实践抛出去,让用户自己实现,而它自己只接受用户返回的结果就行了:只要用户处理返回true(filter.test(each.next()返回true),那么我就删掉当前遍历的数据;如果用户处理返回false(filter.test(each.next()返回false),那么我就保留当前遍历的数据。是不是非常的nice?

    其实这种完美的协作关系,在JDK类库中随处可见,在其他经常用到的框架中也很常见,诸如Guava,Netty,实在是太多了(这也从侧面说明,利用函数式接口解耦框架和业务,是正确的做法),我撷取了部分片段如下:

    //将map中的所有entry进行替换
    void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
    //将map中的entry进行遍历
    void forEach(BiConsumer<? super K, ? super V> action)
    //map中的entry值如果有,则用新值重新建立映射关系
    V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
    
    //Deque遍历元素
    void forEach(Consumer<? super T> action)
    //Deque按给定条件移除元素
    boolean removeIf(Predicate<? super E> filter)
    
    //Guava中获取特定元素
    <T> T get(Object key, final Callable<T> valueLoader) 
    
    //Netty中设置监听
    ChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener)
    
    

    那么,回过头来想想,如果我们封装自己的组件,想要封装的很JDK Style,该怎么做呢?如果上来直接理解Predicate,Function,Callable,Consumer,我想很多人是有困难的,那就听我慢慢道来吧。

    我们先假设如下一段代码,这段代码我相信很多人会很熟悉,很多人也封装过,这就是我们的大名鼎鼎的RedisUtils封装类:

      /**
         * key递增特定的num
         * @param key Redis中保存的key
         * @param num 递增的值
         * @return key计算完毕后返回的结果值
         */
        public Long incrBy(String key,long num) {
            CallerInfo callerInfo = Profiler.registerInfo("sendCouponService.redis.incrBy", "sendCouponService", false, true);
            Long result;
            try {
                result = jrc.incrBy(key, num);
            } catch (Exception e) {
                logger.error("incrBy", null, "sendCouponService.redis.incrBy异常,key={},value={}", e, key, num);
                Profiler.functionError(callerInfo);
                throw new RuntimeException("sendCouponService.redis.incrBy异常,key=" + key, e);
            } finally {
                Profiler.registerInfoEnd(callerInfo);
            }
            return result;
        }

    上面这段代码只是一个示例,其实还有上百个方法基本上都是这种封装结构,这种封装有问题吗?没问题!而且封装方式辛辣老道,一看就是高手所为,因为既加了监控,又做了异常处理,而且还有错误日志记录,一旦发生问题,我们能够第一时间知道哪里出了问题,哪个方法出了问题,然后设定对应的应对方法。

    这种封装方式,如果当做普通的Util来用,完全没有问题,但是如果想封装成组件,则欠缺点什么,我列举如下:

    1. 当前代码写死了用jrc操作,如果后期切换到jimdb,是不是还得为jimdb专门写一套呢?

    2. 当前代码,上百个方法,其实很多地方都是重复的,唯有redis操作那块不同,代码重复度特别高,一旦扩展新方法,基本上是剖解原有代码,然后拷贝现有方法,最后改成新方法。

    3. 当前方法,包含的都是redis单操作,如果遇到那种涉及到多个操作组合的(比如先set,然后expire或者更复杂一点),需要添加新方法,本质上这种新方法其实和业务性有关了。

    从上面列出的这几点来看,其实我们可以完全将其打造成一个兼容jrc操作和cluster操作,同时具有良好框架扩展性(策略模式+模板模式)和良好代码重复度控制(函数式接口回调)的框架。由于本章涉及内容为异步回调,所以这里我们将讲解这种代码如何保持良好的代码重复度控制上。至于良好的框架扩展性,如果感兴趣的话,我会在后面的章节进行讲解。那么我们开始进行优化吧。

    首先,找出公共操作部分(白色)和非公共操作部分(黄色):

    /**
         * key递增特定的num
         * @param key Redis中保存的key
         * @param num 递增的值
         * @return key计算完毕后返回的结果值
         */
        public Long incrBy(String key,long num) {
            CallerInfo callerInfo = Profiler.registerInfo("sendCouponService.redis.incrBy", "sendCouponService", false, true);
            Long result;
            try {
                result = jrc.incrBy(key, num);
            } catch (Exception e) {
                logger.error("incrBy", null, "sendCouponService.redis.incrBy异常,key={},value={}", e, key, num);
                Profiler.functionError(callerInfo);
                return null;
            } finally {
                Profiler.registerInfoEnd(callerInfo);
            }
            return result;
        }

    通过上面的标记,我们发现非公共操作部分,有两类:

    1. ump提示语和日志提示语不一致

    2. 操作方法不一致

    标记出来了公共操作部分,之后我们开始封装公共部分:

    /**
         * 公共模板抽取
         *
         * @param method
         * @param callable
         * @param <T>
         * @return
         */
        public static <T> T invoke(String method) {
            CallerInfo info = Profiler.registerInfo(method, false, true);
            try {
                //TODO 这里放置不同的redis操作方法
            } catch (Exception e) {
                logger.error(method, e);
                AlarmUtil.alarm(method + e.getCause());
                reutrn null;
            } finally {
                Profiler.registerInfoEnd(info);
            }
        }

    但是这里有个问题,我们虽然把公共模板抽取出来了,但是TODO标签里面的内容怎么办呢? 如何把不同的redis操作方法传递进来呢?

    其实在java中,我们可以利用接口的方式,将具体的操作代理出去,由外部调用者来实现,听起来是不是感觉又和IOC搭上了点关系,不错,你想的没错,这确实是控制反转依赖注入的一种做法,通过接口方式将具体的实践代理出去,这也是进行回调操作的原理。接下来看我们的改造:

        /**
         * redis操作接口
         */
        public interface RedisOperation<T>{
            //调用redis方法,入参为空,出参为T泛型
            T invoke();
        }
    
        /**
         *  redis操作公共模板
         * @param method
         * @param  redisOperation
         * @param <T>
         * @return
         */
        public static <T> T invoke(String method,RedisOperation redisOperation) {
            CallerInfo info = Profiler.registerInfo(method, false, true);
            try {
               return  redisOperation.invoke();
            } catch (Exception e) {
                logger.error(method, e);
                AlarmUtil.alarm(method + e.getCause());
                reutrn null;
            } finally {
                Profiler.registerInfoEnd(info);
            }
        }

    这样,我们就打造好了一个公共的redis操作模板,之后就可以像下面的方式来使用了:

        @Override
        public Long incrby(String key, long val){
            String method = "com.jd.marketing.util.RedisUtil.incrby";
            RedisOperation<Long> process = () -> {
                return redisUtils.incrBy(key, val);
            };
            return CacheHelper.invoke(method, process);
        }

    之后的一百多个方法,你也可以使用这样的方式来一一进行包装,之后你会发现原来RedisUtils封装完毕,代码写了2000行,但是用这种方式之后,代码只写了1000行,而且后续有新的联合操作过来,你只需要在如下代码段里面直接把级联操作添加进去即可:

     RedisOperation<Long> process = () -> {
               //TODO other methods
               //TODO other methods
                return redisUtils.incrBy(key, val);
    };
    

    是不是很方便快捷?在这里我需要所以下的是,由于RedisOperation里面的invoke方法是没有入参,带有一个出参结果的调用。所以在回调这里,我用了匿名表达式来()->{}来match这种操作。但是如果回调这里,一个入参,一个出参的话,那么我的匿名表达式需要这样写 param->{}, 多个入参,那就变成了这样 (param1, param2, param3)->{} 。由于这里并非重点,我不想过多讲解,如果对这种使用方式不熟悉,可以完全使用如下的方式来进行书写也行:

     @Override
        public Long incrby(String key, long val){
            String method = "com.jd.marketing.util.RedisUtil.incrby";
            RedisOperation<Long> process = () -> incrByOperation(key, val);
            return CacheHelper.invoke(method, process);
        }
       
       private Long incrByOperation(String key, long val){
           return redisUtils.incrBy(key, val);
       }

    其实说到这里的时候,我就有必要提一下开头的埋下的线索了。其实之前演示的Netty的代码:

    //Netty中设置监听
    ChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener)
    

    GenericFutureListener这个接口

    就是按照上面的写法来做的,是不是豁然开朗呢?至于其调用方式,也和上面讲解的一致,只要符合接口里面方法的调用标准就行(入参和出参符合就行), 比如 future –> {}。

    说到这里,我们可能认为这样太麻烦了,自己定义接口,然后注入到框架中,最后用户自己实现调用方法,一长串。是的,你说的没错,这样确实太麻烦了,JDK于是专门用了一个 FunctionalInterface的annotation来帮我们做了,所以在JDK中,如果你看到Consumer,Function,Supplier等,带有@FunctionalInterface标注的接口,那么就说明他是一个函数式接口,而这种接口是干什么的,具体的原理就是我上面讲的。下面我们来梳理梳理这些接口吧。

    先看一下我们的RedisUtils使用JDK自带的函数式接口的最终封装效果:

    image

    从图示代码可以看出,整体封装变得简洁许多,而且我们用了JDK内置的函数式接口,所以也无需写其他多余的代码,看上去很清爽,重复代码基本不见了。而且,由于JDK提供的其他的函数式接口有运算操作,比如Predicate.or, Predicate.and操作等,大大加强了封装的趣味性和乐趣。

    下面我将JDK中涉及的常用的函数式接口列举一遍,然后来详细讲解讲解吧,列表如下:

    Consumer, 提供void accept(T t)回调
    
    Runnable, 提供void run()回调
    
    Callable, 提供V call() throws Exception回调
    
    Supplier, 提供T get()回调
    
    Function, 提供R apply(T t)回调, 有andThen接续操作
    
    Predicate, 提供boolean test(T t)回调, 等价于 Function<T, boolean>
    
    BiConsumer, 提供void accept(T t, U u)回调,注意带Bi的回调接口,表明入参都是双参数,比如BiPredicate    
    
    ......


    其实还有很多,我这里就不一一列举了。感兴趣的朋友可以在这里找到JDK提供的所有函数式接口

    接下来,我们来讲解其使用示范,以便于明白怎么样去使用它。

    对于Consumer函数式接口,内部的void accept(T t)回调方法,表明了它只能回调有一个入参,没有返参的方法。示例如下:

    /**
         * Consumer调用的例子
         */
        public void ConsumerSample() {
            LinkedHashMap linkedHashMap = new LinkedHashMap();
            linkedHashMap.put("key", "val");
            linkedHashMap.forEach((k, v) -> {
                System.out.println("key" + k + ",val" + v);
            });
        }

    对于Callable接口,其实和Supplier接口是一样的,只是有无Exception抛出的区别,示例如下:

     /**
         * Callable调用的例子
         */
        public Boolean setnx(String key, String val){
            String method = "com.jd.marketing.util.RedisUtil.setnx";
            Callable<Boolean> process = () -> {
                Long rst = redisUtils.setnx(key, val);
                if(rst == null || rst == 0){
                    return false;
                }
                return true;
            };
            return CacheHelper.invoke(method, process);
        }

    对于Predicate<T>接口,等价于Function<T, Boolean>, 示例如下:

     /**
         * Precidate调用的例子
         */
        public void PredicateSample() {
            List list = new ArrayList();
            list.add("a")
            list.removeIf(item -> {
                return item.equals("a");
            });
        }

    说明一下,Predicate的入参为一个参数,出参为boolean,很适合进行条件判断的场合。在JDK的List数据结构中,由于removeIf方法无法耦合进去业务代码,所以利用Predicate函数式接口将业务逻辑实现部分抛给了用户自行处理,用户处理完毕,只要返回给我true,我就删掉当前的item;返回给我false,我就保留当前的item。解耦做的非常漂亮。那么List的removeIf实现方式你觉得是怎样实现的呢?如果我不看JDK代码的话,我觉得实现方式如下:

     public boolean removeIf(Predicate<T> predicate){
            final Iterator<T> iterator = getIterator();
            while (iterator.hasNext()) {
                T current = iterator.next();
                boolean result = predicate.test(current);
                if(result){
                    iterator.remove();
                    return true;
                }
            }
            return false;
        }

    但是实际你去看看List默认的removeIf实现,源码大概和我写的差不多。所以只要理解了函数式接口,我们也能写出JDK Style的代码,酷吧。

    CompletableFuture实现异步处理

    好了,上面就是函数式接口的整体介绍和使用简介,不知道你看了之后,理解了多少呢?接下来我们要讲解的异步,完全基于上面的函数式接口回调,如果之前的都看懂了,下面的讲解你将豁然开朗;反之则要悟了。但是正确的方向都已经指出来了,所以入门应该是没有难度的。

    CompletableFuture,很长的一个名字,我对他的印象停留在一次代码评审会上,当时有人提到了这个类,我只是简单的记录下来了,之后去JDK源码中搜索了一下,看看主要干什么的,也没有怎么想去看它。结果当我搜到这个类,然后看到Author的时候,我觉得我发现了金矿一样,于是我决定深入的研究下去,那个作者的名字就是:

    /**
     * A {@link Future} that may be explicitly completed (setting its
     * value and status), and may be used as a {@link CompletionStage},
     * supporting dependent functions and actions that trigger upon its
     * completion.
     *
     * <p>When two or more threads attempt to
     * {@link #complete complete},
     * {@link #completeExceptionally completeExceptionally}, or
     * {@link #cancel cancel}
     * a CompletableFuture, only one of them succeeds.
     *
     * <p>In addition to these and related methods for directly
     * manipulating status and results, CompletableFuture implements
     * interface {@link CompletionStage} with the following policies: <ul>
     *
     * <li>Actions supplied for dependent completions of
     * <em>non-async</em> methods may be performed by the thread that
     * completes the current CompletableFuture, or by any other caller of
     * a completion method.</li>
     *
     * <li>All <em>async</em> methods without an explicit Executor
     * argument are performed using the {@link ForkJoinPool#commonPool()}
     * (unless it does not support a parallelism level of at least two, in
     * which case, a new Thread is created to run each task).  To simplify
     * monitoring, debugging, and tracking, all generated asynchronous
     * tasks are instances of the marker interface {@link
     * AsynchronousCompletionTask}. </li>
     *
     * <li>All CompletionStage methods are implemented independently of
     * other public methods, so the behavior of one method is not impacted
     * by overrides of others in subclasses.  </li> </ul>
     *
     * <p>CompletableFuture also implements {@link Future} with the following
     * policies: <ul>
     *
     * <li>Since (unlike {@link FutureTask}) this class has no direct
     * control over the computation that causes it to be completed,
     * cancellation is treated as just another form of exceptional
     * completion.  Method {@link #cancel cancel} has the same effect as
     * {@code completeExceptionally(new CancellationException())}. Method
     * {@link #isCompletedExceptionally} can be used to determine if a
     * CompletableFuture completed in any exceptional fashion.</li>
     *
     * <li>In case of exceptional completion with a CompletionException,
     * methods {@link #get()} and {@link #get(long, TimeUnit)} throw an
     * {@link ExecutionException} with the same cause as held in the
     * corresponding CompletionException.  To simplify usage in most
     * contexts, this class also defines methods {@link #join()} and
     * {@link #getNow} that instead throw the CompletionException directly
     * in these cases.</li> </ul>
     *
     * @author Doug Lea
     * @since 1.8
     */

    Doug Lea,Java并发编程的大神级人物,整个JDK里面的并发编程包,几乎都是他的作品,很务实的一个老爷子,目前在纽约州立大学奥斯威戈分校执教。比如我们异常熟悉的AtomicInteger类也是其作品:

    /**
     * An {@code int} value that may be updated atomically.  See the
     * {@link java.util.concurrent.atomic} package specification for
     * description of the properties of atomic variables. An
     * {@code AtomicInteger} is used in applications such as atomically
     * incremented counters, and cannot be used as a replacement for an
     * {@link java.lang.Integer}. However, this class does extend
     * {@code Number} to allow uniform access by tools and utilities that
     * deal with numerically-based classes.
     *
     * @since 1.5
     * @author Doug Lea
    */
    public class AtomicInteger extends Number implements java.io.Serializable {
      // ignore code 
    }

    想查阅老爷子的最新资料,建议到Wikipedia上查找,里面有他的博客链接等,我这里就不再做过多介绍,回到正题上来,我们继续谈谈CompletableFuture吧。我刚才贴的关于这个类的描述,都是英文的,而且特别长,我们不妨贴出中文释义来,看看具体是个什么玩意儿:

     继承自Future,带有明确的结束标记;同时继承自CompletionStage,支持多函数调用行为直至完成态。
     当两个以上的线程对CompletableFuture进行complete调用,completeExceptionally调用或者cancel调用,只有一个会成功。
     
     为了直观的保持相关方法的状态和结果,CompletableFuture按照如下原则继承并实现了CompletionStage接口:
    
     1. 多个同步方法的级联调用,可能会被当前的CompletableFuture置为完成态,也可能会被级联函数中的任何一个方法置为完成态。
    
     2. 异步方法的执行,默认使用ForkJoinPool来进行(如果当前的并行标记不支持多并发,那么将会为每个任务开启一个新的线程来进行)。
        为了简化监控,调试,代码跟踪等,所有的异步任务必须继承自AsynchronousCompletionTask。
    
     3. 所有的CompletionStage方法都是独立的,overrid子类中的其他的方法并不会影响当前方法行为。
    
     CompletableFuture同时也按照如下原则继承并实现了Future接口:
    
     1. 由于此类无法控制完成态(一旦完成,直接返回给调用方),所以cancellation被当做是另一种带有异常的完成状态. 在这种情况下cancel方法和CancellationException是等价的。
        方法isCompletedExceptionally可以用来监控CompletableFuture在一些异常调用的场景下是否完成。
    
     2. get方法和get(long, TimeUint)方法将会抛出ExecutionException异常,一旦计算过程中有CompletionException的话。
        为了简化使用,这个类同时也定义了join()方法和getNow()方法来避免CompletionException的抛出(在CompletionException抛出之前就返回了结果)。
     

    由于没有找到中文文档,所以这里自行勉强解释了一番,有些差强人意。

    在我们日常生活中,我们的很多行为其实都是要么有结果的,要么无结果的。比如说做蛋糕,做出来的蛋糕就是结果,那么一般我们用Callable或者Supplier来代表这个行为,因为这两个函数式接口的执行,是需要有返回结果的。再比如说吃蛋糕,吃蛋糕这个行为,是无结果的。因为他仅仅代表我们去干了一件事儿,所以会用Consumer或者Runnable来代表吃饭这个行为。因为这两个函数式接口的执行,是不返回结果的。有时候我发现家里没有做蛋糕的工具,于是我便去外面的蛋糕店委托蛋糕师傅给我做一个,那么这种委托行为,其实就是一种异步行为,会用Future来描述。因为Future神奇的地方在于,可以让一个同步执行的方法编程异步的,就好似委托蛋糕师傅做蛋糕一样。这样我们就可以在蛋糕师傅给我们做蛋糕期间去做一些其他的事儿,比如听音乐等等。但是由于Future不具有事件完成告知的能力,所以得需要自己去一遍一遍的问师傅,做好了没有。而CompletableFuture则具有这种能力,所以总结起来如下:

    • Callable,有结果的同步行为,比如做蛋糕
    • Runnable,无结果的同步行为,比如吃蛋糕
    • Future,异步封装Callable/Runnable,比如委托给蛋糕师傅(其他线程)去做
    • CompletableFuture,封装Future,使其拥有回调功能,比如让师傅主动告诉我蛋糕做好了

    那么上面描述的场景,我们用代码封装一下吧:

     public static void main(String... args) throws Exception {
            CompletableFuture
                    .supplyAsync(() -> makeCake())
                    .thenAccept(cake -> eatCake(cake));
            System.out.println("先回家听音乐,蛋糕做好后给我打电话,我来取...");
            Thread.currentThread().join();
        }
    
        private static Cake makeCake() {
            System.out.println("我是蛋糕房,开始为你制作蛋糕...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Cake cake = new Cake();
            cake.setName("草莓蛋糕");
            cake.setShape("圆形");
            cake.setPrice(new BigDecimal(99));
            System.out.println("蛋糕制作完毕,请取回...");
            return cake;
        }
    
        private static void eatCake(Cake cake) {
            System.out.println("这个蛋糕是" + cake.getName() + ",我喜欢,开吃...");
        }

    最后执行结果如下:

    我是蛋糕房,开始为你制作蛋糕...
    先回家听音乐,蛋糕做好后给我打电话,我来取...
    蛋糕制作完毕,请取回...
    这个蛋糕是草莓蛋糕,我喜欢,开吃...
    

    由于CompletableFuture的api有50几个,数量非常多,我们可以先将其划分为若干大类(摘自理解CompletableFuture,总结的非常好,直接拿来用):

    创建类:用于CompletableFuture对象创建,比如:

    • completedFuture
    • runAsync
    • supplyAsync
    • anyOf
    • allOf

    状态取值类:用于判断当前状态和同步等待取值,比如:

    • join
    • get
    • getNow
    • isCancelled
    • isCompletedExceptionally
    • isDone

    控制类:可用于主动控制CompletableFuture完成行为,比如:

    • complete
    • completeExceptionally
    • cancel

    接续类:CompletableFuture最重要的特性,用于注入回调行为,比如:

    • thenApply, thenApplyAsync
    • thenAccept, thenAcceptAsync
    • thenRun, thenRunAsync
    • thenCombine, thenCombineAsync
    • thenAcceptBoth, thenAcceptBothAsync
    • runAfterBoth, runAfterBothAsync
    • applyToEither, applyToEitherAsync
    • acceptEither, acceptEitherAsync
    • runAfterEither, runAfterEitherAsync
    • thenCompose, thenComposeAsync
    • whenComplete, whenCompleteAsync
    • handle, handleAsync
    • exceptionally

    上面的方法非常多,而大多具有相似性,我们大可不必马上记忆。先来看看几个一般性的规律,便可辅助记忆(重要):

    1. 以Async后缀结尾的方法,均是异步方法,对应无Async则是同步方法。
    2. 以Async后缀结尾的方法,一定有两个重载方法。其一是采用内部forkjoin线程池执行异步,其二是指定一个Executor去运行。
    3. 以run开头的方法,其方法入参的lambda表达式一定是无参数,并且无返回值的,其实就是指定Runnable
    4. 以supply开头的方法,其方法入参的lambda表达式一定是无参数,并且有返回值,其实就是指Supplier
    5. 以Accept为开头或结尾的方法,其方法入参的lambda表达式一定是有参数,但是无返回值,其实就是指Consumer
    6. 以Apply为开头或者结尾的方法,其方法入参的lambda表达式一定是有参数,但是有返回值,其实就是指Function
    7. 带有either后缀的表示谁先完成则消费谁。

    以上6条记住之后,就可以记住60%以上的API了。

    先来看一下其具体的使用方式吧(网上有个外国人写了CompletableFuture的20个例子,我看有中文版了,放到这里,大家可以参考下)。

      /**
         * CompletableFuture调用completedFuture方法,表明执行完毕
         */
        static void sample1() {
            CompletableFuture cf = CompletableFuture.completedFuture("message");
            Assert.assertTrue(cf.isDone());
            Assert.assertEquals("message", cf.getNow(null));
        }

    sample1代码,可以看出,如果想让一个ComopletableFuture执行完毕,最简单的方式就是调用其completedFuture方法即可。之后就可以用getNow对其结果进行获取,如果获取不到就返回默认值null。

     /**
         * 两个方法串行执行,后一个方法依赖前一个方法的返回
         */
        static void sample2() {
            CompletableFuture cf = CompletableFuture
                    .completedFuture("message")
                    .thenApply(message -> {
                        Assert.assertFalse(Thread.currentThread().isDaemon());
                        return message.toUpperCase();
                    });
            Assert.assertEquals("MESSAGE", cf.getNow(null));
        }

    sample2代码,利用thenApply实现两个函数串行执行,后一个函数的执行以来前一个函数的返回结果。

    /**
         * 两个方法并行执行,两个都执行完毕后,在进行汇总
         */
        static void sample3() {
    
            long start = System.currentTimeMillis();
    
            CompletableFuture cf = CompletableFuture.runAsync(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            CompletableFuture cf1 = CompletableFuture.runAsync(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            CompletableFuture.allOf(cf, cf1).whenComplete((v,t)->{
                System.out.println("都完成了");
            }).join();
            long end = System.currentTimeMillis();
            System.out.println((end-start));
        }

    sample3最后的执行结果为:

    都完成了
    2087

    可以看到,耗时为2087毫秒,如果是串行执行,需要耗时3000毫秒,但是并行执行,则以最长执行时间为准,其实这个特性在进行远程RPC/HTTP服务调用的时候,将会非常有用,我们一会儿再进行讲解如何用它来反哺业务。

     /**
         * 方法执行取消
         */
        static void sample4(){
            CompletableFuture cf = CompletableFuture.supplyAsync(()->{
                try {
                    System.out.println("开始执行函数...");
                    Thread.sleep(2000);
                    System.out.println("执行函数完毕...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "ok";
            });
            CompletableFuture cf2 = cf.exceptionally(throwable -> {
                return throwable;
            });
            cf2.cancel(true);
            if(cf2.isCompletedExceptionally()){
                System.out.println("成功取消了函数的执行");
            }
            cf2.join();
    
        }
    

    调用结果如下:

    开始执行函数...
    成功取消了函数的执行
    Exception in thread "main" java.util.concurrent.CancellationException
        at java.util.concurrent.CompletableFuture.cancel(CompletableFuture.java:2263)
        at com.jd.jsmartredis.article.Article.sample4(Article.java:108)
        at com.jd.jsmartredis.article.Article.main(Article.java:132)

    可以看到我们成功的将函数执行中断,同时由于cf2返回的会一个throwable的Exception,所以我们的console界面将其也原封不动的打印了出来。

    讲解了基本使用之后,如何使用其来反哺我们的业务呢?我们就以通用下单为例吧,来看看通用下单有哪些可以优化的点。

    image

    上图就是我们在通用下单接口经常调用的接口,分为下单地址接口,商品信息接口,京豆接口,由于这三个接口没有依赖关系,所以可以并行的来执行。如果换做是目前的做法,那么肯定是顺序执行,假如三个接口获取都耗时1s的话,那么三个接口获取完毕,我们的耗时为3s。但是如果改成异步方式执行的话,那么将会简单很多,接下来,我们开始改造吧。

    public Result submitOrder(String pin, CartVO cartVO) {
    
            //获取下单地址
            CompletableFuture addressFuture = CompletableFuture.supplyAsync(() -> {
                AddressResult addressResult = addressRPC.getAddressListByPin(pin);
                return addressResult;
            });
    
            //获取商品信息
            CompletableFuture goodsFuture = CompletableFuture.supplyAsync(() -> {
                GoodsResult goodsResult = goodsRPC.getGoodsInfoByPin(pin, cartVO);
                return goodsResult;
            });
    
            //获取京豆信息
            CompletableFuture beanFuture = CompletableFuture.supplyAsync(() -> {
                JinbeanResult jinbeanResult = JinbeanRPC.getJinbeanByPin(pin);
                return jinbeanResult;
            });
    
            CompletableFuture.allOf(addressFuture, goodsFuture, beanFuture).whenComplete((v, throwable) -> {
                if (throwable == null) {
                    logger.error("获取地址,商品,京豆信息失败", throwable);
                    //TODO 尝试重新获取
                } else {
                    logger.error("获取地址,商品,京豆信息成功");
                }
            }).join();
    
            AddressResult addressResult = addressFuture.getNow(null);
            GoodsResult goodsResult = goodsFuture.getNow(null);
            JinbeanResult jinbeanResult = beanFuture.getNow(null);
            
            //TODO 后续处理
        }

    这样,我们利用将普通的RPC执行编程了异步,而且附带了强大的错误处理,是不是很简单?

    但是如果遇到如下图示的调用结构,CompletableFuture能否很轻松的应对呢?

    image

    由于业务变更,需要附带延保信息,为了后续重新计算价格,所以必须将延保商品获取出来,然后计算价格。其实这种既有同步,又有异步的做法,利用CompletableFuture来handle,也是轻松自然,代码如下:

     public Result submitOrder(String pin, CartVO cartVO) {
    
            //获取下单地址
            CompletableFuture addressFuture = CompletableFuture.supplyAsync(() -> {
                AddressResult addressResult = addressRPC.getAddressListByPin(pin);
                return addressResult;
            });
    
            //获取商品信息
            CompletableFuture goodsFuture = CompletableFuture.supplyAsync(() -> {
                GoodsResult goodsResult = goodsRPC.getGoodsInfoByPin(pin, cartVO);
                return goodsResult;
            }).thenApplyAsync((goodsResult, Map)->{
                YanbaoResult yanbaoResult = yanbaoRPC.getYanbaoInfoByGoodID(goodsResult.getGoodId, pin);
                Map<String, Object> map = new HashMap<>();
                map.put("good", goodsResult);
                map.put("yanbao",yanbaoResult);
                return map;
            });
    
            //获取京豆信息
            CompletableFuture beanFuture = CompletableFuture.supplyAsync(() -> {
                JinbeanResult jinbeanResult = JinbeanRPC.getJinbeanByPin(pin);
                return jinbeanResult;
            });
    
            CompletableFuture.allOf(addressFuture, goodsFuture, beanFuture).whenComplete((v, throwable) -> {
                if (throwable == null) {
                    logger.error("获取地址,商品-延保,京豆信息失败", throwable);
                    //TODO 尝试重新获取
                } else {
                    logger.error("获取地址,商品-延保,京豆信息成功");
                }
            }).join();
    
            AddressResult addressResult = addressFuture.getNow(null);
            GoodsResult goodsResult = goodsFuture.getNow(null);
            JinbeanResult jinbeanResult = beanFuture.getNow(null);
    
            //TODO 后续处理
        }

    这样我们就可以了,当然这种改造给我们带来的好处也是显而易见的,我们不需要针对所有的接口进行OPS优化,而是针对性能最差的接口进行OPS优化,只要提升了性能最差的接口,那么整体的性能就上去了。

    洋洋洒洒写了这么多,希望对大家有用,谢谢。

    参考资料:

    理解CompletableFuture

    CompletableFuture 详解

    JDK中CompletableFuture的源码

  • 相关阅读:
    oracle 随机函数
    cmd和dos的区别
    SAP HANA 常用函数
    编程语言发展史
    PL/SQL Job
    ETL工具总结
    JDK、JRE、JVM三者间的关系
    Redis过滤器如何与Envoy代理一起使用
    apache配置https
    kubernetes监控和性能分析工具:heapster+influxdb+grafana
  • 原文地址:https://www.cnblogs.com/scy251147/p/10197076.html
Copyright © 2020-2023  润新知