Задать вопрос
@artemfisher

Как работает асинхронность в Spring MVC?

Добрый день!

Согласно документации для асинхронный запросов необходимо чтобы метод возвращал объект типа DeferredResult.

Порядок обработки методов, возвращающих DeferredResult:
  • Контроллер возвращает DeferredResult и сохраняет его в некоторой очереди или списке в памяти, где к нему можно получить доступ.
  • Spring MVC вызывает request.startAsync().
  • При этом DispatcherServlet и все настроенные фильтры выходят из потока обработки запроса, но ответ (соединение с клиентом) остается открытым.
  • Приложение устанавливает значение DeferredResult из некоторого потока, а Spring MVC отправляет запрос обратно в контейнер сервлетов.
  • Вызывается снова DispatcherServlet и обработка возобновляется с асинхронно полученным возвращаемым значением.


Как итог, получается есть два вида потоков: одни обрабатывают запросы, передают их другим потокам, сами освобождаются и готовы принимать новые запросы. Когда вторые потоки выполнят свою работу, они возвращают ее результат первым и те возвращают результат в качестве ответа клиенту.

Попробовал реализовать это на Tomcat и получилось иначе.

1. Устанавливаю в Tomcat максимальное потоков, которые обрабатывают запросы, равное 3.

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               maxParameterCount="1000"
               URIEncoding="UTF-8"
               maxThreads="3" 
               />


2. Настраиваю Spring MVC.

Т.к. использую AbstractAnnotationConfigDispatcherServletInitializer, то поддержка асинхронности подключена по умолчанию.

Так же в файле конфигурации прописываю

@Configuration
@EnableWebMvc
@ComponentScan
@EnableAsync
public class MainWebConfig implements WebMvcConfigurer {
	
	 @Override 
	 public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
		 configurer.setTaskExecutor(mvcTaskExecutor()); 
		 configurer.setDefaultTimeout(30_000); 
	 } 
	 
	 @Bean 
	 public ThreadPoolTaskExecutor mvcTaskExecutor() { 
		 ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
		 threadPoolTaskExecutor.setCorePoolSize(10);
		 threadPoolTaskExecutor.setThreadNamePrefix("mvc-task-");
		 return threadPoolTaskExecutor;
	  }		
}


3. Создаю контроллер

Для каждого метода делаем задержку потока в 30 секунд, чтобы проще проследить.

@Controller
@RequestMapping("/")
public class MainController {			
		
	@GetMapping("/defer")	
	DeferredResult<String> getDefer(HttpServletRequest request) 
	{
		try
		{
			Thread.sleep(30000);
		}
		catch(InterruptedException ex)
		{
			System.out.println("MainController getDefer InterruptedException");
			Thread.currentThread().interrupt();
		}
		DeferredResult<String> result = new DeferredResult<>();
		result.setResult("defer");
		
		return result;
	}
	
	
	
	@GetMapping("/defer2")	
	DeferredResult<String> getDefer2(HttpServletRequest request) 
	{
		try
		{
			Thread.sleep(30000);
		}
		catch(InterruptedException ex)
		{
			System.out.println("MainController getDefer2 InterruptedException");
			Thread.currentThread().interrupt();
		}
		DeferredResult<String> result = new DeferredResult<>();
		result.setResult("defer2");
		
		return result;
	}
	
	
	@GetMapping("/defer3")	
	DeferredResult<String> getDefer3(HttpServletRequest request) 
	{
		try
		{
			Thread.sleep(30000);
		}
		catch(InterruptedException ex)
		{
			System.out.println("MainController getDefer3 InterruptedException");
			Thread.currentThread().interrupt();
		}
		DeferredResult<String> result = new DeferredResult<>();
		result.setResult("defer3");
		
		return result;
	}
	
	
	@GetMapping("/async")	
	String getAsync(HttpServletRequest request) 
	{
		startAsync();
				
		return "async";
	}
	
	
	@Async
	void startAsync()
	{
		try
		{
			Thread.sleep(30000);
		}
		catch(InterruptedException ex)
		{
			System.out.println("MainController getAsync InterruptedException");
			Thread.currentThread().interrupt();
		}
	}
	
	@GetMapping("/async2")	
	String getAsync2(HttpServletRequest request) 
	{
		startAsync();
				
		return "async2";
	}
}


4. Отслеживать будем в менеджере Tomcat по адресу
localhost:8080/manager/status

Помним, что всего потока будет 3. Так что когда будем запускать менеджер, один поток будет уходить на его обработку.

4.1 Обращаемся по адресам /defer и /defer2
67f93ebc5aa1d309948592.png

Видим, что потоки не были высвобождены, и, следовательно, если загрузить /defer3, то я уже не сразу смогу открыть менеджер Tomcat, т.к. все три потока будут заняты и будут обрабатывать запросы по адресам /defer, /defer2, /defer3 (каждый поток мы удерживаем по 30 секунд).

4.2 Обращаемся по адресам /defer и /async
67f93fa1b8644331867801.png

Здесь мы видим, что и метод с @Async занимает поток, который должен обрабатывать входящие запроса, метод же с @Async должен работать вне контекста потоков.

ВОПРОС: ТАК КАК РАБОТАЕТ АСИНХРОННОСТЬ В SPRING MVC?
  • Вопрос задан
  • 30 просмотров
Подписаться 1 Простой Комментировать
Пригласить эксперта
Ответы на вопрос 1
@shushara4241
Добрый вечер!
В контроллерах getDefer1, 2, 3 вы блокируете поток томкета, правильнее будет переделать на какой-нибудь вариант типо такого:
@GetMapping("/defer")	
DeferredResult<String> getDefer() {
    DeferredResult<String> result = new DeferredResult<>();
    CompletableFuture.runAsync(() -> {
        try {
            Thread.sleep(30000);
            result.setResult("defer");
        } catch (InterruptedException e) {
            result.setErrorResult("Timeout");
        }
    }, mvcTaskExecutor());
    return result;
}

Так должно работать так как вы хотите. С async не работает, потому что спринг не умеет перехватывать вызовы @Async внутри одного класса, нужно чтобы обязательно был другой бин
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы