Всем привет. Я разрабатываю приложения с использованием 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.

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

Комментарии (1)