I have to cache the result of the following public method :
#Cacheable(value = "tasks", key = "#user.username")
public Set<MyPojo> retrieveCurrentUserTailingTasks(UserInformation user) {
Set<MyPojo> resultSet;
try {
nodeInformationList = taskService.getTaskList(user);
} catch (Exception e) {
throw new ApiException("Error while retrieving tailing tasks", e);
}
return resultSet;
}
I also configured Caching here :
#Configuration
#EnableCaching(mode = AdviceMode.PROXY)
public class CacheConfig {
#Bean
public CacheManager cacheManager() {
final SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("tasks"),new ConcurrentMapCache("templates")));
return cacheManager;
}
#Bean
public CacheResolver cacheResolver() {
final SimpleCacheResolver cacheResolver = new SimpleCacheResolver(cacheManager());
return cacheResolver;
}
}
I assert the following :
Cache is initialized and does exist within Spring Context
I used jvisualvm to track ConcurrentMapCache (2 instances), they are
there in the heap but empty
Method returns same values per user.username
I tried the same configuration using spring-boot based project and
it worked
The method is public and is inside a Spring Controller
The annotation #CacheConfig(cacheNames = "tasks") added on top of my
controller
Spring version 4.1.3.RELEASE
Jdk 1.6
Update 001 :
#RequestMapping(value = "/{kinematicId}/status/{status}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public DocumentNodeWrapper getDocumentsByKinematicByStatus(#PathVariable String kinematicId, #PathVariable String status, HttpServletRequest request) {
UserInformation user = getUserInformation(request);
Set<ParapheurNodeInformation> nodeInformationList = retrieveCurrentUserTailingTasks(user);
final List<DocumentNodeVO> documentsList = getDocumentsByKinematic(kinematicId, user, nodeInformationList);
List<DocumentNodeVO> onlyWithGivenStatus = filterByStatus(documentsList);
return new DocumentNodeWrapper("filesModel", onlyWithGivenStatus, user, currentkinematic);
}
Thanks
Is the calling method getDocumentsByKinematicByStatus() in the same bean as the cacheable method ? If true, then this is a normal behavior because you're not calling the cacheable method via proxy but directly.
Related
I am trying to use #Cacheable to cache the roles regardless of the parameter. But the #Cacheable does not quite work and the method would get called twice.
CachingConfig:
#Configuration
#EnableCaching
public class CachingConfig {
#Bean
public CacheManager cacheManager(#Value("${caching.ttl.period}") long period,
#Value("${caching.ttl.unit}") String unit) {
return new ConcurrentMapCacheManager() {
#Override
public Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name, CacheBuilder.newBuilder()
.expireAfterWrite(period, TimeUnit.valueOf(unit)).build().asMap(), true);
}
};
}
}
RoleMappingService:
#Service
public class RoleMappingService {
private final AdminClient adminClient;
public RoleMappingService(AdminClient adminClient) {
this.adminClient = adminClient;
}
#Cacheable(value = "allRoles", key = "#root.method")
public List<Role> getAllRoles(String sessionToken) {
AdminSession adminSession = new AdminSession();
AdminSession.setSessionToken(sessionToken);
List<RoleGroup> allRoleGroups = this.adminClient.getAllRoleGroups(adminSession)
.orElse(Collections.emptyList());
List<Role> allRoles = allRoleGroups
.stream()
.map(RoleGroup::getRoles)
.flatMap(List::stream)
.collect(Collectors.toList());
return allRoles;
}
Test:
#SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RoleCachingTest {
private final JFixture fixture = new JFixture();
private AdminClient adminClient = mock(AdminClient.class);
#Test
public void allRolesShouldBeCached(){
RoleGroup mockRoleGroup = mock(RoleGroup.class);
Role mockRole = this.fixture.create(Role.class);
when(this.adminClient.getAllRoleGroups(any(AdminSession.class)))
.thenReturn(Optional.of(Arrays.asList(mockRoleGroup)));
when(mockRoleGroup.getRoles()).thenReturn(Arrays.asList(mockRole));
RoleMappingService sut = new RoleMappingService(adminClient);
List<Role> firstRes = sut.getAllRoles(
fixture.create(String.class));
List<Role> secondRes = sut.getAllRoles(
fixture.create(String.class));
assertEquals(firstRes.size(), secondRes.size());
assertEquals(firstRes.get(0).getId(), secondRes.get(0).getId());
assertEquals(firstRes.get(0).getRoleName(), secondRes.get(0).getRoleName());
// The getAllRoleGroups() should not be called on the second call
verify(this.adminClient, times(1)).getAllRoleGroups(any(AdminSession.class));
}
The adminClient.getAllRoleGroups() would always get called twice in this test, while I expect it would only get called once because of #Cacheable.
The project structure:
project structure
I think your #Cacheable annotation is not working because you have not specified Interface for class. This is because of proxy created for caching by Spring. Spring has specified below in its documentation . I thing you have not specified proxy-target-class, it means it will be default to false. If it is false it will use JDK interface based proxies. But in your case you class i.e. RollMappingService is not implementing interface. Create interface RollMappingService with method getAllRoles and implement it, will sole your problem.
Controls what type of caching proxies are created for classes annotated with the #Cacheable or #CacheEvict annotations. If the proxy-target-class attribute is set to true, then class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, then standard JDK interface-based proxies are created. (See Section 9.6, “Proxying mechanisms” for a detailed examination of the different proxy types.)
Also modify your test class to create Spring bean for RoleMappingService in following ways and inject mock of AdminClient into it
#Mock
private AdminClient mockedAdminClient;
#InjectMocks
#Autowired
private RoleMappingService roleMappingService
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(roleMappingService,
"adminClient",
mockedAdminClient);
}
I am attempting to use Spring Boot Cache with a Caffeine cacheManager.
I have injected a service class into a controller like this:
#RestController
#RequestMapping("property")
public class PropertyController {
private final PropertyService propertyService;
#Autowired
public PropertyController(PropertyService propertyService) {
this.propertyService = propertyService;
}
#PostMapping("get")
public Property getPropertyByName(#RequestParam("name") String name) {
return propertyService.get(name);
}
}
and the PropertyService looks like this:
#CacheConfig(cacheNames = "property")
#Service
public class PropertyServiceImpl implements PropertyService {
private final PropertyRepository propertyRepository;
#Autowired
public PropertyServiceImpl(PropertyRepository propertyRepository) {
this.propertyRepository = propertyRepository;
}
#Override
public Property get(#NonNull String name, #Nullable String entity, #Nullable Long entityId) {
System.out.println("inside: " + name);
return propertyRepository.findByNameAndEntityAndEntityId(name, entity, entityId);
}
#Cacheable
#Override
public Property get(#NonNull String name) {
return get(name, null, null);
}
}
Now, when I call the RestController get endpoint and supply a value for the name, every request ends up doing inside the method that should be getting cached.
However, if I call the controller get endpoint but pass a hardcoded String into the service class method, like this:
#PostMapping("get")
public Property getPropertyByName(#RequestParam("name") String name) {
return propertyService.get("hardcoded");
}
Then the method is only invoked the first time, but not on subsequent calls.
What's going on here? Why is it not caching the method call when I supply a value dynamically?
Here is some configuration:
#Configuration
public class CacheConfiguration {
#Bean
public CacheManager cacheManager() {
val caffeineCacheManager = new CaffeineCacheManager("property", "another");
caffeineCacheManager.setCaffeine(caffeineCacheBuilder());
return caffeineCacheManager;
}
public Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(500)
.weakKeys()
.recordStats();
}
}
2 solutions (they work for me):
remove .weakKeys()
propertyService.get(name.intern()) - wouldn't really do that, possibly a big cost
Sorry, but I don't have enough knowledge to explain this. Probably something to do with internal key representation by Caffeine.
I am currently using the JHipster generator for really boiler plate code which involves HazelCast as a second level cache. I was able to get Multi-tenancy (schema per tenant) working with a header based tenant context. The problem I have now, is that the #Cacheable annotations all share a context. If the cache is hot, I end up with cross-schema data. For example, tenant1 pulls all records from their table which is cached. Tenant 2 goes to pull the same items from their table, the cache is read, and it never goes to the actual tenant db. An easy fix would be disable caching all together but I would like to not do that. I can not for the life of me figure out how to make hazelcast aware of the tenant context - documentation is lacking. Some others have solved this with using custom name resolvers but it doesn't appear to be as dynamic as I was hoping (i.e. you have to know all of the tenants ahead of time). Thoughts?
Current cache config:
#Configuration
#EnableCaching
public class CacheConfiguration implements DisposableBean {
private final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);
private final Environment env;
private final ServerProperties serverProperties;
private final DiscoveryClient discoveryClient;
private Registration registration;
public CacheConfiguration(Environment env, ServerProperties serverProperties, DiscoveryClient discoveryClient) {
this.env = env;
this.serverProperties = serverProperties;
this.discoveryClient = discoveryClient;
}
#Autowired(required = false)
public void setRegistration(Registration registration) {
this.registration = registration;
}
#Override
public void destroy() throws Exception {
log.info("Closing Cache Manager");
Hazelcast.shutdownAll();
}
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
log.debug("Starting HazelcastCacheManager");
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
}
#Bean
public HazelcastInstance hazelcastInstance(JHipsterProperties jHipsterProperties) {
log.debug("Configuring Hazelcast");
HazelcastInstance hazelCastInstance = Hazelcast.getHazelcastInstanceByName("SampleApp");
if (hazelCastInstance != null) {
log.debug("Hazelcast already initialized");
return hazelCastInstance;
}
Config config = new Config();
config.setInstanceName("SampleApp");
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
if (this.registration == null) {
log.warn("No discovery service is set up, Hazelcast cannot create a cluster.");
} else {
// The serviceId is by default the application's name,
// see the "spring.application.name" standard Spring property
String serviceId = registration.getServiceId();
log.debug("Configuring Hazelcast clustering for instanceId: {}", serviceId);
// In development, everything goes through 127.0.0.1, with a different port
if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) {
log.debug("Application is running with the \"dev\" profile, Hazelcast " +
"cluster will only work with localhost instances");
System.setProperty("hazelcast.local.localAddress", "127.0.0.1");
config.getNetworkConfig().setPort(serverProperties.getPort() + 5701);
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
String clusterMember = "127.0.0.1:" + (instance.getPort() + 5701);
log.debug("Adding Hazelcast (dev) cluster member {}", clusterMember);
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
}
} else { // Production configuration, one host per instance all using port 5701
config.getNetworkConfig().setPort(5701);
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
for (ServiceInstance instance : discoveryClient.getInstances(serviceId)) {
String clusterMember = instance.getHost() + ":5701";
log.debug("Adding Hazelcast (prod) cluster member {}", clusterMember);
config.getNetworkConfig().getJoin().getTcpIpConfig().addMember(clusterMember);
}
}
}
config.getMapConfigs().put("default", initializeDefaultMapConfig(jHipsterProperties));
// Full reference is available at: http://docs.hazelcast.org/docs/management-center/3.9/manual/html/Deploying_and_Starting.html
config.setManagementCenterConfig(initializeDefaultManagementCenterConfig(jHipsterProperties));
config.getMapConfigs().put("com.test.sampleapp.domain.*", initializeDomainMapConfig(jHipsterProperties));
return Hazelcast.newHazelcastInstance(config);
}
private ManagementCenterConfig initializeDefaultManagementCenterConfig(JHipsterProperties jHipsterProperties) {
ManagementCenterConfig managementCenterConfig = new ManagementCenterConfig();
managementCenterConfig.setEnabled(jHipsterProperties.getCache().getHazelcast().getManagementCenter().isEnabled());
managementCenterConfig.setUrl(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUrl());
managementCenterConfig.setUpdateInterval(jHipsterProperties.getCache().getHazelcast().getManagementCenter().getUpdateInterval());
return managementCenterConfig;
}
private MapConfig initializeDefaultMapConfig(JHipsterProperties jHipsterProperties) {
MapConfig mapConfig = new MapConfig();
/*
Number of backups. If 1 is set as the backup-count for example,
then all entries of the map will be copied to another JVM for
fail-safety. Valid numbers are 0 (no backup), 1, 2, 3.
*/
mapConfig.setBackupCount(jHipsterProperties.getCache().getHazelcast().getBackupCount());
/*
Valid values are:
NONE (no eviction),
LRU (Least Recently Used),
LFU (Least Frequently Used).
NONE is the default.
*/
mapConfig.setEvictionPolicy(EvictionPolicy.LRU);
/*
Maximum size of the map. When max size is reached,
map is evicted based on the policy defined.
Any integer between 0 and Integer.MAX_VALUE. 0 means
Integer.MAX_VALUE. Default is 0.
*/
mapConfig.setMaxSizeConfig(new MaxSizeConfig(0, MaxSizeConfig.MaxSizePolicy.USED_HEAP_SIZE));
return mapConfig;
}
private MapConfig initializeDomainMapConfig(JHipsterProperties jHipsterProperties) {
MapConfig mapConfig = new MapConfig();
mapConfig.setTimeToLiveSeconds(jHipsterProperties.getCache().getHazelcast().getTimeToLiveSeconds());
return mapConfig;
}
}
Sample Repository using cacheNames...
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
String USERS_BY_LOGIN_CACHE = "usersByLogin";
String USERS_BY_EMAIL_CACHE = "usersByEmail";
String USERS_BY_ID_CACHE = "usersById";
Optional<User> findOneByActivationKey(String activationKey);
List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime);
Optional<User> findOneByResetKey(String resetKey);
Optional<User> findOneByEmailIgnoreCase(String email);
Optional<User> findOneByLogin(String login);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_ID_CACHE)
Optional<User> findOneWithRolesById(Long id);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
Optional<User> findOneWithRolesByLogin(String login);
#EntityGraph(attributePaths = { "roles", "roles.permissions" })
#Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
Optional<User> findOneWithRolesAndPermissionsByLogin(String login);
#EntityGraph(attributePaths = "roles")
#Cacheable(cacheNames = USERS_BY_EMAIL_CACHE)
Optional<User> findOneWithRolesByEmail(String email);
Page<User> findAllByLoginNot(Pageable pageable, String login);
}
I am using tenant per database (MySQL), but as long as you are setting a thread context above here is what I'm doing - I'm using Spring Boot. I've created a custom Cache Key generator which combines the tenant name + class + and method. You can really choose any combination. Whenever I pass that tenant back it pulls the correct entries. In the Hazelcast command center for my AppointmentType map type I see the number of entries increment per tenant.
Some other references that may be helpful:
https://www.javadevjournal.com/spring/spring-cache-custom-keygenerator/
https://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/cache.html (search for keyGenerator="myKeyGenerator")
In your class where you want to cache (mine is a service class):
#Service
public class AppointmentTypeService {
private static final Logger LOGGER = LoggerFactory.getLogger(AppointmentTypeService.class);
private final AppointmentTypeRepository appointmentTypeRepository;
#Autowired
AppointmentTypeService(AppointmentTypeRepository appointmentTypeRepository) {
this.appointmentTypeRepository = appointmentTypeRepository;
}
//ADD keyGenerator value. Name is the name of the bean of the class
#Cacheable(value="appointmentType", keyGenerator = "multiTenantCacheKeyGenerator")
public List<AppointmentType> list() {
return this.appointmentTypeRepository.findAll();
}
#CacheEvict(value="appointmentType", allEntries=true)
public Long create(AppointmentType request) {
this.appointmentTypeRepository.saveAndFlush(request);
return request.getAppointmentTypeId();
}
#CacheEvict(value="appointmentType", allEntries=true)
public void delete(Long id) {
this.appointmentTypeRepository.deleteById(id);
}
public Optional<AppointmentType> findById(Long id) {
return this.appointmentTypeRepository.findById(id);
}
}
Create key generator class
//setting the bean name here
#Component("multiTenantCacheKeyGenerator")
public class MultiTenantCacheKeyGenerator implements KeyGenerator {
#Override
public Object generate(Object o, Method method, Object... os) {
StringBuilder sb = new StringBuilder();
sb.append(TenantContext.getCurrentTenantInstanceName()) //my tenant context class which is using local thread. I set the value in the Spring filter.
.append("_")
.append(o.getClass().getSimpleName())
.append("-")
.append(method.getName());
}
return sb.toString();
}
}
One approach to defining different cache keys for the tenants is to override the method getCache in org.springframework.cache.CacheManager, as suggested here: Extended spring cache...
As of Jhipster 7.0.1, the CacheManager for Hazelcast is defined in the class CacheConfiguration as stated bellow:
#Configuration
#EnableCaching
public class CacheConfiguration {
//...
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance);
}
//...
}
To have the cache keys prefixed with the tenant id, the following code may be used as a starting point:
#Configuration
#EnableCaching
public class CacheConfiguration {
#Bean
public CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new com.hazelcast.spring.cache.HazelcastCacheManager(hazelcastInstance){
#Override
public Cache getCache(String name) {
String tenantId = TenantStorage.getTenantId();
if (StringUtils.isNotBlank(tenantId)){
return super.getCache(String.format("%s:%s", tenantId, name));
}
return super.getCache(name);
}
};
}
}
Note: in the above code, TenantStorage.getTenantId() is a static function one should implement and that returns the current tenant id.
Consider the class posted by the OP:
#Cacheable(cacheNames = "usersByLogin")
Optional<User> findOneWithRolesByLogin(String login);
The following cache values will be used by HazelCast:
tenant1 => tenant1:usersByLogin
tenant2 => tenant2:usersByLogin
null => usersByLogin
Here is the behavior I wish to achieve:
#Transactional(rollbackFor = NullPointerException.class)
#PostMapping(consumes = {"application/json"})
public Employee createEmployee(#Valid #RequestBody Employee employee) {
Optional<Department> departmentOptional = departmentRepository.findById(employee.getDeptId());
EmployeeAggregate employeeAggregate = employeeAggregateRepository.findAll().get(0);
employeeAggregate.setTotalEmployeeCount(employeeAggregate.getTotalEmployeeCount()+1);
employeeAggregateRepository.save(employeeAggregate); <--This line should be rolled back when exception is thrown.
if(departmentOptional.isPresent()) {
Department department = departmentOptional.get();
return employeeRepository.save(employee);
}
else{
throw new NullPointerException();
}
}
There are two documents, Employees, and EmployeeAggregate. I want to be able to rollback the employee count stored in EmployeeAggregate if I am unable to create an employee in Employees document.
Here is my mongo config file:
#Configuration
public class MongoConfig {
#Bean
#Autowired
#ConditionalOnExpression("'${mongo.transactions}'=='enabled'")
MongoTransactionManager mongoTransactionManager(MongoDbFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
public #Bean MongoClient mongoClient() {
return new MongoClient("localhost");
}
public #Bean
MongoTemplate mongoTemplate() {
return new MongoTemplate(mongoClient(), "EmployeeDatabase");
}
}
Currently, no rollback takes place if Employee creation fails. The count gets updated by one irrespective of successful employee creation or not. An explanation of multidocument transactions in spring boot would be appreciated, since the documentation is a bit confusing.
Service gets data from DB in constructor and store it in HashMap and then returns data from HashMap. Please take a look:
#RestController
#RequestMapping("/scheduler/api")
#Transactional(readOnly = true, transactionManager = "cnmdbTm")
public class RestApiController {
private final Set<String> cache;
#Autowired
public RestApiController(CNMDBFqdnRepository cnmdbRepository, CNMTSFqdnRepository cnmtsRepository) {
cache = new HashSet<>();
cache.addAll(getAllFqdn(cnmdbRepository.findAllFqdn()));
cache.addAll(getAllFqdn(cnmtsRepository.findAllFqdn()));
}
#RequestMapping(value = "/fqdn", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public List<SchedulerRestDto> checkFqdn(#RequestBody List<SchedulerRestDto> queryList) throws ExecutionException {
for (SchedulerRestDto item : queryList) {
item.setFound(1);
if (!cache.contains(item.getFqdn())) {
item.setFound(0);
}
}
return queryList;
}
private Set<String> getAllFqdn(List<String> fqdnList) {
Set<String> result = new HashSet<>();
for (String fqdn : fqdnList) {
result.add(fqdn);
}
return result;
}
}
But I always get a result in about 2sec. I thought it's a bit slowly for 35K string which I got from DB.
I tried to find out where problem is. I store serialized HashMap to file and modified constructor to:
#Autowired
public RestApiController(CNMDBFqdnRepository cnmdbRepository, CNMTSFqdnRepository cnmtsRepository) {
try (final InputStream fis = getResourceAsStream("cache-hashset.ser");
final ObjectInputStream ois = new ObjectInputStream(fis)) {
cache = (Set<String>) ois.readObject();
}
}
after that service returned result less than 100 ms.
I think it's related with DB but I don't know exactly how I can fix it.
Any ideas?
After several hours of experiments I realized that the main cause is annotation #Transactional on the class.
When I moved this annotation on a method, service returned response quicker. In my final decision I moved this annotation to a another class. New constructor is
#Autowired
public RestApiController(FqdnService fqdnService, SqsService sqsService) {
Objects.requireNonNull(fqdnService);
cache = fqdnService.getCache();
}
Code is cleaner and there aren't any performance issues.