├── .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 | }
--------------------------------------------------------------------------------