BAEL-5696: Adds TTL configuration for caching (#12700)

Co-authored-by: Abdul Wahab <abdul.wahab@monese.com>
This commit is contained in:
Abdul Wahab 2022-09-09 00:57:48 +03:00 committed by GitHub
parent cbcf5d4050
commit 176111197a
32 changed files with 651 additions and 2 deletions

View File

@ -559,7 +559,8 @@
<module>spring-boot-rest</module>
<module>spring-caching</module>
<module>spring-caching-2</module>
<module>spring-caching-2/redis</module>
<module>spring-caching-2/ttl</module>
<module>spring-cloud-modules</module>
<!-- <module>spring-cloud-cli</module> --> <!-- Not a maven project -->

View File

@ -12,7 +12,7 @@
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent-boot-2</relativePath>
<relativePath>../../parent-boot-2/pom.xml</relativePath>
</parent>
<dependencies>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-caching-3</artifactId>
<version>0.1-SNAPSHOT</version>
<name>spring-caching-3</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,13 @@
package com.baeldung.caching.ttl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,34 @@
package com.baeldung.caching.ttl.config;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
public class GuavaCachingConfig<T> {
private Cache<Long, T> cache;
Logger logger = LoggerFactory.getLogger(GuavaCachingConfig.class);
public GuavaCachingConfig(int expiryDuration, TimeUnit timeUnit) {
cache = CacheBuilder.newBuilder()
.expireAfterWrite(expiryDuration, timeUnit)
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build();
}
public T get(Long key) {
return cache.getIfPresent(key);
}
public void add(Long key, T value) {
if(key != null && value != null) {
cache.put(key, value);
logger.info(
String.format("A %s record stored in Cache with key: %s", value.getClass().getSimpleName(), key));
}
}
}

View File

@ -0,0 +1,17 @@
package com.baeldung.caching.ttl.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class SpringCachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("hotels");
}
}

View File

@ -0,0 +1,36 @@
package com.baeldung.caching.ttl.controller;
import com.baeldung.caching.ttl.service.HotelService;
import com.baeldung.caching.ttl.model.Hotel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/hotel")
public class HotelController {
private final HotelService hotelService;
@Autowired
public HotelController(HotelService hotelService) {
this.hotelService = hotelService;
}
@GetMapping
@ResponseStatus(HttpStatus.OK)
public List<Hotel> getAllHotels() {
return hotelService.getAllHotels();
}
@GetMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public Hotel getHotelById(@PathVariable Long id) {
return hotelService.getHotelById(id);
}
}

View File

