Статьи

Spring boot и Cache Abstraction

Кэширование является основным компонентом большинства приложений, и пока мы пытаемся избежать доступа к диску, оно будет оставаться сильным. Spring имеет отличную поддержку для кэширования с широким спектром конфигураций. Вы можете начать как угодно просто и перейти к чему-то гораздо более настраиваемому.

Это был бы пример с самой простой формой кэширования, которую предоставляет весна.
Spring поставляется по умолчанию с кэшем в памяти, который довольно прост в настройке.

Давайте начнем с нашего файла Gradle.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
group 'com.gkatzioura'
version '1.0-SNAPSHOT'
 
 
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.4.2.RELEASE")
    }
}
 
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
 
repositories {
    mavenCentral()
}
 
 
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-cache")
    compile("org.springframework.boot:spring-boot-starter")
    testCompile("junit:junit")
}
 
bootRun {
    systemProperty "spring.profiles.active", "simple-cache"
}

Поскольку один и тот же проект будет использоваться для разных поставщиков кеша, будет несколько профилей пружин. Весенним профилем для этого урока будет простой кэш, поскольку мы собираемся использовать кэш на основе ConcurrentMap, который по умолчанию используется.

Мы реализуем приложение, которое будет извлекать информацию о пользователях из нашей локальной файловой системы. Информация должна находиться в файле users.json

1
2
3
4
5
6
[
  {"userName":"user1","firstName":"User1","lastName":"First"},
  {"userName":"user2","firstName":"User2","lastName":"Second"},
  {"userName":"user3","firstName":"User3","lastName":"Third"},
  {"userName":"user4","firstName":"User4","lastName":"Fourth"}
]

Также мы определим простую модель для данных, которые будут получены.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.gkatzioura.caching.model;
 
/**
 * Created by gkatzioura on 1/5/17.
 */
public class UserPayload {
 
    private String userName;
    private String firstName;
    private String lastName;
 
    public String getUserName() {
        return userName;
    }
 
