Spring Patterns. Часть 2. Spring + ThreadLocal. AOP. Transaction cache

от автора

Всем привет. Я разрабатываю приложения с использованием Java, Spring Boot, Hibernate.

В прошлой статье я показал реализацию паттерна Spring Fluent Interface. При помощи которого можно инкапсулировать похожие действия внутри приложения в модуль, предоставлять клиентскому коду удобный декларативный API, и при этом «кишки» модуля могут использовать «магию» Spring.

В этой статье я хочу поделиться опытом работы с Spring и ThreadLocal переменными.

Предисловие

В вашем приложении может внезапно оказаться, что ваша текущая структура кода не идеальна. Например, пришли новые бизнес‑требования, которые никто не ожидал. Или начались проблемы с производительностью. При этом кода написано много, а баг/доработку нужно «вчера». Использование ThreadLocal поможет в этой ситуации.

ThreadLocal — это потоко‑безопасная переменная. Под капотом у которой ConcurrentHashMap. Ключ — текущий поток (там чутка сложнее, но для понимания будет достаточно). Значение может быть любым типом, ThreadLocal типизирована <T>. При этом можно инициализировать значение null, или сразу чем‑то, например пустым списком.

ThreadLocal<List<String>> EXAMPLE_1 = ThreadLocal.withInitial(null); ThreadLocal<List<String>> EXAMPLE_2 = ThreadLocal.withInitial(ArrayList::new);

Какие проблемы могут возникнуть?

Важно очищать ThreadLocal переменную. Дело в том, что скорее всего, ваше приложение использует пул потоков. И может возникнуть ситуация, что поток достали из пула, отправили делать работу, поток записал что‑то в ThreadLocal, отработал, лёг в пул потоков. Далее поток либо умирает по истечению времени. Либо отправляется делать какую‑то работу. И если тот же самый поток пойдет делать ту же самую работу — в его ThreadLocal остались «чужие» данные.

Далее я покажу несколько способов применения ThreadLocal переменную и очистки.

Пример 1. AOP

Допустим у вас есть какая‑то цепочка действий (далее workflow), например:

@RestController → @Service → @Repository → … → @Service → @RestController

И может оказаться так, что вам надо на каком‑то этапе этой цепочки действий получать что‑то, чего нет в параметрах сигнатуры метода. При этом, например, сигнатура метода не ваша, а задана интерфейсом сторонней библиотеки. Или ваша, но придется много править.

Например, есть вот такой сервис:

@Service @RequiredArgsConstructor public class SuperService {      public String run(String name) {         /** очень сложная бизнес логика */         return "42";     }  }

И вам надо иметь доступ к этому name в любом месте workflow.

Тогда простейшее решение выглядит следующим образом:

Мы создаем спринговый синглетон, обертку над ThreadLocal переменной.

@Service @RequiredArgsConstructor public class ExampleThreadLocalVariable {      private static final ThreadLocal<String> VAR = ThreadLocal.withInitial(() -> null);      public void set(String string) {         VAR.set(string);     }      public String get() {         return VAR.get();     }      public void clean() {         VAR.remove();     }  }

Оборачиваем SuperService «проксёй» при помощи @Primary.

@Primary @Service @RequiredArgsConstructor public class PrimaryService1 extends SuperService {      private final ExampleThreadLocalVariable exampleThreadLocalVariable;      @Override     public String run(String name) {         exampleThreadLocalVariable.set(name);         return super.run(name);     }  }

«Инжектим» наш бин с ThreadLocal, записываем name. Каждый раз, перед выполнением оригинального метода, значение ThreadLocal переменной будет перезаписываться.

Теперь мы имеем возможность в любом месте workflow «заинжектить» бин с ThreadLocal и получить значение.

Несколько дополнительных комментариев:

  1. Я предпочитаю оборачивать ThreadLocal переменную спринговым синглетоном. Это позволяет «мокать» переменную в Unit тестах, как‑то настраивать в компонентных тестах, очищать перед/после в интеграционных тестах.

  2. Я предпочитаю инкапсулировать в методы переменной, дополнительную логику. Например, можно вернуть Optional, или ругнуться. По ситуации.

public Optional<String> getOptional() {     return Optional.ofNullable(VAR.get()); }  public String getOrThrow() {     String result = VAR.get();     if (result == null) {         throw new IllegalArgumentException("Need init before use.");     }     return result; }

3. Я предпочитаю явно писать очистку в finally блоке. Это позволит всем читателям кода быстрее понять, что об очистке позаботились.

@Override public String run(String name) {     try {         exampleThreadLocalVariable.set(name);         return super.run(name);     } finally {         exampleThreadLocalVariable.clean();     } }

В этом примере рассмотрен кейс, в котором мы точно знаем в каком месте перезаписывать/очищать ThreadLocal переменную, далее я покажу, что делать, если такое место неизвестно. Если коротко — то в конце транзакции с учетом commit/rollback.

Пример 2. TransactionCache

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

Давайте тут пойдем с конца. Накидаем инфраструктурного кода, для запуска работы в конце транзакции. Создадим вот такой интерфейс:

@FunctionalInterface public interface SimpleAfterCompletionCallback {      void run();  }

И попросим спринг запускать его работу на стадии AFTER_COMPLETION.

@Service @RequiredArgsConstructor public class ExampleEventListener {      @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)     public void runSimpleAfterCompletionCallback(SimpleAfterCompletionCallback callback) {         callback.run();     }  }

т. е. мы будем очищать ThreadLocal переменную в конце транзакции, если транзакция успешна или не успешна.

Клиентский код будет выглядеть следующим образом, наглядности ради — в примере всё в одном классе:

@Service @RequiredArgsConstructor public class TransactionalCache {      private static final ThreadLocal<String> VAR = ThreadLocal.withInitial(() -> null);      private final ApplicationEventPublisher applicationEventPublisher;     private final ExampleThreadLocalVariable exampleThreadLocalVariable;      public String run() {         String result = VAR.get();         if (result == null) {             result = doMainLogic();             initThreadLocalVariable(result);         }         return result;     }      private String doMainLogic() {         /** сложная бизнес логика */         return "42";     }      private void initThreadLocalVariable(String result) {         exampleThreadLocalVariable.set(result);         applicationEventPublisher.publishEvent((SimpleAfterCompletionCallback) exampleThreadLocalVariable::clean);     }  }

Если в ThreadLocal переменную уже что‑то записали — вернем это что‑то. Если не записали, запустим оригинальный метод, его результат запишем в ThreadLocal, опубликуем событие очистки, повесив на конец транзакции. Получается GOF паттерн Registry с спрингом и @Transaction.

Можно вынести ThreadLocal в отдельный бин, и пусть она сама публикует событие при инициализации. Ругается если другой разработчик использует вне транзакции, ругается если get до инициализации. Зависит от вашего конкретного случая.

Заключение

В данной статье мы рассмотрели примеры использования ThreadLocal переменной в мире Spring.

Ознакомились с важностью её очистки.

Рассмотрели два способа очистки, когда место перезаписи/очистки известно и когда не известно.

ThreadLocal переменные в коде — это временное решение проблемы, следующим шагом должен быть рефакторинг и отказ от ThreadLocal.

Код можно посмотреть тут


ссылка на оригинал статьи https://habr.com/ru/articles/849832/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *