Всем привет. Я разрабатываю приложения с использованием 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 и получить значение.
Несколько дополнительных комментариев:
-
Я предпочитаю оборачивать ThreadLocal переменную спринговым синглетоном. Это позволяет «мокать» переменную в Unit тестах, как‑то настраивать в компонентных тестах, очищать перед/после в интеграционных тестах.
-
Я предпочитаю инкапсулировать в методы переменной, дополнительную логику. Например, можно вернуть 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/
Добавить комментарий