    public void setUserName(String userName) {
        this.userName = userName;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

Затем мы добавим бин, который будет читать информацию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.gkatzioura.caching.config;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gkatzioura.caching.model.UserPayload;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
 
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
 
/**
 * Created by gkatzioura on 1/5/17.
 */
@Configuration
@Profile("simple-cache")
public class SimpleDataConfig {
 
    @Autowired
    private ObjectMapper objectMapper;
 
    @Value("classpath:/users.json")
    private Resource usersJsonResource;
 
    @Bean
    public List<UserPayload> payloadUsers() throws IOException {
 
        try(InputStream inputStream = usersJsonResource.getInputStream()) {
 
            UserPayload[] payloadUsers = objectMapper.readValue(inputStream,UserPayload[].class);
            return Collections.unmodifiableList(Arrays.asList(payloadUsers));
        }
    }
}

Очевидно, что для доступа к информации мы будем использовать экземпляр компонента, содержащий всю пользовательскую информацию.

Следующим шагом будет создание интерфейса репозитория для указания методов, которые будут использоваться.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.gkatzioura.caching.repository;
 
import com.gkatzioura.caching.model.UserPayload;
 
import java.util.List;
 
/**
 * Created by gkatzioura on 1/6/17.
 */
public interface UserRepository {
 
    List<UserPayload> fetchAllUsers();
 
    UserPayload firstUser();
 
    UserPayload userByFirstNameAndLastName(String firstName,String lastName);
 
}

Теперь давайте углубимся в реализацию, которая будет содержать необходимые аннотации кеша.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.gkatzioura.caching.repository;
 
import com.gkatzioura.caching.model.UserPayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
 
import java.util.List;
import java.util.Optional;
 
/**
 * Created by gkatzioura on 12/30/16.
 */
@Repository
@Profile("simple-cache")
public class UserRepositoryLocal implements UserRepository {
 
    @Autowired
    private List<UserPayload> payloadUsers;
 
    private static final Logger LOGGER = LoggerFactory.getLogger(UserRepositoryLocal.class);
 
    @Override
    @Cacheable("alluserscache")
    public List<UserPayload> fetchAllUsers() {
 
        LOGGER.info("Fetching all users");
 
        return payloadUsers;
    }
 
    @Override
    @Cacheable(cacheNames = "usercache",key = "#root.methodName")
    public UserPayload firstUser() {
 
        LOGGER.info("fetching firstUser");
 
        return payloadUsers.get(0);
    }
 
    @Override
    @Cacheable(cacheNames = "usercache",key = "{#firstName,#lastName}")
    public UserPayload userByFirstNameAndLastName(String firstName,String lastName) {
 
        LOGGER.info("fetching user by firstname and lastname");
 
        Optional<UserPayload> user = payloadUsers.stream().filter(
                p-> p.getFirstName().equals(firstName)
                &&p.getLastName().equals(lastName))
                .findFirst();
 
        if(user.isPresent()) {
            return user.get();
        } else {
            return null;
        }
    }
 
}

Методы, содержащие @Cacheable, будут запускать заполнение кэша, в отличие от методов, которые содержат @CacheEvict, которые инициируют удаление кэша. Используя @Cacheable вместо того, чтобы просто указывать карту кэша, в которой будут храниться наши значения, мы можем перейти также к указанию ключей на основе имени метода или аргументов метода.

Таким образом мы добиваемся метода кеширования. Например, метод firstUser использует в качестве ключа имя метода, а метод userByFirstNameAndLastName использует аргументы метода для создания ключа.

Два метода с аннотацией @CacheEvict очистят указанные кеши.

LocalCacheEvict будет компонентом, который будет обрабатывать выселение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.gkatzioura.caching.repository;
 
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
 
/**
 * Created by gkatzioura on 1/7/17.
 */
@Component
@Profile("simple-cache")
public class LocalCacheEvict {
 
    @CacheEvict(cacheNames = "alluserscache",allEntries = true)
    public void evictAllUsersCache() {
 
    }
 
    @CacheEvict(cacheNames = "usercache",allEntries = true)
    public void evictUserCache() {
 
    }
 
}

Поскольку мы используем очень простую форму кеша, ttl выселение не поддерживается. Поэтому мы добавим планировщик только для этого конкретного случая, который будет удалять кеш через определенный промежуток времени.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.gkatzioura.caching.scheduler;
 
import com.gkatzioura.caching.repository.LocalCacheEvict;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
 
/**
 * Created by gkatzioura on 1/7/17.
 */
@Component
@Profile("simple-cache")
public class EvictScheduler {
 
    @Autowired
    private LocalCacheEvict localCacheEvict;
 
    private static final Logger LOGGER = LoggerFactory.getLogger(EvictScheduler.class);
 
    @Scheduled(fixedDelay=10000)
    public void clearCaches() {
 
        LOGGER.info("Invalidating caches");
 
        localCacheEvict.evictUserCache();
        localCacheEvict.evictAllUsersCache();
    }
 
 
}

Для подведения итогов мы будем использовать контроллер для вызова указанных методов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.gkatzioura.caching.controller;
 
import com.gkatzioura.caching.model.UserPayload;
import com.gkatzioura.caching.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.List;
 
/**
 * Created by gkatzioura on 12/30/16.
 */
@RestController
public class UsersController {
 
    @Autowired
    private UserRepository userRepository;
 
    @RequestMapping(path = "/users/all",method = RequestMethod.GET)
    public List<UserPayload> fetchUsers() {
 
        return userRepository.fetchAllUsers();
    }
 
    @RequestMapping(path = "/users/first",method = RequestMethod.GET)
    public UserPayload fetchFirst() {
        return userRepository.firstUser();
    }
 
    @RequestMapping(path = "/users/",method = RequestMethod.GET)
    public UserPayload findByFirstNameLastName(String firstName,String lastName ) {
 
        return userRepository.userByFirstNameAndLastName(firstName,lastName);
    }
 
}

И последнее, но не менее важное: наш класс Application должен содержать две дополнительные аннотации. @EnableScheduling необходим для включения планировщиков и @EnableCaching для включения кеширования

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.gkatzioura.caching;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
 
/**
 * Created by gkatzioura on 12/30/16.
 */
@SpringBootApplication
@EnableScheduling
@EnableCaching
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
 
}

Вы можете найти исходный код на github .

Ссылка: Spring boot и Cache Abstraction от нашего партнера по JCG Эммануила Гкатзиураса в блоге gkatzioura .