代码整洁之道-Spring声明式异步

Posted on 2024-06-24

导语

尽管JDK1.4版本(2002年)就提供了NIO机制用于进行非阻塞的网络读写,但是由于历史惯性或者屏蔽复杂度、降低使用心智,业务迭代过程中重度依赖的核心网络资源如JDBC、Redis、RPC都是以BIO的形式透出API。

因此,各个微服务为提升事务吞吐、降低事务延迟,都不约而同的在项目中维护了大量线程池进行精细化的异步调度。

典型代码

img

@Component
public class AsyncBizService implements InitializingBean, DisposableBean {
    private ExecutorService executorService;

    /**
     * 线程池资源初始化
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        executorService = Executors.newFixedThreadPool(100);
    }

    /**
     * 线程池资源回收
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        executorService.shutdownNow();
    }

    /**
     * 使用线程池进行异步业务处理
     */
    public void doBiz() {
        executorService.execute(() -> System.out.println("进行业务逻辑处理"));
    }
}

如上述,常规的线程池使用pattern包含线程池初始化、线程池销毁、线程池使用。一顿脑补分析可知,上述存在【代码耦合】的问题:线程池的初始化和线程池的使用耦合到一起。不符合软件工程中经典的高内聚、低耦合原则。

优化、演进

演进版本1-职责分离、解除耦合

我们将【线程池声明】和【线程池使用】两块代码按职责拆分成两个组件,以满足低耦合的原则,各司其职,让代码结构更清晰易读。

线程池声明

img

@Component("bizExecutor")
public class BizExecutor implements Executor, InitializingBean, DisposableBean {
    private ExecutorService executorService;

    /**
     * 线程池资源初始化
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        executorService = Executors.newFixedThreadPool(100);
    }

    /**
     * 线程池资源回收
     *
     * @throws Exception
     */
    @Override
    public void destroy() throws Exception {
        executorService.shutdownNow();
    }

    @Override
    public void execute(Runnable command) {
        executorService.execute(command);
    }
}

线程池使用

img

@Component
public class AsyncBizService {
    @Resource(name = "bizExecutor")
    private Executor executor;

    /**
     * 使用线程池进行异步业务处理
     */
    public void doBiz() {
        executor.execute(() -> System.out.println("进行业务逻辑处理"));
    }
}

一顿脑补分析可知,上述存在【样板代码】的问题:

所谓样板代码是说:不论你的具体业务是啥,你都得按这个流程去写这段类似的代码。

  1. 线程池初始化和线程池销毁代码属于【样板代码】,每个线程池的声明都需要做同样的工作。
  2. 线程池的使用也属于【样板代码】,每个业务方法都要显式的转发任务到【线程池】。

演进版本2-去样板代码

我们可以依托Spring的AOP机制和内建对象来进行优化。

线程池声明

img

@Configuration
public class BizExecutorConfig {
    /**
     * org.springframework.boot.task.TaskExecutorBuilder是Spring抽象的线程池Builder.
     * <br>
     * org.springframework.core.task.TaskExecutor具有跟随SpringApplication的生命周期控制.
     *
     * @param taskExecutorBuilder
     * @return
     */
    @Bean("bizExecutor") // 指定beanName,便于业务指定
    public TaskExecutor bizExecutor(TaskExecutorBuilder taskExecutorBuilder) {
        return taskExecutorBuilder.corePoolSize(100).build();
    }
}

线程池使用

img

@Component
public class AsyncBizService {

    /**
     * 使用线程池进行异步业务处理
     */
    @Async("bizExecutor")
    public void doBiz() {
        System.out.println("进行业务逻辑处理");
    }
}

@Async原理可参考【交易平台】浅析Spring中Async注解底层异步线程池原理

如上述,我们去除了线程池声明、业务显式委托的样板代码。但是,一顿脑补分析可知,上述存在【魔法值】的问题:

所谓魔法值是指在代码编写时莫名出现的数字,无法直接判断数值代表的含义,必须通过联系代码上下文分析才可以明白,严重降低了代码的可读性。

@Async注解中指定的线程池BeanName

img

魔法值对【代码重构】极其不友好。业务迭代过程中,声明线程池和使用线程池的人往往不是同一拨人,如果这个【魔法值】不小心写错了、改错了,往往需要在运行期才能暴露,非常影响业务迭代效率

所以我们需要想办法去掉这种使用魔法值来指定线程池的方式,尽可能实现编译期检查。

演进版本3-去魔法值

可以利用【Spring注解组合】机制,将魔法值收口线程池声明处,线程池使用处无需关注具体细节;同时,可以充分利用编译期检查,避免使用时魔法值写错、改错。

线程池声明

img

@Configuration
public class BizExecutorConfig {

    /**
     * org.springframework.boot.task.TaskExecutorBuilder是Spring抽象的线程池Builder.
     * <br>
     * org.springframework.core.task.TaskExecutor具有跟随SpringApplication的生命周期控制.
     *
     * @param taskExecutorBuilder
     * @return
     */
    @Bean(AsyncByBizExecutor1.EXECUTOR_NAME)// 指定Qualifier,便于@Async注解关联
    public TaskExecutor bizExecutor(TaskExecutorBuilder taskExecutorBuilder) {
        return taskExecutorBuilder.corePoolSize(100).build();
    }

    @Async(AsyncByBizExecutor1.EXECUTOR_NAME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AsyncByBizExecutor1 {
        String EXECUTOR_NAME = "bizExecutor";
    }
}

基于@Async注解,组合出一个自定义注解。详细可参考:注解编程 之 注解合并

线程池使用

img

@Component
public class AsyncBizService {

    /**
     * 使用线程池进行异步业务处理
     */
    @BizExecutorConfig.AsyncByBizExecutor1
    public void doBiz() {
        System.out.println("进行业务逻辑处理");
    }
}

如上述,【线程池使用】处,没有任何魔法值。因为利用了编译期符号来实现绑定,所以可以借助现代化的IDE实现【符号跳转】,具有极高的可读性和可重构性。

最佳实践

每个线程池声明一个与其绑定的注解,实现可读、可重构的声明式异步。

聚类声明

img

Biz222Executors

img

Biz333Executors

img

按需使用

Biz222Service

img

Biz333Service

img

总结

代码简洁易读可维护是技术人孜孜不倦的技术追求。业务代码中最常见的异步样板代码其实也可以一步步优化、演进,最终呈现一个简洁的使用界面。