Application Caching in Spring - Quick Introduction

Application Caching in Spring - Quick Introduction

·

8 min read

Application caching in Spring allows you to store frequently accessed data in memory, reducing the need to fetch it from the source every time. This can significantly improve the performance of your application.

Java Spring Code Example

// Import necessary libraries
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class DataService {

    // Define a cache named "userData"
    @Cacheable(value = "userData", key = "#userId")
    public UserData getUserData(int userId) {
        // Simulate fetching data from a database
        // Replace this with your actual data retrieval logic
        return fetchDataFromDatabase(userId);
    }

    private UserData fetchDataFromDatabase(int userId) {
        // Simulate database query
        // Replace this with your actual database query
        System.out.println("Fetching data from database for user ID: " + userId);
        // Assuming this method returns UserData object
        return new UserData(userId, "John Doe");
    }
}

In this example, we're creating a DataService class that contains a method getUserData(int userId). This method is annotated with @Cacheable which indicates that the result of this method should be cached.

  • @Cacheable(value = "userData", key = "#userId"): This annotation specifies that the method's result should be cached. The value attribute represents the name of the cache (in this case, "userData"). The key attribute specifies how to generate the cache key, in this case, it's based on the userId.

  • fetchDataFromDatabase(int userId): This is a simulated method that fetches data from a database. In a real-world scenario, this is where you would perform your actual data retrieval logic.

Explanation

  1. When getUserData(int userId) is called for the first time with a specific userId, Spring checks if there's already a cached result for the given userId.

  2. If there's a cache hit (i.e., the result is already cached), Spring returns the cached result without executing the method.

  3. If it's a cache miss (i.e., the result is not cached), Spring executes the method fetchDataFromDatabase(int userId).

  4. The result is returned and cached using the specified cache name ("userData") and cache key (based on the userId).

  5. Subsequent calls with the same userId will retrieve the result from the cache without executing the method again.

Important Notes

  • Ensure you have a caching provider configured in your Spring application (e.g., using Redis or Ehcache).

  • Make sure to handle cache eviction and cache invalidation based on your application's requirements.

This example demonstrates how Spring's caching mechanism can be employed to efficiently handle frequently accessed data. Replace the simulated database access with your actual data retrieval logic.

Simple Application Caching Example with Spring

For this example, let's imagine we have a service that fetches user details by their ID from a database. We want to use caching to improve the performance of this operation.

Step 1: Set Up a Spring Boot Project

  1. Create a new Spring Boot project using Spring Initializer (https://start.spring.io/).

  2. Add the dependencies for "Spring Web" and "Spring Cache" to your project.

Step 2: Enable Caching in Your Application

Open your main application class (usually Application.java) and add @EnableCaching annotation to enable caching in your Spring application:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Step 3: Create a Service Class

Next, create a service class that will contain the method we want to cache. In this case, let's create a class named UserService.

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userData", key = "#userId")
    public UserData getUserData(int userId) {
        // Simulate fetching data from a database
        return fetchDataFromDatabase(userId);
    }

    private UserData fetchDataFromDatabase(int userId) {
        // Simulate database query
        System.out.println("Fetching data from database for user ID: " + userId);
        return new UserData(userId, "John Doe");
    }
}

Step 4: Create a Model Class

Create a simple UserData class to represent user data.

public class UserData {
    private int id;
    private String name;

    // Constructor, getters, and setters
    // ...
}

Step 5: Create a Controller

Create a controller class to handle HTTP requests.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{userId}")
    public UserData getUserData(@PathVariable int userId) {
        return userService.getUserData(userId);
    }
}

Step 6: Run and Test

Start your Spring Boot application and make GET requests to http://localhost:8080/user/{userId} (replace {userId} with an actual user ID).

How It Works

  1. When you make a request to get user data, Spring will first check the cache named "userData" to see if the data for the given user ID is already available.

  2. If found in the cache, Spring will return the cached data directly, without executing the method.

  3. If not found in the cache, the method getUserData will be executed, which will simulate fetching data from a database.

  4. The retrieved data will be cached using the specified cache name ("userData") and cache key (based on the user ID).

  5. Subsequent requests with the same user ID will retrieve the result from the cache without executing the method again.

To add a caching provider like Redis to your Spring Boot application, you'll need to follow specific steps for each provider.(likely you can add Ehcache or Hazelcast to spring)

Adding Redis as the Caching Provider

Step 1: Add Redis Dependencies

Open your pom.xml file and add the following dependencies for Redis:

<dependencies>
    <!-- Other dependencies -->

    <!-- Add Redis Dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

Step 2: Configure Redis in application.properties or application.yml

Open your application.properties or application.yml file and add the following properties to configure Redis:

For application.properties:

