/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cache.interceptor;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.junit.Assert.*;
/**
* Tests corner case of using {@link Cacheable} and {@link CachePut} on the
* same operation.
*
* @author Stephane Nicoll
*/
public class CachePutEvaluationTests {
private ConfigurableApplicationContext context;
private Cache cache;
private SimpleService service;
@Before
public void setup() {
this.context = new AnnotationConfigApplicationContext(Config.class);
this.cache = this.context.getBean(CacheManager.class).getCache("test");
this.service = this.context.getBean(SimpleService.class);
}
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void mutualGetPutExclusion() {
String key = "1";
Long first = this.service.getOrPut(key, true);
Long second = this.service.getOrPut(key, true);
assertSame(first, second);
// This forces the method to be executed again
Long expected = first + 1;
Long third = this.service.getOrPut(key, false);
assertEquals(expected, third);
Long fourth = this.service.getOrPut(key, true);
assertSame(third, fourth);
}
@Test
public void getAndPut() {
this.cache.clear();
long key = 1;
Long value = this.service.getAndPut(key);
assertEquals("Wrong value for @Cacheable key", value, this.cache.get(key).get());
assertEquals("Wrong value for @CachePut key", value, this.cache.get(value + 100).get()); // See @CachePut
// CachePut forced a method call
Long anotherValue = this.service.getAndPut(key);
assertNotSame(value, anotherValue);
// NOTE: while you might expect the main key to have been updated, it hasn't. @Cacheable operations
// are only processed in case of a cache miss. This is why combining @Cacheable with @CachePut
// is a very bad idea. We could refine the condition now that we can figure out if we are going
// to invoke the method anyway but that brings a whole new set of potential regressions.
//assertEquals("Wrong value for @Cacheable key", anotherValue, cache.get(key).get());
assertEquals("Wrong value for @CachePut key", anotherValue, this.cache.get(anotherValue + 100).get());
}
@Configuration
@EnableCaching
static class Config extends CachingConfigurerSupport {
@Bean
@Override
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
public SimpleService simpleService() {
return new SimpleService();
}
}
@CacheConfig(cacheNames = "test")
public static class SimpleService {
private AtomicLong counter = new AtomicLong();
/**
* Represent a mutual exclusion use case. The boolean flag exclude one of the two operation.
*/
@Cacheable(condition = "#p1", key = "#p0")
@CachePut(condition = "!#p1", key = "#p0")
public Long getOrPut(Object id, boolean flag) {
return this.counter.getAndIncrement();
}
/**
* Represent an invalid use case. If the result of the operation is non null, then we put
* the value with a different key. This forces the method to be executed every time.
*/
@Cacheable
@CachePut(key = "#result + 100", condition = "#result != null")
public Long getAndPut(long id) {
return this.counter.getAndIncrement();
}
}
}