spring执行异步任务,含子线程异常捕获、上下文传递、请求阻塞

1. 创建线程池

  1. 打开spring异步开关,在启动类上加@EnableAsync注解;
  2. 创建线程池组件:
@Configuration
public class ExecutorConfig {

	// 优先取配置文件中executor.thread.core_pool_size属性的值,不存在则默认20
	@Value("${executor.thread.core_pool_size:20}")
	private int corePoolSize;
	@Value("${executor.thread.max_pool_size:500}")
	private int maxPoolSize;
	@Value("${executor.thread.queue_capacity:100}")
	private int queueCapacity;
	@Value("${executor.thread.name.prefix:executor}")
	private String namePrefix;
	@Value("${executor.thread.keep.alive.seconds:60}")
	private int keepAliveSeconds;

	@Bean(name = "asyncServiceExecutor")
	public Executor asyncServiceExecutor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		// 线程池各个参数含义自行百度
		executor.setCorePoolSize(corePoolSize);
		executor.setMaxPoolSize(maxPoolSize);
		executor.setQueueCapacity(queueCapacity);
		executor.setThreadNamePrefix(namePrefix);
		executor.setKeepAliveSeconds(keepAliveSeconds);
		// 配置传递上下文信息的装饰器,后面详细解释
		executor.setTaskDecorator(new WebContextTaskDecorator());
		executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
		// 执行初始化
		executor.initialize();
		return executor;
	}
}

2. 控制层

@RestController
public class DoController {
    @Autowired
    private DoService service;
    
    @RequestMapping(value = "/doBatch", method = RequestMethod.POST)
    public JsonResponse<ResVO> doBatch(@RequestBody ReqVO reqVO) {
        ResVO resVO = service.doBatch(reqVO);
        return JsonResponse.success(resVO);
    }
}

3. 服务层

控制层直接调用的服务类:

@Service
public class DoServiceImpl implements DoService {
    @Autowired
    private DoServiceByMultiThread doServiceByMultiThread ;
    
    @Override
    public ResVO doBatch(ReqVO reqVO) {
    	// length为需要执行异步任务的总数,比如10笔下单等
    	int length = 10;
    	// 同步锁,用于阻塞当前请求线程直到异步线程执行完毕
    	CountDownLatch lock = new CountDownLatch(length);
    	// 将结果类传进去用于保存业务正常执行后的结果与线程异常信息
    	ResVO resVO = new ResVO();
    	// 使用线程安全的容器
    	resVO.setSuccessList(Collections.synchronizedList(new ArrayList<>()));
        resVO.setErrorList(Collections.synchronizedList(new ArrayList<>()));
        for (int i = 0; i < length; i++) {
    		doServiceByMultiThread.service(resVO, lock);
    	}
    	try {
            // 阻塞请求线程直到异步线程全部完成
            lock.await();
        } catch (InterruptedException e) {
            LogUtil.info("多线程异常");
        }
	}    
}

结果类ResVO:

public class ResVO {
    // 操作成功列表
    List<SingleResult> successList;
    // 操作失败列表
    List<SingleResult> errorList;
    
    public static class SingleResult {
    	// 操作序列号等其他业务需要的信息
    	private String serialNo;
    	// 错误原因,仅线程抛异常时存在
    	private String reason;
    }
}

执行异步操作的服务:

@Component
public class DoServiceByMultiThread {
	// @Async注解的方法将被spring代理后异步执行,可通过参数指定使用的线程池
	// 该方法不能与调用方位于同一个类中,且返回值只能为void或FutureTask
	@Async("asyncServiceExecutor")
	public void service(ResVO resVO, CountDownLatch lock){
		SingleResult result = new SingleResult();
		try {
			// 获取上下文信息,如果上下文使用的是ThreadLocal,可能会有空指针异常
			// 因为这里的线程并非请求线程,父线程的上下文会丢失,通过后面的TaskDecorator解决该问题
			WebContext webContext = WebContext.getWebContext();
			
			// 可能会抛出异常的业务操作......

			LogUtil.info("编号xxx的异步任务执行成功");
			result.getSuccessList.add(result);
		}
		catch (Throwable e) {
            // 捕获子线程异常并打印,必须使用日志工具类中带异常对象的error方法,否则无法打印完整堆栈信息
            LogUtil.error(e, "编号xxx的异步任务执行失败,错误原因:");
            result.setReason(e.getMessage() == null ? "系统内部错误" : e.getMessage());
            result.getErrorList.add(result);
        } finally {
            // 异步任务完成数+1,必须放在finally块中,否则可能导致请求线程永远阻塞
            lock.countDown();
        }
	}
}

4. 上下文类

// 保存了本次请求与响应的上下文,一般在拦截器中初始化
public class WebContext {
	// 通过ThreadLocal与线程绑定
	private static ThreadLocal<WebContext> localContext = new ThreadLocal();
	private HttpServletRequest request;
	private HttpServletResponse response;

	public WebContext(HttpServletRequest request, HttpServletResponse response) {
		this.request = request;
		this.response = response;
	}

	public HttpServletRequest getRequest() {
		return this.request;
	}

	public HttpServletResponse getResponse() {
		return this.response;
	}

	public static void initWebContext(HttpServletRequest request, HttpServletResponse response) {
		WebContext context = new WebContext(request, response);
		initWebContext(context);
	}

	public static void initWebContext(WebContext webContext) {
		localContext.set(webContext);
	}

	public static WebContext getWebContext() {
		return (WebContext)localContext.get();
	}

	public static void clear() {
		localContext.remove();
	}
}

5. 用于父子线程传递上下文信息的装饰类

// 在初始化线程池前作为参数设置进去,从而让线程池的线程执行任务时可以访问WebContext
public class WebContextTaskDecorator implements TaskDecorator {
    /**
     * @param runnable 执行异步任务的线程池中的线程
     * @return 装饰后的线程(即绑定WebContext后的)
     */
    @Override
    public Runnable decorate(Runnable runnable) {
    	// 此处获取的是与请求线程绑定的WebContext,绑定一般在拦截器中实现
        WebContext webContext = WebContext.getWebContext();
        return () -> {
            try {
                WebContext.initWebContext(webContext);
                runnable.run();
            } finally {
                // 清除当前线程的WebContext
                WebContext.clear();
            }
        };
    }
}

6. 总结

  1. 通过CountDownLatch实现阻塞请求线程直到异步任务执行结束;
  2. 向异步任务传列表(必须是线程安全的容器)入参,统计任务成功与失败条数,并保存失败原因;
  3. 使用TaskDecorator在父子线程中传输上下文信息,让异步任务执行时可以访问类似WebContext的上下文
  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值