@ -0,0 +1,26 @@
package com.baeldung.caching.ttl.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.LinkedHashMap;
import java.util.Map;
@ControllerAdvice
public class ControllerAdvisor extends ResponseEntityExceptionHandler {
@ExceptionHandler(ElementNotFoundException.class)
public ResponseEntity<Object> handleNodataFoundException(
ElementNotFoundException ex) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("status", 404);
body.put("error", "Not Found");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.caching.ttl.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ElementNotFoundException extends RuntimeException {
private static final long serialVersionUID = -5218143265247846948L;
public ElementNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,65 @@
package com.baeldung.caching.ttl.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Objects;
@Entity
public class City implements Serializable {
private static final long serialVersionUID = 3252591505029724236L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double cityCentreLatitude;
private double cityCentreLongitude;
public City() {}
public City(Long id, String name, double cityCentreLatitude, double cityCentreLongitude) {
this.id = id;
this.name = name;
this.cityCentreLatitude = cityCentreLatitude;
this.cityCentreLongitude = cityCentreLongitude;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public double getCityCentreLatitude() {
return cityCentreLatitude;
}
public double getCityCentreLongitude() {
return cityCentreLongitude;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
City city = (City) o;
if (Double.compare(city.cityCentreLatitude, cityCentreLatitude) != 0) return false;
if (Double.compare(city.cityCentreLongitude, cityCentreLongitude) != 0) return false;
if (!Objects.equals(id, city.id)) return false;
return Objects.equals(name, city.name);
}
}

View File

@ -0,0 +1,131 @@
package com.baeldung.caching.ttl.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;
@Entity
public class Hotel implements Serializable {
private static final long serialVersionUID = 5560221391479816650L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double rating;
@ManyToOne(fetch = FetchType.EAGER)
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private City city;
private String address;
private double latitude;
private double longitude;
private boolean deleted = false;
public Hotel() {}
public Hotel(
Long id,
String name,
Double rating,
City city,
String address,
double latitude,
double longitude,
boolean deleted) {
this.id = id;
this.name = name;
this.rating = rating;
this.city = city;
this.address = address;
this.latitude = latitude;
this.longitude = longitude;
this.deleted = deleted;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getRating() {
return rating;
}
public void setRating(Double rating) {
this.rating = rating;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Hotel hotel = (Hotel) o;
if (Double.compare(hotel.latitude, latitude) != 0) return false;
if (Double.compare(hotel.longitude, longitude) != 0) return false;
if (deleted != hotel.deleted) return false;
if (!Objects.equals(id, hotel.id)) return false;
if (!Objects.equals(name, hotel.name)) return false;
if (!Objects.equals(rating, hotel.rating)) return false;
if (!Objects.equals(city, hotel.city)) return false;
return Objects.equals(address, hotel.address);
}
}

View File

@ -0,0 +1,6 @@
package com.baeldung.caching.ttl.repository;
import com.baeldung.caching.ttl.model.City;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CityRepository extends JpaRepository<City, Long> {}

View File

@ -0,0 +1,28 @@
package com.baeldung.caching.ttl.repository;
import com.baeldung.caching.ttl.model.Hotel;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface HotelRepository extends JpaRepository<Hotel, Long> {
default List<Hotel> getAllHotels() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return findAll();
}
default Optional<Hotel> getHotelById(Long hotelId) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return findById(hotelId);
}
}

View File

@ -0,0 +1,21 @@
package com.baeldung.caching.ttl.service;
import com.baeldung.caching.ttl.config.GuavaCachingConfig;
import com.baeldung.caching.ttl.model.Hotel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class GuavaCacheCustomizer {
@Value("${caching.guava.hotelItemTTL}")
Integer hotelItemTTL;
@Bean
public GuavaCachingConfig<Hotel> hotelGuavaCacheStore() {
return new GuavaCachingConfig<>(hotelItemTTL, TimeUnit.MILLISECONDS);
}
}

View File

@ -0,0 +1,52 @@
package com.baeldung.caching.ttl.service;
import com.baeldung.caching.ttl.repository.HotelRepository;
import com.baeldung.caching.ttl.config.GuavaCachingConfig;
import com.baeldung.caching.ttl.exception.ElementNotFoundException;
import com.baeldung.caching.ttl.model.Hotel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class HotelService {
private final HotelRepository hotelRepository;
private final GuavaCachingConfig<Hotel> hotelGuavaCachingConfig;
Logger logger = LoggerFactory.getLogger(HotelService.class);
HotelService(HotelRepository hotelRepository, GuavaCachingConfig<Hotel> hotelGuavaCachingConfig) {
this.hotelRepository = hotelRepository;
this.hotelGuavaCachingConfig = hotelGuavaCachingConfig;
}
@Cacheable("hotels")
public List<Hotel> getAllHotels() {
return hotelRepository.getAllHotels();
}
@CacheEvict(value = "hotels", allEntries = true)
@Scheduled(fixedRateString = "${caching.spring.hotelListTTL}")
public void emptyHotelsCache() {
logger.info("emptying Hotels cache");
}
public Hotel getHotelById(Long hotelId) {
if (hotelGuavaCachingConfig.get(hotelId) != null) {
logger.info(String.format("hotel with id: %s found in cache", hotelId));
return hotelGuavaCachingConfig.get(hotelId);
}
logger.info(String.format("hotel with id: %s is being searched in DB", hotelId));
Hotel hotel = hotelRepository.getHotelById(hotelId)
.orElseThrow(() -> new ElementNotFoundException(String.format("Hotel with id %s not found", hotelId)));
hotelGuavaCachingConfig.add(hotelId, hotel);
return hotel;
}
}

View File

@ -0,0 +1,16 @@
package com.baeldung.caching.ttl.service;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.stereotype.Component;
import static java.util.Arrays.asList;
@Component
public class SpringCacheCustomizer implements CacheManagerCustomizer<ConcurrentMapCacheManager> {
@Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(asList("hotels"));
}
}

View File

@ -0,0 +1,15 @@
spring:
jpa:
open-in-view: true
hibernate:
ddl-auto: create-drop
show-sql: false
server:
port: 8000
caching:
spring:
hotelListTTL: 43200
guava:
hotelItemTTL: 43200

View File

@ -0,0 +1,36 @@
SET REFERENTIAL_INTEGRITY FALSE;
TRUNCATE TABLE city;
TRUNCATE TABLE hotel;
SET REFERENTIAL_INTEGRITY TRUE;
ALTER TABLE city
ALTER COLUMN id RESTART WITH 1;
ALTER TABLE hotel
ALTER COLUMN id RESTART WITH 1;
INSERT INTO city(id, name, city_centre_latitude, city_centre_longitude)
VALUES (1, 'Amsterdam', 52.368780, 4.903303);
INSERT INTO city(id, name, city_centre_latitude, city_centre_longitude)
VALUES (2, 'Manchester', 53.481062, -2.237706);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Monaghan Hotel', false, 9.2, 1, 'Weesperbuurt en Plantage', 52.364799, 4.908971);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('The Thornton Council Hotel', false, 6.3, 1, 'Waterlooplein', 52.3681563, 4.9010029);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('McZoe Trescothiks Hotel', false, 9.8, 1, 'Oude Stad, Harlem', 52.379577, 4.633547);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Stay Schmtay Hotel', false, 8.7, 1, 'Jan van Galenstraat', 52.3756755, 4.8668628);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Fitting Image Hotel', false, NULL, 1, 'Staatsliedenbuurt', 52.380936, 4.8708297);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Raymond of Amsterdam Hotel', false, NULL, 1, '22 High Avenue', 52.3773989, 4.8846443);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('201 Deansgate Hotel', false, 7.3, 2, '201 Deansgate', 53.4788305, -2.2484721);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Fountain Street Hotel', true, 3.0, 2, '35 Fountain Street', 53.4811298, -2.2402227);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Sunlight House', false, 4.3, 2, 'Little Quay St', 53.4785129, -2.2505943);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('St Georges House', false, 9.6, 2, '56 Peter St', 53.477822, -2.2462002);
INSERT INTO hotel(name, deleted, rating, city_id, address, latitude, longitude)
VALUES ('Marriot Bonvoy', false, 9.6, 1, 'Hans Zimmerstraat', 53.477872, -2.2462003);

