Как сделать асинхронный REST с помощью Spring?

Я пытаюсь сделать небольшой REST, используя Spring Boot. Я никогда не использовал Spring и не использовал Java давным-давно (Java 7)!

За последние 2 года я использовал только Python и С# (но, как я уже сказал, я уже использовал Java).

Итак, сейчас я пытаюсь создать REST с использованием асинхронных методов и проверил несколько примеров, но все же я не очень хорошо понимаю "правильный способ" сделать это.

Глядя на следующую документацию: http://carlmartensen.com/completablefuture-deferredresult-async, Java 8 имеет CompletableFuture который я могу использовать с Spring, поэтому я сделал следующий код:

Служба:

@Service
public class UserService {
  private UserRepository userRepository;

  // dependency injection
  // don't need Autowire here
  // https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-spring-beans-and-dependency-injection.html
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Async
  public CompletableFuture<User> findByEmail(String email) throws InterrupedException {
    User user = userRepository.findByEmail(email);
    return CompletableFuture.completedFuture(user);
  }
}

Репозиторий:

public interface UserRepository extends MongoRepository<User, String> {
  @Async
  findByEmail(String email);
}

RestController:

@RestController
public class TestController {

  private UserService userService;

  public TestController(UserService userService) {
    this.userService = userService;
  }

  @RequestMapping(value = "test")
  public @ResponseBody CompletableFuture<User> test(@RequestParam(value = "email", required = true) String email) throws InterruptedException {
    return userService.findByEmail(email).thenApplyAsync(user -> {
      return user;
    })
  }  
}

Этот код дает мне ожидаемый результат. Затем, просматривая другую документацию (извините, я потерял ссылку), я вижу, что Spring принимает следующий код (который также дает мне ожидаемый результат):

  @RequestMapping(value = "test")
  public @ResponseBody CompletableFuture<User> test(@RequestParam(value = "email", required = true) String email) throws InterruptedException {
    return userService.findByEmail(email);
  }  
}

Есть ли разница между этими двумя методами?

Затем, глядя на следующее руководство: https://spring.io/guides/gs/async-method/, есть аннотация @EnableAsync в классе SpringBootApplication. Если я @EnableAsync аннотацию @EnableAsync и создаю asyncExecutor подобный коду из последней ссылки, мое приложение не возвращает ничего в конечной точке /test (только ответ 200 OK, но с пустым телом).

Итак, мой отдых асинхронный без аннотации @EnableAsync? И почему, когда я использую @EnableAsync, тело ответа пустое?

Ответ 1

Тело ответа пустое, потому что аннотация @Async используется в методе findEmail класса UserRepository, это означает, что данные, возвращаемые в следующем предложении, отсутствуют. User user = userRepository.findByEmail(email); потому что метод findByEmail работает в другом потоке и возвращает нуль вместо объекта List.

Аннотация @Async включается, когда вы объявляете @EnableAsync, поэтому это происходит только при использовании @EnableAsync потому что он активирует метод @Async для findEmail для запуска его в другом потоке.

Метод return userService.findByEmail(email); вернет объект CompletableFuture, созданный из класса UserService.

Разница со вторым вызовом метода заключается в том, что метод thenApplyAsync создаст совершенно новый CompletableFuture из предыдущего, который поступает из userService.findByEmail(email) и будет возвращать только пользовательский объект, полученный из первого CompletableFuture.

 return userService.findByEmail(email).thenApplyAsync(user -> {
      return user;
    })

Если вы хотите, чтобы получить ожидаемые результаты просто удалить @Async аннотации от метода findByEmail, и, наконец, добавить @EnableAsync Аннотация

Если вам нужно уточнить идеи использования асинхронных методов, допустим, что вам нужно вызвать три метода, и каждый из них занимает 2 секунды, чтобы завершиться, в обычном сценарии вы будете называть их method1, затем method2 и, наконец, method3, в этом случае вы Весь запрос займет 6 секунд. Когда вы активируете асинхронный подход, вы можете вызвать три из них и просто подождать 2 секунды вместо 6.

Добавьте этот длинный метод в службу пользователя:

@Async
public  CompletableFuture<Boolean> veryLongMethod()  {

    try {
        Thread.sleep(2000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return CompletableFuture.completedFuture(true);
}

И трижды позвони из контроллера, вот так

  @RequestMapping(value = "test")
  public @ResponseBody CompletableFuture<User> test(@RequestParam(value = "email", required = true) String email) throws InterruptedException {
        CompletableFuture<Boolean> boolean1= siteService.veryLongMethod();
        CompletableFuture<Boolean> boolean2= siteService.veryLongMethod();
        CompletableFuture<Boolean> boolean3= siteService.veryLongMethod();

        CompletableFuture.allOf(boolean1,boolean2,boolean3).join();
    return userService.findByEmail(email);
  }  

Наконец, измерьте время, которое занимает ваш ответ, если это занимает более 6 секунд, то вы не используете асинхронный метод, если это займет всего 2 секунды, то у вас все получится.

Также см. Следующую документацию: аннотация @Async, асинхронные методы Spring, класс CompletableFuture

Надеюсь, это поможет.

Ответ 2

Я сталкиваюсь с проблемами производительности при запуске методов Async. Асинхронные дочерние потоки начинают выполнять очень поздно (от 20 до 30 секунд). Я использую ThreadPoolTaskExecutor() в моем основном классе приложений SpringBoot. Вы также можете попробовать то же самое, если вы считаете, что производительность является фактором.