当前位置: 首页 > news >正文

Async 注解原理分析

@Async 注解由 Spring 框架提供,被该注解标注的类或方法会在 异步线程 中执行。这意味着当方法被调用时,调用者将不会等待该方法执行完成,而是可以继续执行后续的代码。

@Async 注解的使用非常简单,需要两个步骤:

  1. 在启动类上添加注解 @EnableAsync ,开启异步任务。
  2. 在需要异步执行的方法或类上添加注解 @Async
@SpringBootApplication
// 开启异步任务
@EnableAsync
public class YourApplication {public static void main(String[] args) {SpringApplication.run(YourApplication.class, args);}
}// 异步服务类
@Service
public class MyService {// 推荐使用自定义线程池,这里只是演示基本用法@Asyncpublic CompletableFuture<String> doSomethingAsync() {// 这里会有一些业务耗时操作// ...// 使用 CompletableFuture 可以更方便地处理异步任务的结果,避免阻塞主线程return CompletableFuture.completedFuture("Async Task Completed");}}

接下来,我们一起来看看 @Async 的底层原理。

@Async 原理分析

@Async 可以异步执行任务,本质上是使用 动态代理 来实现的。通过 Spring 中的后置处理器 BeanPostProcessor 为使用 @Async 注解的类创建动态代理,之后 @Async 注解方法的调用会被动态代理拦截,在拦截器中将方法的执行封装为异步任务提交给线程池处理。

接下来,我们来详细分析一下。

开启异步

使用 @Async 之前,需要在启动类上添加 @EnableAsync 来开启异步,@EnableAsync 注解如下:

// 省略其他注解 ...
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync { /* ... */ }

@EnableAsync 注解上通过 @Import 注解引入了 AsyncConfigurationSelector ,因此 Spring 会去加载通过 @Import 注解引入的类。

AsyncConfigurationSelector 类实现了 ImportSelector 接口,因此在该类中会重写 selectImports() 方法来自定义加载 Bean 的逻辑,如下:

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {@Override@Nullablepublic String[] selectImports(AdviceMode adviceMode) {switch (adviceMode) {// 基于 JDK 代理织入的通知case PROXY:return new String[] {ProxyAsyncConfiguration.class.getName()};// 基于 AspectJ 织入的通知case ASPECTJ:return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};default:return null;}}
}

selectImports() 方法中,会根据通知的不同类型来选择加载不同的类,其中 adviceMode 默认值为 PROXY

这里以基于 JDK 代理的通知为例,此时会加载 ProxyAsyncConfiguration 类,如下:

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public AsyncAnnotationBeanPostProcessor asyncAdvisor() {// ...// 加载后置处理器AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();// ...return bpp;}
}

后置处理器

ProxyAsyncConfiguration 类中,会通过 @Bean 注解加载一个后置处理器 AsyncAnnotationBeanPostProcessor ,这个后置处理器是使 @Async 注解起作用的关键。

如果某一个类或者方法上使用了 @Async 注解,AsyncAnnotationBeanPostProcessor 处理器就会为该类创建一个动态代理。

该类的方法在执行时,会被代理对象的拦截器所拦截,其中被 @Async 注解标记的方法会异步执行。

AsyncAnnotationBeanPostProcessor 代码如下:

public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {@Overridepublic void setBeanFactory(BeanFactory beanFactory) {super.setBeanFactory(beanFactory);// 创建 AsyncAnnotationAdvisor,它是一个 Advisor// 用于拦截带有 @Async 注解的方法并将这些方法异步执行。AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);// 如果设置了自定义的 asyncAnnotationType,则将其设置到 advisor 中。// asyncAnnotationType 用于指定自定义的异步注解,例如 @MyAsync。if (this.asyncAnnotationType != null) {advisor.setAsyncAnnotationType(this.asyncAnnotationType);}advisor.setBeanFactory(beanFactory);this.advisor = advisor;}
}

AsyncAnnotationBeanPostProcessor 的父类实现了 BeanFactoryAware 接口,因此在该类中重写了 setBeanFactory() 方法作为扩展点,来加载 AsyncAnnotationAdvisor

创建 Advisor

AdvisorSpring AOPAdvicePointcut 的抽象。Advice 为执行的通知逻辑,Pointcut 为通知执行的切入点。

在后置处理器 AsyncAnnotationBeanPostProcessor 中会去创建 AsyncAnnotationAdvisor , 在它的构造方法中,会构建对应的 AdvicePointcut ,如下:

public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {private Advice advice; // 异步执行的 Adviceprivate Pointcut pointcut; // 匹配 @Async 注解方法的切点// 构造函数public AsyncAnnotationAdvisor(/* 参数省略 */) {// 1. 创建 Advice,负责异步执行逻辑this.advice = buildAdvice(executor, exceptionHandler);// 2. 创建 Pointcut,选择要被增强的目标方法this.pointcut = buildPointcut(asyncAnnotationTypes);}// 创建 Adviceprotected Advice buildAdvice(/* 参数省略 */) {// 创建处理异步执行的拦截器AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);// 使用执行器和异常处理器配置拦截器interceptor.configure(executor, exceptionHandler);return interceptor;}// 创建 Pointcutprotected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {ComposablePointcut result = null;for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {// 1. 类级别切点:如果类上有注解则匹配Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);// 2. 方法级别切点:如果方法上有注解则匹配Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);if (result == null) {result = new ComposablePointcut(cpc);} else {// 使用 union 合并之前的切点result.union(cpc);}// 将方法级别切点添加到组合切点result = result.union(mpc);}// 返回组合切点,如果没有提供注解类型则返回 Pointcut.TRUEreturn (result != null ? result : Pointcut.TRUE);}
}

AsyncAnnotationAdvisor 的核心在于构建 AdvicePointcut

  • 构建 Advice :会创建 AnnotationAsyncExecutionInterceptor 拦截器,在拦截器的 invoke() 方法中会执行通知的逻辑。
  • 构建 Pointcut :由 ClassFilterMethodMatcher 组成,用于匹配哪些方法需要执行通知( Advice )的逻辑。
后置处理逻辑

AsyncAnnotationBeanPostProcessor 后置处理器中实现的 postProcessAfterInitialization() 方法在其父类 AbstractAdvisingBeanPostProcessor 中,在 Bean 初始化之后,会进入到 postProcessAfterInitialization() 方法进行后置处理。

在后置处理方法中,会判断 Bean 是否符合后置处理器中 Advisor 通知的条件,如果符合,则创建代理对象。如下:

// AbstractAdvisingBeanPostProcessor
public Object postProcessAfterInitialization(Object bean, String beanName) {if (this.advisor == null || bean instanceof AopInfrastructureBean) {return bean;}if (bean instanceof Advised) {Advised advised = (Advised) bean;if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {if (this.beforeExistingAdvisors) {advised.addAdvisor(0, this.advisor);}else {advised.addAdvisor(this.advisor);}return bean;}}// 判断给定的 Bean 是否符合后置处理器中 Advisor 通知的条件,符合的话,就创建代理对象。if (isEligible(bean, beanName)) {ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);if (!proxyFactory.isProxyTargetClass()) {evaluateProxyInterfaces(bean.getClass(), proxyFactory);}// 添加 Advisor。proxyFactory.addAdvisor(this.advisor);customizeProxyFactory(proxyFactory);// 返回代理对象。return proxyFactory.getProxy(getProxyClassLoader());}return bean;
}

@Async 注解方法的拦截

@Async 注解方法的执行会在 AnnotationAsyncExecutionInterceptor 中被拦截,在 invoke() 方法中执行拦截器的逻辑。此时会将 @Async 注解标注的方法封装为异步任务,交给执行器来执行。

invoke() 方法在 AnnotationAsyncExecutionInterceptor 的父类 AsyncExecutionInterceptor 中定义,如下:

public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered {@Override@Nullablepublic Object invoke(final MethodInvocation invocation) throws Throwable {Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);// 1、确定异步任务执行器AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);// 2、将要执行的方法封装为 Callable 异步任务Callable<Object> task = () -> {try {// 2.1、执行方法Object result = invocation.proceed();// 2.2、如果方法返回值是 Future 类型,阻塞等待结果if (result instanceof Future) {return ((Future<?>) result).get();}}catch (ExecutionException ex) {handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());}catch (Throwable ex) {handleError(ex, userDeclaredMethod, invocation.getArguments());}return null;};// 3、提交任务return doSubmit(task, executor, invocation.getMethod().getReturnType());}
}

invoke() 方法中,主要有 3 个步骤:

  1. 确定执行异步任务的执行器。
  2. @Async 注解标注的方法封装为 Callable 异步任务。
  3. 将任务提交给执行器执行。
1、获取异步任务执行器

determineAsyncExecutor() 方法中,会获取异步任务的执行器(即执行异步任务的 线程池 )。代码如下:

// 确定异步任务的执行器
protected AsyncTaskExecutor determineAsyncExecutor(Method method) {// 1、先从缓存中获取。AsyncTaskExecutor executor = this.executors.get(method);if (executor == null) {Executor targetExecutor;// 2、获取执行器的限定符。String qualifier = getExecutorQualifier(method);if (StringUtils.hasLength(qualifier)) {// 3、根据限定符获取对应的执行器。targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);}else {// 4、如果没有限定符,则使用默认的执行器。即 Spring 提供的默认线程池:SimpleAsyncTaskExecutor。targetExecutor = this.defaultExecutor.get();}if (targetExecutor == null) {return null;}// 5、将执行器包装为 TaskExecutorAdapter 适配器。// TaskExecutorAdapter 是 Spring 对于 JDK 线程池做的一层抽象,还是继承自 JDK 的线程池 Executor。这里可以不用管太多,只要知道它是线程池就可以了。executor = (targetExecutor instanceof AsyncListenableTaskExecutor ?(AsyncListenableTaskExecutor) targetExecutor : new TaskExecutorAdapter(targetExecutor));this.executors.put(method, executor);}return executor;
}

determineAsyncExecutor() 方法中确定了异步任务的执行器(线程池),主要是通过 @Async 注解的 value 值来获取执行器的限定符,根据限定符再去 BeanFactory 中查找对应的执行器就可以了。

如果在 @Async 注解中没有指定线程池,则会通过 this.defaultExecutor.get() 来获取默认的线程池,其中 defaultExecutor 在下边方法中进行赋值:

// AsyncExecutionInterceptor
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {// 1、尝试从 beanFactory 中获取线程池。Executor defaultExecutor = super.getDefaultExecutor(beanFactory);// 2、如果 beanFactory 中没有,则创建 SimpleAsyncTaskExecutor 线程池。return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}

其中 super.getDefaultExecutor() 会在 beanFactory 中尝试获取 Executor 类型的线程池。代码如下:

protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {if (beanFactory != null) {try {// 1、从 beanFactory 中获取 TaskExecutor 类型的线程池。return beanFactory.getBean(TaskExecutor.class);}catch (NoUniqueBeanDefinitionException ex) {try {// 2、如果有多个,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);}catch (NoSuchBeanDefinitionException ex2) {if (logger.isInfoEnabled()) {// ...}}}catch (NoSuchBeanDefinitionException ex) {try {// 3、如果没有,则尝试从 beanFactory 中获取执行名称的 Executor 线程池。return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);}catch (NoSuchBeanDefinitionException ex2) {// ...}}}return null;
}

getDefaultExecutor() 中,如果从 beanFactory 获取线程池失败的话,则会创建 SimpleAsyncTaskExecutor 线程池。

该线程池的在每次执行异步任务时,都会创建一个新的线程去执行任务,并不会对线程进行复用,从而导致异步任务执行的开销很大。一旦在 @Async 注解标注的方法某一瞬间并发量剧增,应用就会大量创建线程,从而影响服务质量甚至出现服务不可用。

同一时刻如果向 SimpleAsyncTaskExecutor 线程池提交 10000 个任务,那么该线程池就会创建 10000 个线程,其的 execute() 方法如下:

// SimpleAsyncTaskExecutor:execute() 内部会调用 doExecute()
protected void doExecute(Runnable task) {// 创建新线程Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));thread.start();
}

建议:在使用 @Async 时需要自己指定线程池,避免 Spring 默认线程池带来的风险。

@Async 注解中的 value 指定了线程池的限定符,根据限定符可以获取 自定义的线程池 。获取限定符的代码如下:

// AnnotationAsyncExecutionInterceptor
protected String getExecutorQualifier(Method method) {// 1.从方法上获取 Async 注解。Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class);// 2. 如果方法上没有找到 @Async 注解,则尝试从方法所在的类上获取 @Async 注解。if (async == null) {async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class);}// 3. 如果找到了 @Async 注解,则获取注解的 value 值并返回,作为线程池的限定符。//    如果 "value" 属性值为空字符串,则使用默认的线程池。//    如果没有找到 @Async 注解,则返回 null,同样使用默认的线程池。return (async != null ? async.value() : null);
}
2、将方法封装为异步任务

在 invoke() 方法获取执行器之后,会将方法封装为异步任务,代码如下:

// 将要执行的方法封装为 Callable 异步任务
Callable<Object> task = () -> {try {// 2.1、执行被拦截的方法 (proceed() 方法是 AOP 中的核心方法,用于执行目标方法)Object result = invocation.proceed();// 2.2、如果被拦截方法的返回值是 Future 类型,则需要阻塞等待结果,//     并将 Future 的结果作为异步任务的结果返回。 这是为了处理异步方法嵌套调用的情况。//     例如,一个异步方法内部调用了另一个异步方法,则需要等待内部异步方法执行完成,//     才能返回最终的结果。if (result instanceof Future) {return ((Future<?>) result).get(); // 阻塞等待 Future 的结果}}catch (ExecutionException ex) {// 2.3、处理 ExecutionException 异常。 ExecutionException 是 Future.get() 方法抛出的异常,handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments()); // 处理原始异常}catch (Throwable ex) {// 2.4、处理其他类型的异常。 将异常、被拦截的方法和方法参数作为参数调用 handleError() 方法进行处理。handleError(ex, userDeclaredMethod, invocation.getArguments());}// 2.5、如果方法返回值不是 Future 类型,或者发生异常,则返回 null。return null;
};

相比于 RunnableCallable 可以返回结果,并且抛出异常。

invocation.proceed() 的执行(原方法的执行)封装为 Callable 异步任务。这里仅仅当 result (方法返回值)类型为 Future 才返回,如果是其他类型则直接返回 null

因此使用 @Async 注解标注的方法如果使用 Future 类型之外的返回值,则无法获取方法的执行结果。

3、提交异步任务

AsyncExecutionInterceptor # invoke() 中将要执行的方法封装为 Callable 任务之后,就会将任务交给执行器来执行。提交相关的代码如下:

protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {// 根据方法的返回值类型,选择不同的异步执行方式并返回结果。// 1. 如果方法返回值是 CompletableFuture 类型if (CompletableFuture.class.isAssignableFrom(returnType)) {// 使用 CompletableFuture.supplyAsync() 方法异步执行任务。return CompletableFuture.supplyAsync(() -> {try {return task.call();}catch (Throwable ex) {throw new CompletionException(ex); // 将异常包装为 CompletionException,以便在 future.get() 时抛出}}, executor);}// 2. 如果方法返回值是 ListenableFuture 类型else if (ListenableFuture.class.isAssignableFrom(returnType)) {// 将 AsyncTaskExecutor 强制转换为 AsyncListenableTaskExecutor,// 并调用 submitListenable() 方法提交任务。// AsyncListenableTaskExecutor 是 ListenableFuture 的专用异步执行器,// 它可以返回一个 ListenableFuture 对象,允许添加回调函数来监听任务的完成。return ((AsyncListenableTaskExecutor) executor).submitListenable(task);}// 3. 如果方法返回值是 Future 类型else if (Future.class.isAssignableFrom(returnType)) {// 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务,并返回一个 Future 对象。return executor.submit(task);}// 4. 如果方法返回值是 void 或其他类型else {// 直接调用 AsyncTaskExecutor 的 submit() 方法提交任务。// 由于方法返回值是 void,因此不需要返回任何结果,直接返回 null。executor.submit(task);return null;}
}

在 doSubmit() 方法中,会根据 @Async 注解标注方法的返回值不同,来选择不同的任务提交方式,最后任务会由执行器(线程池)执行。

总结

理解 @Async 原理的核心在于理解 @EnableAsync 注解,该注解开启了异步任务的功能。

主要流程如上图,会通过后置处理器来创建代理对象,之后代理对象中 @Async 方法的执行会走到 Advice 内部的拦截器中,之后将方法封装为异步任务,并提交线程池进行处理。

@Async 使用建议

自定义线程池

如果没有显式地配置线程池,在 @Async 底层会先在 BeanFactory 中尝试获取线程池,如果获取不到,则会创建一个 SimpleAsyncTaskExecutor 实现。SimpleAsyncTaskExecutor 本质上不算是一个真正的线程池,因为它对于每个请求都会启动一个新线程而不重用现有线程,这会带来一些潜在的问题,例如资源消耗过大。

具体线程池获取可以参考这篇文章:浅析 Spring 中 Async 注解底层异步线程池原理|得物技术。

一定要显式配置一个线程池,推荐ThreadPoolTaskExecutor。并且,还可以根据任务的性质和需求,为不同的异步方法指定不同的线程池。

@Configuration
@EnableAsync
public class AsyncConfig {@Bean(name = "executor1")public Executor executor1() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(3);executor.setMaxPoolSize(5);executor.setQueueCapacity(50);executor.setThreadNamePrefix("AsyncExecutor1-");executor.initialize();return executor;}@Bean(name = "executor2")public Executor executor2() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(2);executor.setMaxPoolSize(4);executor.setQueueCapacity(100);executor.setThreadNamePrefix("AsyncExecutor2-");executor.initialize();return executor;}
}

@Async 注解中指定线程池的 Bean 名称:

@Service
public class AsyncService {@Async("executor1")public void performTask1() {// 任务1的逻辑System.out.println("Executing Task1 with Executor1");}@Async("executor2")public void performTask2() {// 任务2的逻辑System.out.println("Executing Task2 with Executor2");}
}

避免 @Async 注解失效

@Async 注解会在以下几个场景失效,需要注意:

1、同一类中调用异步方法

如果你在同一个类内部调用一个@Async注解的方法,那这个方法将不会异步执行。

@Service
public class MyService {public void myMethod() {// 直接通过 this 引用调用,绕过了 Spring 的代理机制,异步执行失效asyncMethod();}@Asyncpublic void asyncMethod() {// 异步执行的逻辑}
}

这是因为 Spring 的异步机制是通过 代理 实现的,而在同一个类内部的方法调用会绕过 Spring 的代理机制,也就是绕过了代理对象,直接通过 this 引用调用的。由于没有经过代理,所有的代理相关的处理(即将任务提交线程池异步执行)都不会发生。

为了避免这个问题,比较推荐的做法是将异步方法移至另一个 Spring Bean 中。

@Service
public class AsyncService {@Asyncpublic void asyncMethod() {// 异步执行的逻辑}
}@Service
public class MyService {@Autowiredprivate AsyncService asyncService;public void myMethod() {asyncService.asyncMethod();}
}

2、使用 static 关键字修饰异步方法

如果@Async注解的方法被 static 关键字修饰,那这个方法将不会异步执行。

这是因为 Spring 的异步机制是通过代理实现的,由于静态方法不属于实例而是属于类且不参与继承,Spring 的代理机制(无论是基于 JDK 还是 CGLIB)无法拦截静态方法来提供如异步执行这样的增强功能。

篇幅问题,这里没有进一步详细介绍,不了解的代理机制的朋友,可以看看我写的 Java 代理模式详解这篇文章。

如果你需要异步执行一个静态方法的逻辑,可以考虑设计一个非静态的包装方法,这个包装方法使用 @Async 注解,并在其内部调用静态方法

@Service
public class AsyncService {@Asyncpublic void asyncWrapper() {// 调用静态方法SClass.staticMethod();}
}public class SClass {public static void staticMethod() {// 执行一些操作}
}

3、忘记开启异步支持

Spring Boot 默认情况下不启用异步支持,确保在主配置类 Application 上添加@EnableAsync注解以启用异步功能。

@SpringBootApplication
@EnableAsync
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

4、@Async 注解的方法所在的类必须是 Spring Bean

@Async 注解的方法必须位于 Spring 管理的 Bean 中,只有这样,Spring 才能在创建 Bean 时应用代理,代理能够拦截方法调用并实现异步执行的逻辑。如果该方法不在 Spring 管理的 bean 中,Spring 就无法创建必要的代理,@Async 注解就不会产生任何效果。

返回值类型

建议将 @Async 注解方法的返回值类型定义为 voidFuture

  • 如果不需要获取异步方法返回的结果,将返回值类型定义为 void
  • 如果需要获取异步方法返回的结果,将返回值类型定义为 Future(例如CompletableFutureListenableFuture )。

如果将 @Async 注解方法的返回值定义为其他类型(如 ObjectString 等等),则无法获取方法返回值。

这种设计符合异步编程的基本原则,即调用者不应立即期待一个结果,而是应该能够在未来某个时间点获取结果。如果返回类型是 Future,调用者可以使用这个返回的 Future 对象来查询任务的状态,取消任务,或者在任务完成时获取结果。

处理异步方法中的异常

异步方法中抛出的异常默认不会被调用者捕获。为了管理这些异常,建议使用CompletableFuture的异常处理功能,或者配置一个全局的AsyncUncaughtExceptionHandler来处理没有正确捕获的异常。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer{@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return new CustomAsyncExceptionHandler();}}// 自定义异常处理器
class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {@Overridepublic void handleUncaughtException(Throwable ex, Method method, Object... params) {// 日志记录或其他处理逻辑}
}

未考虑事物管理

@Async注解的方法需要事务支持时,务必在该异步方法上独立使用。

@Service
public class AsyncTransactionalService {@Async// Propagation.REQUIRES_NEW 表示 Spring 在执行异步方法时开启一个新的、与当前事务无关的事务@Transactional(propagation = Propagation.REQUIRES_NEW)public void asyncTransactionalMethod() {// 这里的操作会在新的事务中执行// 执行一些数据库操作}
}

未指定异步方法执行顺序

@Async注解的方法执行是非阻塞的,它们可能以任意顺序完成。如果需要按照特定的顺序处理结果,你可以将方法的返回值设定为 FutureCompletableFuture ,通过返回值对象来实现一个方法在另一个方法完成后再执行。

@Async
public CompletableFuture<String> fetchDataAsync() {return CompletableFuture.completedFuture("Data");
}@Async
public CompletableFuture<String> processDataAsync(String data) {return CompletableFuture.supplyAsync(() -> "Processed " + data);
}

processDataAsync 方法在 fetchDataAsync后执行:

CompletableFuture<String> dataFuture = asyncService.fetchDataAsync();
dataFuture.thenCompose(data -> asyncService.processDataAsync(data)).thenAccept(result -> System.out.println(result));

相关文章:

Async 注解原理分析

Async 注解由 Spring 框架提供&#xff0c;被该注解标注的类或方法会在 异步线程 中执行。这意味着当方法被调用时&#xff0c;调用者将不会等待该方法执行完成&#xff0c;而是可以继续执行后续的代码。 Async 注解的使用非常简单&#xff0c;需要两个步骤&#xff1a; 在启…...

pyTorch-迁移学习-图片数据增强-四种天气图片的多分类问题

目录 1.导包 2.加载数据、拼接训练与测试数据的文件夹路径 3数据预处理 3.1数据增强 3.2用分类存储的图片数据创建dataloader 4.加载预训练好的模型 (迁移学习) 4.1固定、修改预训练好的模型 5.将模型拷到GPU上 6.定义优化器与损失函数 7.学习率衰减 8.定义训…...

Linux脚本基础详解

一、基础知识 Linux 脚本主要是指在 Linux 系统中编写的用于自动化执行任务的脚本程序&#xff0c;其中最常用的便是 Bash 脚本。下面我们将从语法、使用方法和示例三个方面详细讲解 Linux 脚本。 1. 脚本简介 定义&#xff1a;Linux 脚本是一系列命令的集合&#xff0c;可以…...

MQTT-Dashboard-数据集成-WebHook、日志管理

常用的 Docker Volume 命令及其用法。 1、创建数据卷 使用 docker volume create 命令可以创建一个新的数据卷。例如&#xff0c;创建一个名为 my_volume 的数据卷&#xff1a; docker volume create my_volume 2、列出数据卷 使用 docker volume ls 命令可以列出所有的数据卷…...

Elixir语言的移动应用安全

Elixir语言的移动应用安全解析 引言 在当今的数字化时代&#xff0c;移动应用已经成为我们日常生活中不可或缺的一部分。从购物、社交到在线银行&#xff0c;几乎每一个生活领域都与移动应用紧密相连。然而&#xff0c;随着应用的普及&#xff0c;安全问题也随之而来。如何确…...

【科学技术部政务服务平台-用户注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…...

HTTP 教程 : 从 0 到 1 全面指南 教程【全文三万字保姆级详细讲解】

目录 HTTP 的请求-响应 HTTP 方法 HTTP 状态码 HTTP 版本 安全性 HTTP/HTTPS 简介 HTTP HTTPS HTTP 工作原理 HTTPS 作用 HTTP 与 HTTPS 区别 HTTP 消息结构 客户端请求消息 服务器响应消息 实例 HTTP 请求方法 各个版本定义的请求方法 HTTP/1.0 HTTP/1.1 …...

【LeetCode 热题100】139:单词拆分(动态规划全解析+细节陷阱)(Go语言版)

&#x1f680; LeetCode 热题 139&#xff1a;单词拆分&#xff08;Word Break&#xff09;| 动态规划全解析细节陷阱 &#x1f4cc; 题目描述 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请判断 s 是否可以由字典中出现的单词拼接成。 说明&#xff1a;不要求字典…...

2025年招投标行业的深度变革:洞察趋势,把握未来

2025年&#xff0c;随着政府工作报告对招投标行业的一系列改革措施的提出&#xff0c;整个行业正面临一场前所未有的深度变革。这些政策旨在推动全国统一大市场的建设、加速数字化转型、促进绿色低碳发展&#xff0c;并强化风险防控。在这场变革中&#xff0c;企业不仅要适应新…...

树莓派学习专题<3>:使能VNC远程桌面与VNC文件传输

树莓派学习专题&#xff1c;3&#xff1e;&#xff1a;使能VNC远程桌面与VNC文件传输 1. 配置VNC2. 使用VNC viewer连接到树莓派3. 使用VNC viewer传输文件 1. 配置VNC 在终端或SSH中&#xff0c;使用如下命令打开树莓派系统配置项&#xff1a; sudo su raspi-config以上两项…...

AI烘焙大赛中的算法:理解PPO、GRPO与DPO最简单的方式

&#x1f9e0; 向所有学习者致敬&#xff01; “学习不是装满一桶水&#xff0c;而是点燃一把火。” —— 叶芝 我的博客主页&#xff1a; https://lizheng.blog.csdn.net &#x1f310; 欢迎点击加入AI人工智能社区&#xff01; &#x1f680; 让我们一起努力&#xff0c;共创…...

qt自定义信号槽需要注意的事项

在 Qt 中&#xff0c;自定义信号和槽是与事件和对象交互的核心机制之一。创建自定义信号和槽时&#xff0c;有几个重要事项需要注意&#xff0c;以确保它们能够正确工作。以下是一些需要注意的关键点&#xff1a; 1. 信号和槽的声明 信号声明&#xff1a;信号应该在 signals …...

OpenCV--图像轮廓检测

在图像处理与计算机视觉领域&#xff0c;轮廓检测是一项极为关键的技术。轮廓作为物体边界的重要表征&#xff0c;承载了图像中物体的形状、尺寸和位置等关键信息。通过轮廓检测&#xff0c;我们能够提取出图像中物体的轮廓&#xff0c;为后续的物体识别、图像分割、形状分析等…...

从搜索丝滑过渡到动态规划的学习指南

搜索&动态规划 前言砝码称重满分代码及思路solution 1&#xff08;动态规划&#xff09;solution 2&#xff08;BFS&#xff09; 跳跃满分代码及思路solution 1(动态规划)solution 2 (BFS) 积木画满分代码及思路动态规划思路讲解solution 前言 本文主要是通过一些竞赛真题…...

通用文字识别技术的出现,深刻改变信息的处理方式

在数字化浪潮席卷全球的今天&#xff0c;文字作为人类文明最基础的载体&#xff0c;正经历着一场前所未有的技术革命。通用文字识别&#xff08;OCR&#xff0c;Optical Character Recognition&#xff09;技术已经从简单的"图片转文字"工具&#xff0c;进化为能够理…...

linux 下du 和 ls-alh 的区别

我一直以为du -m 可以显示文件大小。发现不对。正确的做法你是用ls -alh 来使用...

【k8s学习之CSI】理解 LVM 存储概念和相关操作

鸟哥的 Linux 私房菜 – Quota, Software RAID, LVM, iSCSI 0 | 理解 vg 相关概念 在 Linux LVM&#xff08;逻辑卷管理&#xff09; 中&#xff0c;以下是 partition&#xff08;分区&#xff09;、PV&#xff08;物理卷&#xff09;、VG&#xff08;卷组&#xff09;、LV&am…...

【分享开发笔记,赚取电动螺丝刀】使用STM32F103的hal库,采用PWM+DMA发送方式驱动WS2812的RGB彩灯

简单和大家介绍一下本文章的主要内容&#xff1a;使用STM32F103C8最小系统板&#xff0c;使用STM32 cubeMX 6.14版本生成底层的驱动库、结合定时器的PWM 输出功能、使用DMA发送数据的 方式&#xff0c;驱动WS2812 的RGB三色灯。 本次小的DIY所需的物料&#xff1a;stm32f103c8…...

CubeMX配置STM32VET6实现网口通信(无操作系统版-附源码)

下面是使用CubeMX配置STM32F407VET6&#xff0c;实现以太网通讯&#xff08;PHY芯片为LAN8720&#xff09;的具体步骤总结&#xff1a; 一、硬件连接方式&#xff1a; 硬件原理图&#xff1a; 使用外部晶振为PHY芯片提供时钟。 STM32F407VET6 与 LAN8720 采用 RMII 模式连接。…...

一种反激式开关电源设计流程

引&#xff1a;随着生产和技术的发展&#xff0c;对环保和能源的要求也越来越高&#xff0c;开关电源的应用也越来越广泛&#xff0c;开关电源电路结构种类繁多&#xff0c;包括单端转换器和双端转换器。本文介绍一种利用反激式变换电路实现5V开关电源的设计方法&#xff0c;以…...

数据结构实验3.2:链栈的基本操作与括号匹配问题

文章目录 一&#xff0c;问题描述二&#xff0c;基本要求三&#xff0c;算法分析&#xff08;一&#xff09;链栈的存储结构设计&#xff08;二&#xff09;链栈基本操作的时间复杂度分析&#xff08;三&#xff09;括号匹配算法分析 四&#xff0c;示例代码五&#xff0c;实验…...

一周学会Pandas2 Python数据处理与分析-NumPy算术运算和统计计算

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 算术运算 数组的灵魂就在于可以进行批量的运算而不是要在循环里面进行元素的运算&#xff1a; 示例&#xff1a; …...

2011年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析

2011年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析 全国大学生数学建模竞赛(China Undergraduate Mathematical Contest in Modeling)是国家教委高教司和中国工业与应用数学学会共同主办的面向全国大学生的群众性科技活动,目的在于激励学生学习数学的积极性,提高学…...

科普:GBDT与XGBoost比较

本文不去讲GBDT与XGBoost算法的原理及算法本身&#xff0c;而是从应用者的角度&#xff0c;对二者比较&#xff0c;以便选择。 XGBoost是GBDT的“工程化增强版”&#xff0c;在保持Boosting核心思想的同时&#xff0c;通过数学优化&#xff08;二阶导数、正则化&#xff09;和工…...

大数据技术之 Scala(5)

以下是今天学习的知识点与代码测试&#xff1a; 一、不可变数组与可变数组的转换 说明 arr1.toBuffer //不可变数组转可变数组arr2.toArray //可变数组转不可变数组 arr2.toArray 返回结果才是一个不可变数组&#xff0c;arr2 本身没有变化arr1.toBuffer 返回结果才是一个可变…...

int 与 Integer 的区别详解

1. 本质区别 特性intInteger类型基本数据类型&#xff08;Primitive&#xff09;包装类&#xff08;Wrapper Class&#xff09;存储位置栈&#xff08;或作为对象成员在堆中&#xff09;堆&#xff08;对象实例&#xff09;默认值0null&#xff08;可能导致 NullPointerExcept…...

初阶数据结构(3)顺序表

Hello~,欢迎大家来到我的博客进行学习&#xff01; 目录 1.线性表2.顺序表2.1 概念与结构2.2 分类2.2.1 静态顺序表2.2.2 动态顺序表 2.3 动态顺序表的实现初始化尾插头插尾删头删查找指定位置之前插入数据删除指定位置的数据销毁 1.线性表 首先我们需要知道的是&#xff0c;…...

智能DNS解析:解决高防IP地区访问异常的实战指南

摘要&#xff1a;针对高防IP在部分地区无法访问的问题&#xff0c;本文设计基于智能DNS的流量调度方案&#xff0c;提供GeoDNS配置与故障切换代码示例。 一、问题背景 运营商误拦截或线路波动可能导致高防IP在福建、江苏等地访问异常。传统切换方案成本高&#xff0c;智能DNS可…...

瑞芯微RK3568嵌入式AI项目实战:项目方向(三)

基于RK3568的成熟开源项目和实战资源丰富&#xff0c;以下是针对小白的精选推荐及学习路径规划&#xff0c;结合多个开源项目和详细教程&#xff0c;帮助快速入门嵌入式开发&#xff1a; 一、OpenHarmony智能设备开发 1. 凌蒙派-RK3568开发板项目 项目特点&#xff1a;支持Op…...

go游戏后端开发26:红中麻将发牌逻辑

首先&#xff0c;麻将游戏创建房间的逻辑与之前我们做过的“赢三张”创建房间的逻辑是一致的&#xff0c;整体上没有问题。不同之处在于&#xff0c;我们在创建房间时会根据游戏类型来创建对应的“game”&#xff0c;即创建的是麻将的“game”。大家之前写过相关代码&#xff0…...

DataFrame的遍历、排序、去重与分组

一.遍历 1.1 series遍历 import pandas as pds pd.Series([a,b,c,d,e,f],index[1,2,3,4,5,6])for i in s:print(i) a b c d e f 可见&#xff0c;遍历series会直接拿到其中的值 1.2 DataFrame遍历 1.2.1 直接遍历 import pandas as pd data {name: [Alice, Bob, Charlie]…...

QEMU源码全解析 —— 块设备虚拟化(17)

接前一篇文章:QEMU源码全解析 —— 块设备虚拟化(16) 本文内容参考: 《趣谈Linux操作系统》 —— 刘超,极客时间 《QEMU/KVM源码解析与应用》 —— 李强,机械工业出版社 《KVM实战 —— 原理、进阶与性能调优》—— 任永杰 程舟,机械工业出版社...

Java 中使用 File 类创建文件

文章目录 Java 中的 File 类一、文件&#xff08;File&#xff09;定义 二、File 类的介绍1 创建文件对象的相关构造器2 createNewFile() 的作用3 获取文件相关信息的常用方法4 目录的操作和文件的删除 Java 中的 File 类 在 Java 中&#xff0c;文件和文件流是处理数据输入/输…...

PowerBI-按钮过滤筛选-宜宾五粮液股份有限公司财务分析

下面为Powerbi制作的财务主题数据分析模版&#xff0c;以可视化的效果展示了某股份有限公司的财务关键指标数据&#xff0c;如营业收入、净利润、毛利率和净利率等。以瀑布图的方式展示了利润表项目金额情况&#xff0c;以树图方式展示了企业资产负债数据。另外图表可以进行筛选…...

【PyQt5】QSS样式表如何使用

在 PyQt5 中&#xff0c;使用 QSS&#xff08;Qt Style Sheets&#xff09;来为窗口中的各个控件添加样式是非常方便的&#xff0c;类似于 HTML 中的 CSS。你可以通过 QWidget.setStyleSheet() 方法应用 QSS 样式&#xff0c;或者通过 .qss 文件来设置样式。 基本步骤&#xf…...

数据结构:用生活中的例子解释 AOE 网中活动的最早和最迟开始时间的含义和计算方法

生活实例&#xff1a;装修房子中的活动安排 假设你要装修一套房子&#xff0c;主要流程如下&#xff1a; 拆旧&#xff08;活动 A&#xff0c;3 天&#xff09; → 拆旧完成&#xff08;事件 X&#xff09;水电改造&#xff08;活动 B&#xff0c;4 天&#xff09; → 水电完…...

第二篇:系统分析师——7-11章

目录 一、目标二、计划三、完成情况四、意外之喜(最少2点)1.计划内的明确认知和思想的提升标志2.计划外的具体事情提升内容和标志 五、总结 一、目标 通过参加考试&#xff0c;训练学习能力&#xff0c;而非单纯以拿证为目的。 1.在复习过程中&#xff0c;训练快速阅读能力、掌…...

二十七- Scala

抽象属性和方法 1. 基本语法&#xff1a; 定义抽象类&#xff1a;abstract class Person{} //通过 abstract 关键字标记抽象类 定义抽象属性&#xff1a;val|var name:String //一个属性没有初始化&#xff0c;就是抽象属性 定义抽象方法&#xff1a;def hello():String //…...

轨检探伤专用一体机平板电脑:为铁路安全保驾护航

在铁路轨道检测领域&#xff0c;高效、精准的探伤设备是保障列车安全运行的核心工具。鲁成伟业针对轨检探伤小车的严苛需求&#xff0c;推出了多款高性能专用一体机平板电脑&#xff0c;以创新的技术设计和卓越的适应性&#xff0c;成为行业标杆解决方案。以下从产品性能、技术…...

2018年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析

2018年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析 全国大学生数学建模竞赛(China Undergraduate Mathematical Contest in Modeling)是国家教委高教司和中国工业与应用数学学会共同主办的面向全国大学生的群众性科技活动,目的在于激励学生学习数学的积极性,提高学…...

Python数据爬取

一.example1包下的 注意&#xff1a;需要在终端安装pip3 install -i https://requests.readthedocs.io/en/latest/ requests 1.Python网络爬虫初探-get请求 import requests rrequests.get(https://www.baidu.com) print(r.text) r1requests.get(https://www.jd.com) print…...

【详细解析:如何在小程序中实现动态二维码和预约信息展示】

背景介绍 随着小程序的快速发展&#xff0c;二维码已成为很多线上线下场景中必不可少的工具。在这篇文章中&#xff0c;我将分享如何在小程序中实现一个动态生成二维码并展示预约信息的功能。我们会使用 Vue 和 uni-app 框架&#xff0c;结合自定义的 API&#xff0c;来获取预…...

信息学奥赛一本通 1929:【04NOIP普及组】火星人 | 洛谷 P1088 [NOIP 2004 普及组] 火星人

【题目链接】 ybt 1929&#xff1a;【04NOIP普及组】火星人 洛谷 P1088 [NOIP 2004 普及组] 火星人 【题目考点】 1. 深搜回溯 2. STL next_permutation函数 头文件<algorithm> 函数定义&#xff1a;next_permutation(lb, ub, cmp) lb&#xff1a;区间下界&#xff…...

mysql8.0.29 win64下载

mysql win64安装包 mysql win64安装包下载 mysql win64安装包下载 通过网盘分享的文件&#xff1a;mysql 链接: https://pan.baidu.com/s/1sEOl-wSVtOG5gfIRdt5MXw?pwdgi7i 提取码: gi7i...

C++笔记-string(下)

这篇我们自己来简单实现一下string类中的各个接口&#xff0c;来帮助我们更好地理解string类接口的底层原理。 1.构造函数和析构函数 对于构造函数我们要写两种情况&#xff1a;空字符串和非空字符串 因为我们要自己实现string类&#xff0c;所以就不能用std命名空间&#xf…...

Android studio学习之路(六)--真机的调试以及多媒体照相的使用

多媒体应用&#xff08;语言识别&#xff0c;照相&#xff0c;拍视频&#xff09;在生活的各个方面都具有非常大的作用&#xff0c;所以接下来将会逐步介绍多媒体的使用&#xff0c;但是在使用多媒体之前&#xff0c;使用模拟器肯定是不行的&#xff0c;所以我们必须要使用真机…...

Airflow集成Lark机器人

🥭1. 实现目标 🕐 通过自定义函数,实现Lark机器人告警功能 🕐 通过Lark机器人代替邮件数据的发送功能 🥭2.自定义函数实现 from airflow import DAG from airflow.operators.python_operator import PythonOperator from airflow.models import Variable import requ…...

【电视软件】小飞电视v2.7.0 TV版-清爽无广告秒换台【永久更新】

软件介绍 小飞电视是一款电视端的直播软件&#xff0c;无需二次付费和登录&#xff0c;资源丰富&#xff0c;高清流畅。具备开机自启、推送功能、自定义直播源、个性化设置及节目预告等实用功能&#xff0c;为用户带来良好的观看体验。基于mytv开源项目二改&#xff0c;涵盖央…...

2025年- H1-Lc109-160. 相交列表--java版

1.题目描述 2.思路 “双指针切换链表头” 思路一&#xff1a;双指针路径对齐 while (pA ! pB) { pA (pA null) ? headB : pA.next; pB (pB null) ? headA : pB.next; } 让两个指针走相同的总路径长度&#xff01; 设&#xff1a; 链表 A 独有部分长度是 lenA 链表 B …...

《大模型MCP服务协议与多智能体开发实战10讲》课程大纲

以下是针对大模型MCP&#xff08;Model Context Protocol&#xff09;服务协议的多智能体开发系列专栏的10节课课程设计&#xff0c;结合MCP协议特性与多智能体系统的前沿实践&#xff0c;课程结构从协议原理到工程落地&#xff0c;涵盖核心技术、实战案例与前沿趋势&#xff1…...