Зачем нам об этом отдельный пост?
- Общая практика, применяемая в мире Java для запуска асинхронного процесса, заключается в создании отдельного потока или реализации класса с использованием метода Runnable. Но этот подход не будет работать для Spring, потому что вся идея использования Spring заключается в использовании мощи «внедрения зависимостей», поэтому, если мы создадим поток вне его контекста, Spring не сможет автоматически связывать поля внутри класса.
- Большинство веб-приложений используют AspectJ как обычную практику для внедрения пользовательского контекста на основе информации для входа в систему с использованием переменных ThreadLocal. Этот подход будет работать нормально в большинстве случаев, но в этом сценарии, когда новый поток создается spring, который у нас не обрабатывается, эти переменные ThreadLocal не будут доступны во вновь созданном потоке. Итак, как мы можем уведомить Spring о передаче пользовательского контекста вновь созданному потоку?
Учитывая эти вещи, я хотел бы объяснить, как мы можем сделать это законным и простым способом, не нарушая слишком много существующего кода или архитектуры. Как и в случае с любой другой проблемой, есть несколько подходов к решению этой проблемы, и вот мой подход к этому.
Создание асинхронного процесса с использованием Spring
Служебный класс для получения ссылки на компонент Spring:
Создайте класс utils с именем «ApplicationContextUtils», реализующий интерфейс «ApplicationContextAware», который можно использовать для получения ссылки на любой компонент Spring программно с использованием имени файла класса (вместо использования аннотации autowire).
@Service public class ApplicationContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ApplicationContextUtils.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } }
Обязательно реализуйте интерфейс ApplicationContextAware и переопределите метод setApplicationContext.
Асинхронный код:
@Service @Scope("prototype") public class AsyncProcess { @Autowired private AnotherBean x; @Async @Transactional public void process() { for(int i=0;i<10; i++){ System.out.println("Running "+i); } } }
Аннотируйте класс с помощью @Service и определите область как «прототип», чтобы мы получали новый экземпляр компонента каждый раз, когда мы запрашиваем ссылку на него.
Мы используем аннотацию @Async, чтобы spring знал, что этот метод process () должен выполняться в отдельном потоке.
Обратите внимание, что мы автоматически подключаем AnotherBean с помощью стандартной аннотации @Autowire.
Вызов bean-компонента:
@Service public class MainProgram { @Autowired private ApplicationContextUtils context; public void mainMethod() { AsyncProcess p = context.getBean(AsyncProcess.class); p.process(); } }
Используя класс ApplicationContextUtils, мы получаем ссылку на Spring bean-компонент AsyncProcess и вызываем метод process (). Обратите внимание, что мы не создали ни одного потока и не реализовали что-либо в интерфейсе Runnable, поскольку мы использовали аннотацию @Async над объявлением метода process (), Spring позаботится о создании потока и его запуске.
Сделав это, мы создали асинхронный процесс с использованием Spring framework, что означает, что все хорошие вещи, такие как @Autowire, будут по-прежнему работать, и нет необходимости вносить какие-либо изменения в существующий код.
Внедрение пользовательского контекста в этот асинхронный процесс
Переменная ThreadLocal реализована как HashMap
import java.util.HashMap; public class ThreadCache extends HashMap<String, String> { private static final ThreadLocal<ThreadCache> cache = ThreadLocal.withInitial(ThreadCache::new); public static ThreadCache getCache() { return cache.get(); } }
Внедрение пользовательского контекста с помощью АОП
public class SecurityInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Principal principal = request.getUserPrincipal(); String userEmail = null; if (principal instanceof UserPrincipal) { UserPrincipal userPrincipal = (UserPrincipal) principal; userEmail = userPrincipal.getUserAttributeValue("email"); } ThreadCache.getCache().put("userName", userEmail); } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { super.afterCompletion(request, response, handler, ex); } }
Как видите, мы получаем «электронное письмо» от принципала пользователя и устанавливаем его для переменной ThreadLocal «ThreadCache» в поле «userName».
Теперь давайте изменим AsyncProcess, чтобы прочитать эту переменную ThreadLocal и распечатать ее.
@Service @Scope("prototype") public class AsyncProcess { @Autowired private AnotherBean x; @Async @Transactional public void process() { System.out.println("Thread local variable is:" + ThreadCache.getCache().get("userName")); } }
Запуск этого кода сгенерирует:
java.lang.NullPointerException
Поскольку метод «process ()» в java-файле «AsyncProcess» выполняется в совершенно другом потоке, чем в «MainProgram», эти значения не будут существовать в новом потоке, что приведет к «NullPointerException»
Исправление: используйте InheritableThreadLocal вместо ThreadLocal, и это то, чем они отличаются друг от друга.
InheritableThreadLocal. Класс InheritableThreadLocal является подклассом ThreadLocal. Вместо того, чтобы каждый поток имел собственное значение внутри ThreadLocal, InheritableThreadLocal предоставляет доступ к значениям для потока и всех дочерних потоков, созданных этим потоком.
Обновленный код для «ThreadCache» будет выглядеть так:
public class ThreadCache extends HashMap<String, String> { private static final InheritableThreadLocal<ThreadCache> cache = new InheritableThreadLocal<ThreadCache>() { public ThreadCache initialValue() { return new LocalCache(); } }; public static ThreadCache getCache() { return cache.get(); } }
После внесения изменений повторный запуск программы выдаст ожидаемый результат.
Использованная литература:
Узнайте больше о ThreadLocal и InheritableThreadLocal здесь → http://tutorials.jenkov.com/java-concurrency/threadlocal.html
https://javarevisited.blogspot.com/2012/05/ как использовать-threadlocal-in-java-Benefits.html