Table of contents
Open Table of contents
Article body
The scenario: my company recently needed to convert uploaded Word documents into images for a document preview feature. The user uploads a Word file through the frontend and receives a success message as soon as the upload finishes. The Java backend then calls the image conversion service asynchronously.
Pseudocode:
Controller layer: receive the uploaded file.
serviceImpl layer: 1. Upload the file -> 2. Upload succeeds -> 3. Call the image conversion service asynchronously.
All I knew at the time was that asynchronous calls in Spring Boot use @Async and that the annotated method must be public. So I wrote the following code:
// Some methods in serviceImpl
@override
public String Upload(File file){
// 1. Upload
Boolean result = upload(file);
if(result){
return "上传失败";
}
// 2. Call the image conversion service asynchronously.
this.transformToPictures()
// 3. Return the upload result immediately.
return "上传成功";
}
@Async
public Re transformToPictures(){
// Image conversion logic
return Re.ok();
}
Unit testing exposed the problem. An exception during the image operation directly affected the main method, causing the frontend to report an upload error even though the upload had already succeeded.
The behavior showed that the supposedly asynchronous method was actually running synchronously. Why had @Async not taken effect? I looked up some information:
-
An @Async method can only return void or Future
, so returning Re in the code above clearly does not work. -
@Async is implemented through dynamic proxies; I did not investigate the details deeply. If an asynchronous method is defined and called within the same class, you must obtain that class’s proxy. I therefore changed the code as follows. When calling an asynchronous method in another class, you can call it directly without obtaining the proxy yourself.
// Some methods in serviceImpl
@override
public String Upload(File file){
// 1. Upload
Boolean result = upload(file);
if(result){
return "上传失败";
}
UploadServiceImpl serviceImpl = SpringContextUtil.getBean(UploadServiceImpl.class)
// 2. Call the image conversion service asynchronously.
serviceImpl.transformToPictures()
// 3. Return the upload result immediately.
return "上传成功";
}
// Change the return type to void.
@Async
public void transformToPictures(){
}
However, springContextUtil.getBean() immediately threw an error saying it could not find the injected bean, which left me confused. I set that aside and moved the code into the controller, using springContextUtil.getBean(MyController.class). The asynchronous call worked there. What was wrong with the service code? After thinking about it, I realized that serviceImpl implements the service interface, so getBean should take Service.class to obtain the proxy for the implementation. The asynchronous method was not available on that interface yet, so it also needed to be added to the service interface, overridden in serviceImpl, and annotated with @Async.
- The final code became:
public class UploadServiceImpl implements UploadService{
// Some methods in serviceImpl
@Override
public String Upload(File file){
// 1. Upload
Boolean result = upload(file);
if(result){
return "上传失败";
}
UploadService service = SpringContextUtil.getBean(UploadService.class)
// 2. Call the image conversion service asynchronously.
service.transformToPictures()
// 3. Return the upload result immediately.
return "上传成功";
}
// Change the return type to void.
@Async
@Override
public void transformToPictures(){
}
}
Now a successful upload returns a success result immediately. Even if the asynchronous method throws an exception, it does not affect the main method, and the frontend user does not see that exception.
Tips:
Besides getBean(), there is another way to obtain the current class’s proxy:
Add the AOP dependency to the Spring Boot project’s pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Enable AOP in application.yml:
aop:
auto: true
proxy-target-class: true
Where the code above obtains the current proxy, use the Java AOP API:
UploadService uploadService = (UploadService) AopContext.currentProxy();
There are similar considerations for @Transactional.

and

If method A in a class has no @Transactional annotation and calls method B in the same class, which does have @Transactional, B will not roll back even if it throws an exception. To make the annotation take effect, obtain the class’s proxy and call through it. If the call comes from another class, rollback can work normally.