Spring Patterns. Part 2. Spring + ThreadLocal. AOP. Transaction cache

Hi all. I am developing applications using Java, Spring Boot, Hibernate.

In the last article I showed the implementation of the Spring Fluent Interface pattern. With the help of which you can encapsulate similar actions within the application into a module, provide the client code with a convenient declarative API, and at the same time, the “guts” of the module have access to the “magic” of Spring.

https://habr.com/ru/articles/846864/

In this article I want to share my experience with Spring and ThreadLocal variables.

Preface:

Your application may suddenly find that your current code structure is not ideal. For example, new business requirements came that no one expected. Or performance problems started. At the same time, a lot of code has been written, but the bug/rework needs “yesterday”. Using ThreadLocal will help in this situation.

ThreadLocal is a thread-safe variable. Under the hood it has a ConcurrentHashMap. The key is the current flow (it's a little more complicated, but it will be enough for understanding). The value can be any type, ThreadLocal is typed. In this case, you can initialize the value to null, or immediately with something, for example, an empty list.

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

What problems might arise?

It is important to clear the ThreadLocal variable. The point is that most likely your application uses a thread pool. And a situation may arise that a thread was taken from the pool, sent to do work, the thread wrote something to ThreadLocal, worked, and went into the thread pool. Then the thread either dies after time has elapsed. Or he goes off to do some work. And if the same thread goes to do the same work, “foreign” data remains in its ThreadLocal.

Next I will show several ways to use the ThreadLocal variable and clean it up.

Example 1. AOP.

Let's say you have some kind of chain of actions (hereinafter referred to as workflow), for example:

@RestController -> @Service -> @Repository -> … -> @Service -> @RestController

And it may turn out that at some stage of this chain of actions you need to receive something that is not in the parameters of the method signature. In this case, for example, the method signature is not yours, but is set by the interface of a third-party library. Or yours, but you'll have to do a lot of editing.

For example, there is this service:

@Service
@RequiredArgsConstructor
public class SuperService {

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

}

And you need to have access to this name anywhere in the workflow.

Then the simplest solution looks like this:

We create a spring singleton, a wrapper around the ThreadLocal variable.

@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();
    }

}

We wrap SuperService as a “proxy” using @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);
    }

}

“Inject” our bean with ThreadLocal, write down the name. Each time before the original method is executed, the value of the ThreadLocal variable will be overwritten.

Now we have the opportunity to “inject” a bean with ThreadLocal anywhere in the workflow and get the value.

A few additional comments:

1. I prefer to wrap the ThreadLocal variable with a spring singleton. This allows you to “mock” a variable in Unit tests, somehow configure it in component tests, and clear it before/after in integration tests.

2. I prefer to encapsulate additional logic in variable methods. For example, you can return Optional, or swear. Depending on the situation.

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. I prefer to explicitly write cleanup in the finally block. This will allow all code readers to quickly understand that the cleanup has been taken care of.

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

This example considers a case in which we know exactly in which place to overwrite/clear the ThreadLocal variable; then I will show what to do if such a place is unknown. In short, at the end of the transaction, taking into account commit/rollback.

Example 2. TransactionCache.

A situation may arise that during the execution of one workflow the same heavy logic is launched several times, for example, this could be a complex query in the database, the result of which will not change during the execution of the workflow.

Let's start from the end here. Let's add some infrastructure code to start the work at the end of the transaction. Let's create the following interface:

@FunctionalInterface
public interface SimpleAfterCompletionCallback {

    void run();

}

And let's ask Spring to start its work at the AFTER_COMPLETION stage.

@Service
@RequiredArgsConstructor
public class ExampleEventListener {

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

}

Those. we will clear the ThreadLocal variable at the end of the transaction if the transaction is successful or not successful.

The client code will look like this, for the sake of clarity – in the example everything is in one class:

@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);
    }

}

If something has already been written to the ThreadLocal variable, we will return that something. If you haven’t written it down, let’s run the original method, write its result to ThreadLocal, publish the cleanup event, and attach it to the end of the transaction. The result is a GOF Registry pattern with spring and @Transaction.

You can put ThreadLocal in a separate bean, and let it itself publish the event upon initialization. It complains if another developer uses it outside of a transaction, it complains if get is used before initialization. Depends on your specific case.

Conclusion.

In this article, we looked at examples of using the ThreadLocal variable in the Spring world.

We learned about the importance of cleaning it.

We considered two cleaning methods, when the location of the rewrite/clearing is known and when it is not known.

ThreadLocal variables in the code are a temporary solution to the problem, the next step should be to refactor and abandon ThreadLocal.

The code can be viewed here:

https://github.com/AlekseyShibayev/spring-patterns/tree/main/src/main/java/com/company/app/threadlocal

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *