Let's say we have a request that fetches a post stored in the database. That request may need to alter the fetched post in some way. Before the post is altered various checks may need to happen, such as an ownership check. But we want to avoid each check fetching the same post from the database. This is when a cache that is scoped to a request's life cycle comes in handy.
In a previous post I introduced a method for carrying out ownership checks
using Spring Security's @PreAuthorize
annotation. Whilst the approach was clean and maintainable
it did introduce a performance issue. This is because @PreAuthorize
executes before the method body.
The post is fetched during the security check, and then again inside the method. This leads to duplicate
fetches from the database. Once in the isOwner
call and then again in the findOwnedPost
call. You
can see for yourself in the below code example.
@Component
@AllArgsConstructor
public class PostSecurity {
private final PostRepository postRepository;
public boolean isOwner(Long id, String userEmail) {
return postRepository.findById(id) // first fetch
.map(Post::getUser)
.map(AppUser::getEmail)
.map(it -> it.equals(userEmail))
.orElse(false);
}
}
@PreAuthorize("@postSecurity.isOwner(#postId, authentication.name)")
public Post getOwnedPost(Long postId) {
return postRepository.findById(postId) // second fetch
.orElseThrow(() -> new EntityNotFoundException("Post not found"));
}
One simple way to avoid this would be to do the ownership check inside the getOwnedPost
method.
This may seem straightforward, but it would lead to code that is harder to maintain. For example
the authenticated UserDetails
would need to be passed around, and a checked exceptions would need
added to each caller... yuck.
A nice solution would be to cache the owned post instance for the lifecycle of the request. This would
mean that the isOwner
method would cache the post on look up. The post could then be retrieved from
the cache inside the getOwnedPost
method. Meaning that there is only one post look up. We would then
add a request filter to clear the cache after each request.
Using a request based cache
Each Spring request uses it's own thread. This means that we can cache values for the current request's
thread, without affecting any other request. To do this we'll use a plain old java object to model our
RequestCache
. I want to use this solution for different entity types, so I'll make it generic by making
it a generic class, so it can hold any type of entity.
public class RequestCache<T> {
private final ThreadLocal<Optional<T>> holder = new ThreadLocal<>();
public void set(T value) {
holder.set(Optional.ofNullable(value));
}
public Optional<T> get() {
return holder.get();
}
public void clear() {
holder.remove();
}
}
InheritableThreadLocal
or a
more advanced request-scoped solution like Spring’s @RequestScope
. But for typical synchronous requests,
ThreadLocal
works well.
Now let's create a RequestCacheRegistry
. This will be a @Component
to allow it to be injected into
the various services that will use it. You will be able to see at this stage how this approach can be
extended to work with various entities, by adding a unique RequestCache
property for each cached item.
@Getter
@Component
public class RequestCacheRegistry {
public final RequestCache<Post> ownedPost = new RequestCache<>();
public void clearAll() {
ownedPost.clear();
}
}
Now that our RequestCacheRegistry
has been set up, we can use it within our PostService
. First we'll
need to modify the PostSecurity
component to cache the post that is returned from the PostRepository
.
As the PostRepository
returns an Optional<Post>
the cache can be updated in a chained map
call.
@Component
@AllArgsConstructor
public class PostSecurity {
private final PostRepository postRepository;
private final RequestCacheRegistry requestCacheRegistry;
public boolean isOwner(Long id, String userEmail) {
return postRepository.findById(id)
.filter(it -> it.getUser().getEmail().equals(userEmail))
.map(it -> {
requestCacheRegistry.getOwnedPost().set(it); // cache post
return true;
})
.orElse(false);
}
}
Now that the requestCacheRegisty.ownedPost
is set. It can be access inside the getOwnedPost
method that
has the PreAuthorize
annotation.
@PreAuthorize("@postSecurity.isOwner(#postId, authentication.name)")
public Post getOwnedPost(Long postId) {
return requestCacheRegistry.getOwnedPost().get().orElseGet(() -> findById(postId));
}
With all this in place there is now only one database call to fetch the required post. We have also implemented
a pattern that will allow for other entities to be cached in the same way, using the RequestCacheRegistry
.
Clearing the cache
It is important that we clear the request cache after each request completes. This will help prevent any thread
leaks from occurring. A clean way of doing this is to implement a request filter. The following request filter
will be called automatically on each request. Once the filter chain completes, this custom filter will call
the cacheRegistry.clearAll
method. Ensuring the cache registry is safely cleared after each request.
@Component
@RequiredArgsConstructor
public class RequestCacheCleanUpFilter extends OncePerRequestFilter {
private final RequestCacheRegistry cacheRegistry;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
cacheRegistry.clearAll();
}
}
}