View File

@ -0,0 +1,71 @@
package com.baeldung.caching.ttl.controller;
import com.baeldung.caching.ttl.model.Hotel;
import com.baeldung.caching.ttl.repository.CityRepository;
import com.baeldung.caching.ttl.repository.HotelRepository;
import com.booking.testing.SlowTest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureMockMvc
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:data.sql")
@SlowTest
class HotelControllerIntegrationTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper mapper;
@Autowired private HotelRepository repository;
@Autowired private CityRepository cityRepository;
@Test
@DisplayName("When all hotels are requested then they are all returned")
void whenAllHotelsRequested_thenReturnAllHotels() throws Exception {
mockMvc
.perform(get("/hotel"))
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", hasSize((int) repository.findAll().stream().count())));
}
@Test
@DisplayName("When a hotel is requested by id then the hotel is returned")
void whenAGivenHotelsRequested_thenReturnTheHotel() throws Exception {
Long hotelId = 1L;
Hotel hotel =
mapper
.readValue(
mockMvc
.perform(
get("/hotel/" + hotelId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString(),
Hotel.class);
assertThat(
repository
.findById(hotelId)
.orElseThrow(() -> new IllegalStateException(String
.format("Hotel with id %s does not exist even in repository", hotelId))),
equalTo(hotel));
}
}

View File

@ -0,0 +1,4 @@
package com.booking.testing;
public @interface SlowTest {
}

View File

@ -0,0 +1,15 @@
server:
port: 8001
spring:
jpa:
open-in-view: true
hibernate:
ddl-auto: create-drop
show-sql: false
caching:
spring:
hotelListTTL: 43200
guava:
hotelItemTTL: 43200