Зачем нам об этом отдельный пост?

  • Общая практика, применяемая в мире 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