├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── io │ └── github │ └── yantrashala │ └── springcache │ └── tools │ ├── CacheOperations.java │ ├── CacheSupportImpl.java │ ├── CachingAnnotationsAspect.java │ ├── InvocationRegistry.java │ ├── LoggingAspect.java │ └── ProfilingAspect.java └── test └── java └── io └── github └── yantrashala └── springcache └── tools └── TestCacheOperations.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.settings 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | /target/ 15 | *.project 16 | *.classpath 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-cache-self-refresh 2 | Given one or more methods annotated with the Spring's @Cacheable annotation. Allow the cached data to be refreshed behind the scene, while maintaining the already cached data. 3 | A use could be to shield your system against an unstable remote, using a scheduler to get any updates if the remote is available any time. 4 | 5 | Requirements: 6 | Already configured Spring cache abstraction, with or without any actual provider. 7 | 8 | To use: 9 | Create a pointcut to intercept the cacheable packages/classes/methods with io.github.yantrashala.springcache.tools.CachingAnnotationsAspect.interceptCacheables(ProceedingJoinPoint) 10 | This class register the invocations, keeping a copy of all arguments used for the invocation. 11 | CacheOperations class provides the refresh cache method that causes all cached invocations to be re-triggered, resulting in update of the cached values. 12 | 13 | Since the utility uses AOP, to run the Test case, please add a javaagent entry to your command line like -javaagent:${user.home}/.m2/repository/org/springframework/spring-agent/2.5.6/spring-agent-2.5.6.jar 14 | or simply 15 | -javaagent:spring-agent-2.5.6.jar if you have the jar in the same directory. 16 | 17 | 18 | Extra files: 19 | General purpose LoggingAspect and ProfilingAspect 20 | 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.sapient.engineering.tools 5 | cache-refresh 6 | 0.0.1-SNAPSHOT 7 | Cacherefresh 8 | Cache that keeps refreshing itself 9 | 10 | 5.1.8.RELEASE 11 | 1.7.0 12 | 4.10 13 | 0.999.13 14 | 1.7.2 15 | 2.2.2 16 | 1.0.1 17 | 2.2.2 18 | 1.9.0 19 | UTF-8 20 | 21 | 22 | 23 | 24 | uk.com.robust-it 25 | cloning 26 | ${cloning-robust-version} 27 | 28 | 29 | 30 | 31 | org.springframework 32 | spring-core 33 | ${spring.version} 34 | 35 | 36 | org.springframework 37 | spring-beans 38 | ${spring.version} 39 | 40 | 41 | org.springframework 42 | spring-aspects 43 | ${spring.version} 44 | 45 | 46 | org.springframework 47 | spring-context 48 | ${spring.version} 49 | 50 | 51 | org.springframework 52 | spring-aop 53 | ${spring.version} 54 | 55 | 56 | org.springframework 57 | spring-test 58 | ${spring.version} 59 | test 60 | 61 | 62 | 63 | org.aspectj 64 | aspectjrt 65 | ${org.aspectj-version} 66 | 67 | 68 | org.aspectj 69 | aspectjweaver 70 | ${org.aspectj-version} 71 | 72 | 73 | cglib 74 | cglib-nodep 75 | ${cglib-version} 76 | 77 | 78 | 79 | org.slf4j 80 | slf4j-api 81 | ${slf4j-version} 82 | 83 | 84 | org.slf4j 85 | jcl-over-slf4j 86 | ${slf4j-version} 87 | 88 | 89 | 90 | 91 | ch.qos.logback 92 | logback-classic 93 | ${logback.version} 94 | 95 | 96 | ch.qos.logback 97 | logback-core 98 | ${logback.version} 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-compiler-plugin 109 | 3.5.1 110 | 111 | false 112 | 1.8 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/CacheOperations.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * Provides methods to refresh cached objects. 7 | * 8 | * @author szaidi 9 | * @copyright @2016 http://yantrashala.github.io 10 | * @version 1.0 11 | */ 12 | public interface CacheOperations { 13 | 14 | /** 15 | * Returns all the configured cache names. 16 | * @return 17 | */ 18 | Collection getCacheNames(); 19 | 20 | /** 21 | * Refreshes caches corresponding to the supplied cache names array. 22 | * 23 | * @param cacheNames 24 | */ 25 | void refreshCaches(String... cacheNames); 26 | 27 | /** 28 | * Refreshes caches corresponding to the supplied cache name. 29 | * 30 | * @param cacheName 31 | */ 32 | void refreshCache(String cacheName); 33 | 34 | /** 35 | * Refreshes all caches configured in the application 36 | * 37 | * @param cacheName 38 | */ 39 | void refreshAllCaches(); 40 | 41 | /** 42 | * Clears all values from the named caches 43 | * 44 | * @param cacheNames 45 | */ 46 | void evictCache(String... cacheNames); 47 | 48 | /** 49 | * Clears all values from the named cache 50 | * 51 | * @param cacheName 52 | */ 53 | void evictCache(String cacheName); 54 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/CacheSupportImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.Arrays; 6 | import java.util.Collection; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.CopyOnWriteArraySet; 11 | 12 | import javax.annotation.PostConstruct; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.cache.Cache; 16 | import org.springframework.cache.CacheManager; 17 | import org.springframework.cache.interceptor.KeyGenerator; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.util.MethodInvoker; 20 | 21 | /** 22 | * Registers invocations of methods with @Cacheable annotations. 23 | * 24 | * @author Saiyed Zaidi 25 | * @copyright @2016 http://yantrashala.github.io 26 | * @version 1.0 27 | */ 28 | @Component("cacheSupport") 29 | public class CacheSupportImpl implements CacheOperations, InvocationRegistry { 30 | 31 | /** 32 | * Maintains Sets of CachedInvocation objects corresponding to each cache 33 | * configured in the application. At initialization, this map gets populated 34 | * with the cache name as the key and a hashSet as the value, for every 35 | * configured cache. 36 | */ 37 | private Map> cacheToInvocationsMap; 38 | 39 | /** 40 | * Avoid concurrent modification issues by using CopyOnWriteArraySet that 41 | * copies the internal array on every modification 42 | */ 43 | private final Set allInvocations = new CopyOnWriteArraySet(); 44 | 45 | @Autowired 46 | private CacheManager cacheManager; 47 | 48 | @Autowired 49 | private KeyGenerator keyGenerator; 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | @Override 55 | public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments, 56 | Set annotatedCacheNames) { 57 | Object key = keyGenerator.generate(targetBean, targetMethod, arguments); 58 | final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments); 59 | allInvocations.add(invocation); 60 | for (final String cacheName : annotatedCacheNames) { 61 | cacheToInvocationsMap.get(cacheName).add(invocation); 62 | } 63 | } 64 | 65 | /** 66 | * Creates a MethodInvoker instance from the cached invocation object and 67 | * invokes it to get the return value 68 | * 69 | * @param invocation 70 | * @return Return value resulted from the method invocation 71 | * @throws NoSuchMethodException 72 | * @throws ClassNotFoundException 73 | * @throws IllegalAccessException 74 | * @throws InvocationTargetException 75 | */ 76 | private Object execute(CachedInvocation invocation) 77 | throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 78 | final MethodInvoker invoker = new MethodInvoker(); 79 | invoker.setTargetObject(invocation.getTargetBean()); 80 | invoker.setArguments(invocation.getArguments()); 81 | invoker.setTargetMethod(invocation.getTargetMethod().getName()); 82 | invoker.prepare(); 83 | return invoker.invoke(); 84 | } 85 | 86 | /** 87 | * Initializes the storage objects in a optimum way based upon the number of 88 | * configured caches. Helps avoid creating Set objects on the fly and 89 | * related concurrency issues. Populates the cacheToInvocationsMap with the 90 | * cache name as the key and a hashSet as the value, for every configured 91 | * cache. Depends on CacheManager to get the configured cache names. 92 | */ 93 | @PostConstruct 94 | public void initialize() { 95 | cacheToInvocationsMap = new ConcurrentHashMap>( 96 | cacheManager.getCacheNames().size()); 97 | for (final String cacheName : cacheManager.getCacheNames()) { 98 | cacheToInvocationsMap.put(cacheName, new CopyOnWriteArraySet()); 99 | } 100 | } 101 | 102 | /** 103 | * Uses the supplied cached invocation details to invoke the target method 104 | * with appropriate arguments and update the relevant caches. Updates all 105 | * caches if the cacheName argument is null. 106 | * 107 | * @param invocation 108 | * @param cacheNames 109 | */ 110 | private void updateCache(CachedInvocation invocation, String... cacheNames) { 111 | String[] cacheNamesArray = cacheNames; 112 | boolean invocationSuccess; 113 | Object computed = null; 114 | try { 115 | computed = execute(invocation); 116 | invocationSuccess = true; 117 | } catch (final IllegalAccessException | ClassNotFoundException | NoSuchMethodException 118 | | InvocationTargetException e) { 119 | invocationSuccess = false; 120 | //TODO Invocation failed, log the issue, cache can not be updated 121 | } 122 | 123 | for(String cacheName: cacheManager.getCacheNames()){ 124 | Cache cache = cacheManager.getCache(cacheName); 125 | } 126 | 127 | if (invocationSuccess) { 128 | if (cacheNamesArray == null) { 129 | cacheNamesArray = cacheToInvocationsMap.keySet().toArray(new String[cacheToInvocationsMap.size()]); 130 | } 131 | for (final String cacheName : cacheNamesArray) { 132 | if (cacheToInvocationsMap.get(cacheName) != null) { 133 | cacheManager.getCache(cacheName).put(invocation.getKey(), computed); 134 | } 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * {@inheritDoc} 141 | */ 142 | @Override 143 | public void refreshAllCaches() { 144 | for (final CachedInvocation invocation : allInvocations) { 145 | updateCache(invocation, (String) null); 146 | } 147 | } 148 | 149 | /** 150 | * {@inheritDoc} 151 | */ 152 | @Override 153 | public void refreshCache(String cacheName) { 154 | if (cacheToInvocationsMap.get(cacheName) != null) { 155 | for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) { 156 | updateCache(invocation, cacheName); 157 | } 158 | } 159 | // Otherwise Wrong cache name, missing spring configuration for the 160 | // cache name used in annotations 161 | } 162 | 163 | /** 164 | * {@inheritDoc} 165 | */ 166 | @Override 167 | public void refreshCaches(String... cacheNames) { 168 | for (final String cacheName : cacheNames) { 169 | refreshCache(cacheName); 170 | } 171 | } 172 | 173 | /** 174 | * {@inheritDoc} 175 | */ 176 | public void evictCache(String... cacheNames) { 177 | if (cacheNames != null) { 178 | for (final String cacheName : cacheNames) { 179 | evictCache(cacheName); 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * {@inheritDoc} 186 | */ 187 | public void evictCache(String cacheName) { 188 | if (cacheName != null) { 189 | Cache cache = cacheManager.getCache(cacheName); 190 | if (cache != null) { 191 | cache.clear(); 192 | } 193 | } 194 | } 195 | 196 | public void setCacheManager(CacheManager cacheManager) { 197 | this.cacheManager = cacheManager; 198 | } 199 | 200 | protected Map> getCacheGrid() { 201 | return cacheToInvocationsMap; 202 | } 203 | 204 | public void setCacheGrid(Map> cacheGrid) { 205 | this.cacheToInvocationsMap = cacheGrid; 206 | } 207 | 208 | protected Set getInvocations() { 209 | return allInvocations; 210 | } 211 | 212 | /** 213 | * Holds the method invocation information to use while refreshing the 214 | * cache. 215 | * 216 | * @author szaidi 217 | * @copyright @2016 Sapient Consulting 218 | * @see CacheSupportImpl.java 219 | * @version 1.0 220 | */ 221 | protected static final class CachedInvocation { 222 | private Object key; 223 | private final Object targetBean; 224 | private final Method targetMethod; 225 | private Object[] arguments; 226 | 227 | protected CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) { 228 | this.key = key; 229 | this.targetBean = targetBean; 230 | this.targetMethod = targetMethod; 231 | if (arguments != null && arguments.length != 0) { 232 | this.arguments = Arrays.copyOf(arguments, arguments.length); 233 | // TODO check if deep cloning is needed and implement 234 | } 235 | } 236 | 237 | /* 238 | * (non-Javadoc) 239 | * 240 | * @see java.lang.Object#equals(java.lang.Object) 241 | */ 242 | @Override 243 | public boolean equals(Object obj) { 244 | if (this == obj) { 245 | return true; 246 | } 247 | if (!(obj instanceof CachedInvocation)) { 248 | return false; 249 | } 250 | final CachedInvocation other = (CachedInvocation) obj; 251 | return key.equals(other.getKey()); 252 | } 253 | 254 | /** 255 | * @return the arguments 256 | */ 257 | private Object[] getArguments() { 258 | return arguments; 259 | } 260 | 261 | /** 262 | * @return the targetBean 263 | */ 264 | private Object getTargetBean() { 265 | return targetBean; 266 | } 267 | 268 | /** 269 | * @return the targetMethod 270 | */ 271 | private Method getTargetMethod() { 272 | return targetMethod; 273 | } 274 | 275 | /* 276 | * (non-Javadoc) 277 | * 278 | * @see java.lang.Object#hashCode() 279 | */ 280 | @Override 281 | public int hashCode() { 282 | return key.hashCode(); 283 | } 284 | 285 | public Object getKey() { 286 | return key; 287 | } 288 | 289 | /* 290 | * (non-Javadoc) 291 | * 292 | * @see java.lang.Object#toString() 293 | */ 294 | @Override 295 | public String toString() { 296 | return "CachedInvocation [Key=" + key + ", targetBean=" + targetBean + ", targetMethod=" + targetMethod 297 | + ", arguments=" + (arguments != null ? arguments.length : "none") + " ]"; 298 | } 299 | 300 | } 301 | 302 | public void setKeyGenerator(KeyGenerator keyGenerator) { 303 | this.keyGenerator = keyGenerator; 304 | } 305 | 306 | /** 307 | * {@inheritDoc} 308 | */ 309 | @Override 310 | public Collection getCacheNames() { 311 | return cacheManager.getCacheNames(); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/CachingAnnotationsAspect.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.lang.reflect.Method; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import org.aspectj.lang.ProceedingJoinPoint; 13 | import org.aspectj.lang.reflect.MethodSignature; 14 | import org.springframework.aop.framework.AopProxyUtils; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.cache.annotation.Cacheable; 17 | import org.springframework.core.BridgeMethodResolver; 18 | import org.springframework.stereotype.Component; 19 | import org.springframework.util.ClassUtils; 20 | 21 | /** 22 | * Aspect to intercept invocation of methods annotated with @Cacheable. 23 | * 24 | * @author Saiyed Zaidi 25 | * @copyright @2016 http://yantrashala.github.io 26 | * @version 1.0 27 | */ 28 | @Component 29 | public class CachingAnnotationsAspect { 30 | 31 | @Autowired 32 | private InvocationRegistry cacheRefreshSupport; 33 | 34 | /** 35 | * Intercepts invocations of methods annotated with @Cacheable and 36 | * invokes cacheRefreshSupport with the execution information. Pointcut 37 | * configured externally using XML config to keep the application flexible. 38 | * 39 | * Configure this aspect to intercept the classes where refreshing caches are needed. 40 | * 41 | * @param joinPoint 42 | * @return 43 | * @throws Throwable 44 | */ 45 | public Object interceptCacheables(ProceedingJoinPoint joinPoint) throws Throwable {// NOSONAR 46 | // No sonar comment is to avoid "throws Throwable" sonar violation 47 | Method annotatedElement = getSpecificmethod(joinPoint); 48 | List annotations = getMethodAnnotations(annotatedElement, Cacheable.class); 49 | Set cacheSet = new HashSet(); 50 | for (Cacheable cacheables : annotations) { 51 | cacheSet.addAll(Arrays.asList(cacheables.value())); 52 | } 53 | cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), annotatedElement, joinPoint.getArgs(), cacheSet); 54 | return joinPoint.proceed(); 55 | } 56 | 57 | /** 58 | * Finds out the most specific method when the execution reference is an 59 | * interface or a method with generic parameters 60 | * 61 | * @param pjp 62 | * @return 63 | */ 64 | private Method getSpecificmethod(ProceedingJoinPoint pjp) { 65 | MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); 66 | Method method = methodSignature.getMethod(); 67 | // The method may be on an interface, but we need attributes from the 68 | // target class. If the target class is null, the method will be 69 | // unchanged. 70 | Class targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget()); 71 | if (targetClass == null && pjp.getTarget() != null) { 72 | targetClass = pjp.getTarget().getClass(); 73 | } 74 | Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); 75 | // If we are dealing with method with generic parameters, find the 76 | // original method. 77 | specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); 78 | return specificMethod; 79 | } 80 | 81 | /** 82 | * Parses all annotations declared on the Method 83 | * 84 | * @param ae 85 | * @param annotationType 86 | * Annotation type to look for 87 | * @return 88 | */ 89 | private static List getMethodAnnotations(AnnotatedElement ae, Class annotationType) { 90 | List anns = new ArrayList(2); 91 | // look for raw annotation 92 | T ann = ae.getAnnotation(annotationType); 93 | if (ann != null) { 94 | anns.add(ann); 95 | } 96 | // look for meta-annotations 97 | for (Annotation metaAnn : ae.getAnnotations()) { 98 | ann = metaAnn.annotationType().getAnnotation(annotationType); 99 | if (ann != null) { 100 | anns.add(ann); 101 | } 102 | } 103 | return (anns.isEmpty() ? null : anns); 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/InvocationRegistry.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.Set; 5 | 6 | /** 7 | * Records invocations of methods with @Cacheable annotations. Uses the 8 | * invocations to refresh the cached values 9 | * 10 | * @author Saiyed Zaidi 11 | * @copyright @2016 http://yantrashala.github.io 12 | * @version 1.0 13 | */ 14 | public interface InvocationRegistry { 15 | 16 | /** 17 | * Records invocations of methods with @Cacheable annotations 18 | * 19 | * @param invokedBean 20 | * @param invokedMethod 21 | * @param invocationArguments 22 | * @param cacheNames 23 | */ 24 | void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set cacheNames); 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/LoggingAspect.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.util.Arrays; 4 | import java.util.Map; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | import org.aspectj.lang.JoinPoint; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.stereotype.Component; 12 | 13 | /** 14 | * A simple logging aspect to log the arguments and return values from the configured methods. 15 | * 16 | * @author Saiyed Zaidi 17 | * @copyright @2016 http://yantrashala.github.io 18 | * @version 1.0 19 | */ 20 | @Component 21 | public class LoggingAspect { 22 | 23 | private static final String METHOD_ARGS_RETURN = "method={} args={} return={}"; 24 | private static final String METHOD_ARGS = "method={} args={}"; 25 | private static final String EXCEPTION_CAUGHT_SOURCE_METHOD_ARGS_EXCEPTION_TRACE = "exception.logged source=[{}] \nmethod=[{}] \nargs=[{}] \nexception=[{}] \ntrace={}"; 26 | private static final int CHAR_LENGTH_120 = 120; 27 | private static final Map, Logger> CLASS_LOGGERS = new ConcurrentHashMap, Logger>(); 28 | private static final Logger ASPECTLOGGER = LoggingAspect.getLogger(LoggingAspect.class); 29 | 30 | /** 31 | * Gets the logger for the targeted class name. 32 | * 33 | * @param className 34 | * Class for which logger is to be created or fetched 35 | * @return 36 | */ 37 | protected static Logger getLogger(final Class className) { 38 | Logger logger = LoggingAspect.CLASS_LOGGERS.get(className); 39 | if (logger == null) { 40 | logger = LoggerFactory.getLogger(className); 41 | LoggingAspect.CLASS_LOGGERS.put(className, logger); 42 | } 43 | return logger; 44 | } 45 | 46 | /** 47 | * Logs the entry to and exit from any method under the configured package. 48 | * 49 | * @param joinPoint 50 | * @return 51 | */ 52 | public Object logAround(final ProceedingJoinPoint joinPoint) throws Throwable { // NOSONAR 53 | // No sonar comment is to skip sonar Throwable violation, which can not 54 | // be avoided 55 | Object ret = joinPoint.proceed(); 56 | final Logger targetLogger = LoggingAspect.getLogger(joinPoint.getSourceLocation().getWithinType()); 57 | if (targetLogger.isDebugEnabled()) { 58 | targetLogger.debug(LoggingAspect.METHOD_ARGS_RETURN, new String[] { joinPoint.getSignature().getName(), 59 | Arrays.toString(joinPoint.getArgs()), ret != null ? ret.toString() : null }); 60 | } 61 | return ret; 62 | 63 | } 64 | 65 | /** 66 | * Logs the entry to any method under configured package. 67 | * 68 | * @param joinPoint 69 | * @return 70 | */ 71 | public void logBefore(final JoinPoint joinPoint) throws Throwable { // NOSONAR 72 | // No sonar comment is to skip sonar violation at this line 73 | final Logger targetLogger = LoggingAspect.getLogger(joinPoint.getSourceLocation().getWithinType()); 74 | if (targetLogger.isDebugEnabled()) { 75 | targetLogger.debug(LoggingAspect.METHOD_ARGS, joinPoint.getSignature().getName(), joinPoint.getArgs()); 76 | } 77 | } 78 | 79 | /** 80 | * Logs all exception thrown under the configured package. 81 | * 82 | * @param joinPoint 83 | * Execution point at which exception arose 84 | * @param throwable 85 | * Exception to log 86 | */ 87 | public void logGeneralExceptions(final JoinPoint joinPoint, final Throwable throwable) { 88 | buildAndLogStackTraceBuffer(joinPoint.getSourceLocation().getWithinType(), joinPoint.getSignature().getName(), 89 | joinPoint.getArgs(), throwable); 90 | } 91 | 92 | /** 93 | * Creates stacktrace from the supplied throwable and logs it along with the 94 | * Class name, method name and arguments 95 | * 96 | * @param withInType 97 | * @param signatureName 98 | * @param args 99 | * @param throwable 100 | */ 101 | protected void buildAndLogStackTraceBuffer(final Class withInType, final String signatureName, 102 | final Object[] args, final Throwable throwable) { 103 | StringBuilder traceBuilder = buildTrace(throwable); 104 | 105 | logStackTrace(withInType, signatureName, args, throwable, traceBuilder); 106 | 107 | if (throwable.getCause() != null) { 108 | buildAndLogStackTraceBuffer(withInType, signatureName, args, throwable.getCause()); 109 | } 110 | } 111 | 112 | /** 113 | * Extracts the stacktrace from the Throwable 114 | * 115 | * @param throwable 116 | * @return 117 | */ 118 | protected StringBuilder buildTrace(Throwable throwable) { 119 | StringBuilder traceBuilder = null; 120 | if (throwable != null) { 121 | final StackTraceElement[] causeTrace = throwable.getStackTrace(); 122 | if (causeTrace != null) { 123 | traceBuilder = new StringBuilder(causeTrace.length * LoggingAspect.CHAR_LENGTH_120); 124 | for (final StackTraceElement element : causeTrace) { 125 | traceBuilder.append(element).append('\n'); 126 | } 127 | } 128 | } 129 | return traceBuilder; 130 | } 131 | 132 | /** 133 | * Logs the class name, method signature name and arguments and the 134 | * exception stacktrace 135 | * 136 | * @param withInType 137 | * @param signatureName 138 | * @param args 139 | * @param throwable 140 | * @param stackTraceBufer 141 | */ 142 | protected void logStackTrace(final Class withInType, final String signatureName, final Object[] args, 143 | final Throwable throwable, StringBuilder stackTraceBufer) { 144 | LoggingAspect.getLogger(withInType).error(LoggingAspect.EXCEPTION_CAUGHT_SOURCE_METHOD_ARGS_EXCEPTION_TRACE, 145 | new String[] { withInType.getName(), signatureName, Arrays.toString(args), throwable.toString(), 146 | stackTraceBufer != null ? stackTraceBufer.toString() : null }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/io/github/yantrashala/springcache/tools/ProfilingAspect.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.util.StopWatch; 10 | 11 | /** 12 | * Aspect to profile all methods and log execution times 13 | * 14 | * @author Saiyed Zaidi 15 | * @copyright @2016 http://yantrashala.github.io 16 | * @version 1.0 17 | */ 18 | @Component 19 | public class ProfilingAspect { 20 | 21 | private static final Logger LOGGER = LoggerFactory.getLogger(ProfilingAspect.class); 22 | 23 | /** 24 | * Applies the advice to a specific package as per the pointcut 25 | * 26 | * @param joinPoint 27 | * @return 28 | * @throws Throwable 29 | */ 30 | public Object profileIntegrations(final ProceedingJoinPoint joinPoint) throws Throwable { // NOSONAR 31 | // No sonar comment is to skip sonar violation at this line 32 | return logExecutionTime(joinPoint); 33 | } 34 | 35 | /** 36 | * Logs execution time of the joinpoint 37 | * 38 | * @param joinPoint 39 | * @return Result of joinpoint execution 40 | * @throws Throwable 41 | * Can not avoid the mandatory throwable thrown by 42 | * joinpoint.proceed() 43 | */ 44 | public Object logExecutionTime(final ProceedingJoinPoint joinPoint) throws Throwable { // NOSONAR 45 | // No sonar comment is to skip sonar violation at this line 46 | StopWatch stopWatch = new StopWatch(); 47 | stopWatch.start(); 48 | Object ret = joinPoint.proceed(); 49 | stopWatch.stop(); 50 | LOGGER.info("class={} method={} args={} execTime={}", 51 | new String[] { joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName(), 52 | (joinPoint.getArgs() == null || joinPoint.getArgs().length == 0) ? "None" 53 | : Arrays.toString(joinPoint.getArgs()), 54 | stopWatch.getTotalTimeMillis() + " millis" }); 55 | return ret; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/github/yantrashala/springcache/tools/TestCacheOperations.java: -------------------------------------------------------------------------------- 1 | package io.github.yantrashala.springcache.tools; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotEquals; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Random; 9 | 10 | import org.aspectj.lang.ProceedingJoinPoint; 11 | import org.aspectj.lang.annotation.Around; 12 | import org.aspectj.lang.annotation.Aspect; 13 | import org.aspectj.lang.annotation.Pointcut; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.cache.Cache; 18 | import org.springframework.cache.CacheManager; 19 | import org.springframework.cache.annotation.Cacheable; 20 | import org.springframework.cache.annotation.EnableCaching; 21 | import org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean; 22 | import org.springframework.cache.interceptor.DefaultKeyGenerator; 23 | import org.springframework.cache.interceptor.KeyGenerator; 24 | import org.springframework.cache.support.SimpleCacheManager; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.context.annotation.ComponentScan; 27 | import org.springframework.context.annotation.Configuration; 28 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 29 | import org.springframework.context.annotation.EnableLoadTimeWeaving; 30 | import org.springframework.stereotype.Component; 31 | import org.springframework.test.context.ContextConfiguration; 32 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 33 | 34 | @RunWith(SpringJUnit4ClassRunner.class) 35 | @ContextConfiguration(classes = { TestCacheOperations.TestConfiguration.class }) 36 | @EnableLoadTimeWeaving 37 | @EnableAspectJAutoProxy 38 | public class TestCacheOperations { 39 | 40 | private static final String CACHE_NAME = "default"; 41 | @Autowired 42 | BusinessService businessService; 43 | 44 | @Autowired 45 | UnstableBusinessService unstableService; 46 | 47 | @Autowired 48 | CacheOperations cacheOperations; 49 | 50 | @Autowired 51 | CacheManager cacheManager; 52 | 53 | /** 54 | * Tests standard Spring cache to validate the setup 55 | */ 56 | @Test 57 | public void testStandardCacheSimple() { 58 | String response1 = businessService.business("param1", "param2"); 59 | String response2 = businessService.business("param1", "param2"); 60 | assertEquals(response2, response1); 61 | } 62 | 63 | /** 64 | * Tests standard Spring cache negative case to validate the setup is good 65 | * for testing reloads 66 | */ 67 | @Test 68 | public void testStandardCacheNegative() { 69 | String response1 = businessService.business("param1", "param2"); 70 | cacheManager.getCache(CACHE_NAME).clear(); 71 | String response2 = businessService.business("param1", "param2"); 72 | assertNotEquals(response2, response1); 73 | } 74 | 75 | /** 76 | * Tests cached value refresh across cache invocations. 77 | */ 78 | @Test 79 | public void testCacheReloadPositive() { 80 | String response1 = businessService.business("param1", "param2"); 81 | cacheOperations.refreshCache(CACHE_NAME); 82 | String response2 = businessService.business("param1", "param2"); 83 | assertNotEquals(response2, response1); 84 | } 85 | 86 | /** 87 | * Tests cached value retention when business service fails during refresh. 88 | */ 89 | @Test 90 | public void testCacheReloadWithError() { 91 | String response1 = unstableService.business("paramx", "paramy"); 92 | unstableService.setDown(true); 93 | cacheOperations.refreshCache(CACHE_NAME); 94 | String response2 = unstableService.business("paramx", "paramy"); 95 | assertEquals(response2, response1); 96 | } 97 | 98 | /** 99 | * Uses simple cache setup and default keygenerator to setup Spring cache 100 | * abstraction. 101 | * 102 | * @author Saiyed Zaidi 103 | * 104 | */ 105 | @Configuration 106 | @EnableCaching 107 | @EnableAspectJAutoProxy 108 | @ComponentScan(basePackages = { "io.github.yantrashala.springcache.tools" }) 109 | public static class TestConfiguration { 110 | 111 | @Bean 112 | public SimpleCacheManager cacheManager() { 113 | SimpleCacheManager cacheManager = new SimpleCacheManager(); 114 | List caches = new ArrayList(); 115 | caches.add(cacheBean().getObject()); 116 | cacheManager.setCaches(caches); 117 | return cacheManager; 118 | } 119 | 120 | @Bean 121 | public ConcurrentMapCacheFactoryBean cacheBean() { 122 | ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean(); 123 | cacheFactoryBean.setName(CACHE_NAME); 124 | return cacheFactoryBean; 125 | } 126 | 127 | /** 128 | * Better to use own Keygenerator instead of default as the default 129 | * ignores the method and class name in the key generation logic. Using 130 | * Default for simplicity. 131 | * 132 | * @return 133 | */ 134 | @Bean 135 | public KeyGenerator getKeyGenerator() { 136 | return new DefaultKeyGenerator(); 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Any Business service that uses @Cacheable 143 | * 144 | * @author Saiyed Zaidi 145 | * 146 | */ 147 | @Component("businessService") 148 | class BusinessServiceImpl implements BusinessService { 149 | 150 | @Cacheable(value = "default") 151 | public String business(String param1, String param2) { 152 | return "output " + new Random().nextInt(); 153 | } 154 | } 155 | 156 | interface BusinessService { 157 | String business(String param1, String param2); 158 | } 159 | 160 | @Component("unstableService") 161 | class UnstableBusinessServiceImpl implements UnstableBusinessService { 162 | 163 | boolean down = false; 164 | 165 | public void setDown(boolean down) { 166 | this.down = down; 167 | } 168 | 169 | @Cacheable(value = "default") 170 | public String business(String param1, String param2) { 171 | if (down) { 172 | throw new RuntimeException("Service down"); 173 | } 174 | return "output " + new Random().nextInt(); 175 | } 176 | } 177 | 178 | /** 179 | * An Unstable service that may go down and start throwing exceptions. 180 | * 181 | * @author szaidi 182 | * 183 | */ 184 | interface UnstableBusinessService { 185 | /** 186 | * Business as usual 187 | * 188 | * @param param1 189 | * @param param2 190 | * @return 191 | */ 192 | String business(String param1, String param2); 193 | 194 | /** 195 | * Set service down status 196 | * 197 | * @param down 198 | */ 199 | void setDown(boolean down); 200 | } 201 | 202 | /** 203 | * Advice to allow watching the Business service invocations. Passes the 204 | * invocations to CachingAnnotationsAspect 205 | * 206 | * @author Saiyed Zaidi 207 | * 208 | */ 209 | @Aspect 210 | @Component 211 | class TestAdvice { 212 | 213 | @Pointcut("execution(public * io.github.yantrashala.springcache.tools.BusinessService.*(..))") 214 | public void methodsToBeInspected() { 215 | } 216 | 217 | @Autowired 218 | CachingAnnotationsAspect cachingAnnotationsAspect; 219 | 220 | @Around("methodsToBeInspected()") 221 | public Object interceptCaches(ProceedingJoinPoint joinPoint) throws Throwable { 222 | return cachingAnnotationsAspect.interceptCacheables(joinPoint); 223 | } 224 | } --------------------------------------------------------------------------------