3

I'm trying to implement a very simple asynchronous method call in my SpringBoot application but, whatever I do, I just cannot get it to work. I've looked at dozens of tutorials but they all seem unnecessarily complicated for what I'm doing. I'd appreciate any help you could provide.

Essentially, when a certain REST endpoint of my SpringBoot app is hit, it kicks off a slow task. Instead of making the client wait for the slow task to finish before getting a response, I want to asynchronously call the task method, and return a response to the client right away. For simplicity, the response that I want to return right away is generic -- I don't care about returning the result of the slow task itself.

I'm using Java 8 and SpringBoot 2.1.7


Initially, I started with just 2 classes:

1) Class 1:

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

2) Class 2:

@RestController
@RequestMapping("/rest")
public class Controller {

  @RequestMapping(value = "/slowTask", method = RequestMethod.GET)
  @ResponseBody
  public ResponseEntity<?> slowTask() {
    try{
            asyncSlowTask();
            // Want below response to be immediate -- instead, only occurs after above function is done.
            return new ResponseEntity<String>("Accepted request to do slow task", HttpStatus.ACCEPTED);
        }
    catch (Exception e) {
            return new ResponseEntity<String>("Exception", HttpStatus.INTERNAL_SERVER_ERROR);
        }
  }

  @Async
  public void asyncSlowTask(){
    // Sleep for 10s
    Thread.sleep(10000);
  }

}

I tested this by hitting the app locally: curl http://localhost:8080/rest/slowTask

What I expected is that the curl command would return right away -- instead, it only returned after 10 seconds (after my slow task was done).


At this point, I read that the asynchronous method shouldn't be 'self-invoked' -- I had no idea if that's what I was doing but, to be safe, I moved the async method to a new class, so that my classes now looked like this:

1) Class 1:

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

2) Class 2:

@RestController
@RequestMapping("/rest")
public class Controller {

  @RequestMapping(value = "/slowTask", method = RequestMethod.GET)
  @ResponseBody
  public ResponseEntity<?> slowTask() {
    try{
            AsyncClass.asyncSlowTask();
            // Want below response to be immediate -- instead, only occurs after above function is done.
            return new ResponseEntity<String>("Accepted request to do slow task", HttpStatus.ACCEPTED);
        }
    catch (Exception e) {
            return new ResponseEntity<String>("Exception", HttpStatus.INTERNAL_SERVER_ERROR);
        }
  }
}

3) Class 3:

public class AsyncClass {
  @Async
  public static void asyncSlowTask(){
    // Sleep for 10s
    Thread.sleep(10000);
  }
}

Unfortunately, this didn't make any difference -- the request is still returned only after my slow task is completed.

I've also tried a bunch of other minor variations (including putting @EnableSync and @Configuration on every class), but none of them worked.


So, here are my questions:
1) Did I need to move the async method to that 3rd class, or was my original, two-class setup good enough?
2) Which class should have the @EnableSync annotation?
I'm honestly completely lost as to where it should go -- all I know is that it should go in "one of your @Configuration classes", which means nothing to me.
3) Is there anything else I'm doing wrong??

Again, appreciate any help you can provide!

5
  • Would it be troublesome to delegate the asynchronous nature of this service to the consumers? Commented May 12, 2020 at 1:06
  • @mre Unfortunately, it needs to be handled within this SpringBoot app itself. Again, as far as I can tell this is a pretty common, simple pattern, but I'm missing some nuance Commented May 12, 2020 at 1:08
  • This resource may be helpful - spring.io/guides/gs/async-method Commented May 12, 2020 at 1:09
  • @mre Thanks. I've read it a couple of times, but so far hasn't helped. Commented May 12, 2020 at 1:13
  • It seems like maybe you need to wrap the synchronous task execution in a java.util.concurrent.CompletableFuture? Commented May 12, 2020 at 1:19

2 Answers 2

6

From the Spring Doc,

The default advice mode for processing @Async annotations is proxy which allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way.

Any feature(async, scheduler, transactional, etc.,) which depends on spring proxy works only on the object managed by spring container.

Your code should look like this,

@RestController
@RequestMapping("/rest")
public class Controller {

private final AsyncClass asyncClass;

public Controller(final AsyncClass asyncClass) {
    this.asyncClass = asyncClass;
}

@RequestMapping(value = "/slowTask", method = RequestMethod.GET)
public ResponseEntity<?> slowTask() {
    try {
        asyncClass.asyncSlowTask();
        return new ResponseEntity<>("Accepted request to do slow task", HttpStatus.ACCEPTED);
    } catch (final RuntimeException e) {
        return new ResponseEntity<>("Exception", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

}

@Component
public class AsyncClass {

  @Async
  @SneakyThrows
  public void asyncSlowTask() {
    Thread.sleep(10000);
  }
}

Which class should have the @EnableSync annotation?

You can have the annotation on the Application class or any @Configuration class which should be fine.

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you Ramachandran! I had seen that doc already but I didn't understand until I read this that it meant I wasn't supposed to instantiate AsyncClass myself in Controller (which I was implicitly doing by making asyncSlowTask() a static method). Once I got that, and modified my code to match the Spring Async Guide's, I ended up with pretty much the same code as yours, except that I used @Service as my annotation for AsyncClass.
I don't really understand what's happening here though: ``` private final AsyncClass asyncClass; public Controller(AsyncClass asyncClass) { this.asyncClass = asyncClass; } ``` Would you mind clarifying this? Is Spring automatically (behind the scenes) calling the Controller() constructor and passing in an AsyncClass object for me?
@seth yes. So as of Spring 4.3, you no longer need to specify an explicit injection annotation in such a single-constructor scenario.
Wow man, this one answer is worth a 100 documentations. Pure Gold!
2

Try create a Async configuration

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }

    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolTaskExecutor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

and write down your methods with @Async("threadPoolTaskExecutor")

@Async("threadPoolTaskExecutor")
public void asyncSlowTask() {
    Thread.sleep(10000);
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.