导语
尽管JDK1.4版本(2002年)就提供了NIO机制用于进行非阻塞的网络读写,但是由于历史惯性或者屏蔽复杂度、降低使用心智,业务迭代过程中重度依赖的核心网络资源如JDBC、Redis、RPC都是以BIO的形式透出API。
因此,各个微服务为提升事务吞吐、降低事务延迟,都不约而同的在项目中维护了大量线程池进行精细化的异步调度。
典型代码
@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-职责分离、解除耦合
我们将【线程池声明】和【线程池使用】两块代码按职责拆分成两个组件,以满足低耦合的原则,各司其职,让代码结构更清晰易读。
线程池声明
@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);
}
}
线程池使用
@Component
public class AsyncBizService {
@Resource(name = "bizExecutor")
private Executor executor;
/**
* 使用线程池进行异步业务处理
*/
public void doBiz() {
executor.execute(() -> System.out.println("进行业务逻辑处理"));
}
}
一顿脑补分析可知,上述存在【样板代码】的问题:
所谓样板代码是说:不论你的具体业务是啥,你都得按这个流程去写这段类似的代码。
- 线程池初始化和线程池销毁代码属于【样板代码】,每个线程池的声明都需要做同样的工作。
- 线程池的使用也属于【样板代码】,每个业务方法都要显式的转发任务到【线程池】。
演进版本2-去样板代码
我们可以依托Spring的AOP机制和内建对象来进行优化。
线程池声明
@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();
}
}
线程池使用
@Component
public class AsyncBizService {
/**
* 使用线程池进行异步业务处理
*/
@Async("bizExecutor")
public void doBiz() {
System.out.println("进行业务逻辑处理");
}
}
@Async原理可参考【交易平台】浅析Spring中Async注解底层异步线程池原理
如上述,我们去除了线程池声明、业务显式委托的样板代码。但是,一顿脑补分析可知,上述存在【魔法值】的问题:
所谓魔法值是指在代码编写时莫名出现的数字,无法直接判断数值代表的含义,必须通过联系代码上下文分析才可以明白,严重降低了代码的可读性。
如@Async
注解中指定的线程池BeanName
:
魔法值对【代码重构】极其不友好。业务迭代过程中,声明线程池和使用线程池的人往往不是同一拨人,如果这个【魔法值】不小心写错了、改错了,往往需要在运行期才能暴露,非常影响业务迭代效率。
所以我们需要想办法去掉这种使用魔法值来指定线程池的方式,尽可能实现编译期检查。
演进版本3-去魔法值
可以利用【Spring注解组合】机制,将魔法值收口线程池声明处,线程池使用处无需关注具体细节;同时,可以充分利用编译期检查,避免使用时魔法值写错、改错。
线程池声明
@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注解,组合出一个自定义注解。详细可参考:注解编程 之 注解合并
线程池使用
@Component
public class AsyncBizService {
/**
* 使用线程池进行异步业务处理
*/
@BizExecutorConfig.AsyncByBizExecutor1
public void doBiz() {
System.out.println("进行业务逻辑处理");
}
}
如上述,【线程池使用】处,没有任何魔法值。因为利用了编译期符号来实现绑定,所以可以借助现代化的IDE实现【符号跳转】,具有极高的可读性和可重构性。
最佳实践
每个线程池声明一个与其绑定的注解,实现可读、可重构的声明式异步。
聚类声明
Biz222Executors
Biz333Executors
按需使用
Biz222Service
Biz333Service
总结
代码简洁易读可维护是技术人孜孜不倦的技术追求。业务代码中最常见的异步样板代码其实也可以一步步优化、演进,最终呈现一个简洁的使用界面。