propertiesCopy code# Redis Configuration
spring.redis.host=localhost
spring.redis.port=6379

For application.yml:

yamlCopy code# Redis Configuration
spring:
  redis:
    host: localhost
    port: 6379

These configurations assume that Redis is running on the local machine with the default port (6379). Adjust the properties as needed for your specific Redis setup.

Step 3: Enable Caching

If you've already added @EnableCaching to your main-application class (as in the previous example), you're all set.

Step 4: Test with Redis

Now, when you run your Spring Boot application, it will use Redis as the caching provider.

Caching Annotations:

  1. @Cacheable:

    • Indicates that a method's result should be cached for a specific cache name.

    • If the same method is called again with the same arguments, the cached result is returned instead of re-executing the method.

Example:

    @Cacheable(value = "userData", key = "#userId")
    public UserData getUserData(int userId) {
        // ...
    }
  1. @CachePut:

    • Forces a method to execute and update the cache with the result.

    • It's used for scenarios where you want to ensure that a method always updates the cache.

Example:

    @CachePut(value = "userData", key = "#userId")
    public UserData updateUser(int userId, UserData newData) {
        // ...
    }
  1. @CacheEvict:

    • Removes entries from the cache.

    • It's used when you want to explicitly evict (invalidate) cached entries.

Example:

    @CacheEvict(value = "userData", key = "#userId")
    public void deleteUser(int userId) {
        // ...
    }
  1. @Caching:

    • Allows multiple caching annotations to be applied to the same method.

Example:

    @Caching(
        cacheable = @Cacheable(value = "userData", key = "#userId"),
        put = @CachePut(value = "userData", key = "#userId")
    )
    public UserData getUserData(int userId) {
        // ...
    }

Cache Manager Methods:

  1. getCache(String name):

    • Retrieves the named cache.

Example:

    Cache userDataCache = cacheManager.getCache("userData");
  1. getCacheNames():

    • Returns the names of all caches.

Example:

    Set<String> cacheNames = cacheManager.getCacheNames();

When to Use These Annotations and Methods:

  1. @Cacheable:

    • Use when you want to cache the result of a method that has expensive computation or database access.

    • It's suitable for read-heavy operations where the data doesn't change frequently.

  2. @CachePut:

    • Use when you want to ensure a method always updates the cache, regardless of whether the result was previously cached.
  3. @CacheEvict:

    • Use when you want to explicitly evict (invalidate) cached entries, such as after a data update or deletion.
  4. @Caching:

    • Use when you want to apply multiple caching annotations to the same method.
  5. Cache Manager Methods:

    • Use when you need to interact with the cache manager programmatically, for example, to retrieve a specific cache or get a list of cache names.

Example Usage:

@Cacheable(value = "userData", key = "#userId")
public UserData getUserData(int userId) {
   // Simulate fetching data from a database
   return fetchDataFromDatabase(userId);
}

@CachePut(value = "userData", key = "#userId")
public UserData updateUser(int userId, UserData newData) {
   // Simulate updating data in a database
   return updateDataInDatabase(userId, newData);
}

@CacheEvict(value = "userData", key = "#userId")
public void deleteUser(int userId) {
   // Simulate deleting data from a database
   deleteDataFromDatabase(userId);
}

In this example, getUserData fetches user data from a database and caches the result. updateUser updates the user data in the database and ensures that the cache is updated. deleteUser deletes the user data from the database and evicts it from the cache.


More

In the @Cacheable annotation you provided:

javaCopy code@Cacheable(key = "#id", value = "Product", unless = "#result.price < 1000")
public Product findProduct(@PathVariable int id) {
    return productService.findProductById(id);
}
  • key = "#id": This specifies that the id parameter of the method will be used as the cache key. This means that if the method is called with the same id, it will return the cached result if available.

  • value = "Product": This indicates the name of the cache where the result will be stored. In this case, it's set to "Product".

  • unless = "#result.price < 1000": This is a condition that, if evaluated to true, will prevent the result from being cached. In this case, it means that if the price of the returned Product is less than 1000, the result won't be cached.

    • #result: This is a SpEL (Spring Expression Language) expression referring to the return value of the method. In this case, it refers to the Product object returned by findProductById(id).

    • #result.price < 1000: This is a condition in SpEL. It checks if the price of the returned Product is less than 1000.

Putting it all together, this unless condition means that if the Product returned has a price less than 1000, the caching won't be applied, and the method will be executed every time it's called with that id.

For example, if you have a product with a price of 800, it won't be cached because it meets the condition #result.price < 1000. However, if you have a product with a price of 1200, it will be cached and subsequent calls with the same id will return the cached result without executing the method.

For demo GitHub. Have fun learning🚀!!