全局异常处理--异步线程中的异常捕获

/ 默认分类 / 0 条评论 / 842浏览

全局异常处理--异步线程中的异常捕获

spring提供了方便的全局异常处理,@ControllerAdvice,@ExceptionHandler,关于他们的使用这里不做过多的介绍,这里我们讨论的问题是,再使用异步线程执行的时候,如果再异步的方法中抛出了异常,此时,ControllerAdvice是无法捕获异常的,此时我们可以使用下面方法解决。

<一>

    @GetMapping("/hello")
    public R<String> test() {
        String resp = testService.getData();
        return R.ok().data(resp);
    }

对于上面的接口,同步调用,如果再getData中出现异常时可以正常被ControllerAdvice捕获的

<二>

但是如果这样:

    @GetMapping("/hello")
    public R<String> test() {
        new Thread(() -> {String resp = testService.getData();}).start();
        return R.ok().data(resp);
    }
<三>

这样的话,因为异步调用,并且没有返回值,再ControllerAdvice中是无法捕获的,因为已经直接被Controller返回了

但是我们可以看到,这里我使用的是Runable,这样是没有实现我们第一个的需求的,因为我们需要获取线程执行的返回值,所以我们可以这样修改:

    @GetMapping("/hello")
    public R<String> test() {
        Callable<String> callable = () -> testService.getData();
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        new Thread(futureTask).start();
        return R.ok().data(futureTask.get());
    }

这样,如果在testService.getData中出现异常了是可以被ControllerAdvice捕获到的,使用过java中的多线程编程的小伙伴应该都知道,其实上面的这个接口实际上还是一个同步的接口,因为这里我们需要获取打破getData的返回值,在需要获取返回值的时候,controller方法的线程,如果想返回return R.ok().data(futureTask.get());就必须要等待getData方法所在的线程执行结束,这样才能获取到返回值,所以这里是可以被ControllerAdvice捕获到异常的(因为Controller方法还没有返回)

<四>

但是像上面第一种的方式,假如就是不需要返回值的异步操作,那这样我们该如何捕获异常,首先最简单的方式就是使用trycatch捕获,但是这样需要在每一个异步调用中增加代码,于是我们可以想到直接在线程池中设置,说到线程池,首先我们上面的这个test接口对于简单异步的使用是不被推荐使用的,下面是阿里巴巴Java开发手册的一个说明:

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

下面我们来说说线程池的方式:


springboot默认异步支持

默认使用ThreadPoolTaskExecutor,可以看下自动配置类:

TaskExecutionAutoConfiguration

读取的配置文件如下: TaskExecutionProperties 查看到下面的配置数据:

public static class Pool {

		/**
		 * Queue capacity. An unbounded capacity does not increase the pool and therefore
		 * ignores the "max-size" property.
		 */
		private int queueCapacity = Integer.MAX_VALUE;

		/**
		 * Core number of threads.
		 */
		private int coreSize = 8;

		/**
		 * Maximum allowed number of threads. If tasks are filling up the queue, the pool
		 * can expand up to that size to accommodate the load. Ignored if the queue is
		 * unbounded.
		 */
		private int maxSize = Integer.MAX_VALUE;

		/**
		 * 是否开启线程超时
		 * Whether core threads are allowed to time out. This enables dynamic growing and
		 * shrinking of the pool.
		 */
		private boolean allowCoreThreadTimeout = true;

		/**
		 * 线程保持空闲不被终止的最大时间,类似与数据源连接池中的minEvictableIdleTimeMillis
		 * Time limit for which threads may remain idle before being terminated.
		 */
		private Duration keepAlive = Duration.ofSeconds(60);

所以相比于上面手动创建线程,下面的方法是直接使用springboot中默认配置的线程池来执行

  1. 在启动类或者当前Controller类添加注解@EnableAsync
  2. 在getData方法上添加@Async注解表示该方法为异步执行
  3. 在Controller中调用该方法

这里需要注意的是,上面执行的线程是没有返回值,如果需要使用有返回值的,spring也提供了支持:

    @Async
    public Future<String> getData() {
            String result = "test";
            return new AsyncResult<>(result);
    }

这样在controller调用的时候就可以通过future的get方法获取回调得到的线程执行结果


上面介绍的是使用sprinboot中默认的线程池来执行异步逻辑 说到这里,我们还是要来看下springboot中的线程池的自动配置类:

//只有在容器中存在ThreadPoolTaskExecutor的时候才会注册TaskExecutionAutoConfiguration
@ConditionalOnClass(ThreadPoolTaskExecutor.class)
@Configuration(proxyBeanMethods = false)
//这个就不多介绍了,springboot默认大于配置的精髓,支持用户自定义配置的主要设计方式
@EnableConfigurationProperties(TaskExecutionProperties.class)
public class TaskExecutionAutoConfiguration {

	/**
	 * Bean name of the application {@link TaskExecutor}.
	 */
	public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

	@Bean
	@ConditionalOnMissingBean
	public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
			ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers,
			ObjectProvider<TaskDecorator> taskDecorator) {
		TaskExecutionProperties.Pool pool = properties.getPool();
		TaskExecutorBuilder builder = new TaskExecutorBuilder();
		builder = builder.queueCapacity(pool.getQueueCapacity());
		builder = builder.corePoolSize(pool.getCoreSize());
		builder = builder.maxPoolSize(pool.getMaxSize());
		builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
		builder = builder.keepAlive(pool.getKeepAlive());
		Shutdown shutdown = properties.getShutdown();
		builder = builder.awaitTermination(shutdown.isAwaitTermination());
		builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
		builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
		builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
		builder = builder.taskDecorator(taskDecorator.getIfUnique());
		return builder;
	}

	@Lazy
	@Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME,
			AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
	//这里可以看出,我们是可以自定义线程池的
	@ConditionalOnMissingBean(Executor.class)
	public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
		return builder.build();
	}

}

所以按照springboot设计的一贯方式,这里我们就可以自定义线程池来使用,在容器中注入即可