001: /*
002: * Copyright 2002-2004 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: *
016: * Code obtained from http://opensource.atlassian.com/confluence/spring/display/DISC/Caching+the+result+of+methods+using+Spring+and+EHCache
017: * and modified for use within Kuali
018: */
019:
020: // begin Kuali Foundation modification
021: package org.kuali.core.util.cache;
022:
023: // Kuali Foundation modification: changed some imports
024: import java.io.Serializable;
025:
026: import org.aopalliance.intercept.MethodInterceptor;
027: import org.aopalliance.intercept.MethodInvocation;
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030: import org.kuali.core.util.CopiedObject;
031: import org.kuali.core.util.ObjectUtils;
032: import org.springframework.beans.factory.InitializingBean;
033: import org.springframework.util.Assert;
034:
035: import com.opensymphony.oscache.base.Cache;
036: import com.opensymphony.oscache.base.NeedsRefreshException;
037:
038: /**
039: * begin Kuali Foundation modification
040: * This class implements org.aopalliance.intercept.MethodInterceptor. This interceptor builds the cache key for the method and
041: * checks if an earlier result was cached with that key. If so, the cached result is returned; otherwise, the intercepted method is
042: * called and the result cached for future use.
043: * end Kuali Foundation modification
044: *
045: * @author Kuali Rice Team (kuali-rice@googlegroups.com)
046: * @since 2004.10.07
047: */
048: public class MethodCacheInterceptor implements MethodInterceptor,
049: InitializingBean {
050: private static final Log LOG = LogFactory
051: .getLog(MethodCacheInterceptor.class);
052:
053: private Cache cache;
054: // begin Kuali Foundation modification
055: private int expirationTimeInSeconds = 1000;
056: private long maxEntrySizeInBytes = 0;
057:
058: // end Kuali Foundation modification
059:
060: /**
061: * begin Kuali Foundation modification
062: * @param cache name of cache to be used
063: * end Kuali Foundation modification
064: */
065: public void setCache(Cache cache) {
066: this .cache = cache;
067: }
068:
069: // begin Kuali Foundation modification
070: /**
071: * Entries older than this will have their contents replaced by the return value from a call to the appropriate method
072: *
073: * @param expirationTimeInSeconds
074: */
075: public void setExpirationTimeInSeconds(int expirationTimeInSeconds) {
076: this .expirationTimeInSeconds = expirationTimeInSeconds;
077: }
078:
079: /**
080: * Entries whose size is larger than the current value will not be cached. If the maxEntrySizeInBytes <= 0, no size limit will
081: * be applied.
082: *
083: * @param maxEntrySizeInBytes
084: */
085: public void setMaxEntrySizeInBytes(long maxEntrySizeInBytes) {
086: this .maxEntrySizeInBytes = maxEntrySizeInBytes;
087: }
088:
089: // end Kuali Foundation modification
090:
091: /**
092: * Checks if required attributes are provided.
093: *
094: * begin Kuali Foundation modification
095: * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
096: * end Kuali Foundation modification
097: */
098: public void afterPropertiesSet() throws Exception {
099: Assert
100: .notNull(cache,
101: "A cache is required. Use setCache(Cache) to provide one.");
102: }
103:
104: /**
105: * begin Kuali Foundation modification
106: * Caches method results, if possible.
107: * <p>
108: * Results must be Serializable to be cached. Method with unSerializable results will never have their results cached, and will
109: * log error messages complaining about that fact.
110: *
111: * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
112: * end Kuali Foundation modification
113: */
114: public Object invoke(MethodInvocation invocation) throws Throwable {
115: // begin Kuali Foundation modification
116: boolean cancelUpdate = true;
117:
118: Object methodResult = null;
119: String cacheKey = buildCacheKey(invocation);
120:
121: // lookup result in cache
122: if (LOG.isTraceEnabled()) {
123: LOG.trace("looking for method result for invocation '"
124: + cacheKey + "'");
125: }
126: try {
127: CopiedObject cachedEntry = (CopiedObject) cache
128: .getFromCache(cacheKey, expirationTimeInSeconds);
129: if (LOG.isDebugEnabled()) {
130: LOG
131: .debug("using cached result deepCopy for invocation '"
132: + cacheKey + "'");
133: }
134:
135: // really expensive hack to try to keep from returning direct references to modifiable cached values
136: // because if you return a direct reference to a cached value to someone, and it is of a mutable type, any changes they
137: // make to what seems like "their copy" will also be reflected in the cached value, which is a Really Bad Thing
138: methodResult = ObjectUtils.deepCopy(cachedEntry
139: .getContent());
140: cancelUpdate = false;
141: } catch (NeedsRefreshException e) {
142: // call intercepted method
143: try {
144: if (LOG.isTraceEnabled()) {
145: LOG
146: .trace("calling intercepted method for invocation '"
147: + cacheKey + "'");
148: }
149: methodResult = invocation.proceed();
150: } catch (Exception invocationException) {
151: LOG
152: .warn("unable to cache methodResult: caught exception invoking intercepted method: '"
153: + invocationException);
154: throw invocationException;
155: }
156:
157: // cache method result, if possible
158: // there's no way to tell whether a result is cacheable until after the method gets called,
159: // since methods may hand back a Serializable object even if the method is declared to return
160: // a more general nonSerializable type (e.g. List is not Serializable, but if the method
161: // actually returns ArrayList instances, they are)
162: //
163: // nulls are a special case, since isAssignableFrom will never return true, yet they are
164: // Serializable (at least for this kind of use)
165: //
166: // caching a deepCopy of the methodResult to prevent someone changing the cached value directly,
167: // through a shared reference
168: if ((methodResult == null)
169: || (Serializable.class
170: .isAssignableFrom(methodResult.getClass()))) {
171: try {
172: CopiedObject cacheCopy = ObjectUtils
173: .deepCopyForCaching((Serializable) methodResult);
174:
175: CopiedObject oldContent = (CopiedObject) e
176: .getCacheContent();
177: if (oldContent != null) {
178: cacheCopy.setOldSize(oldContent.getSize());
179: }
180:
181: // if no size limit, or under size limit, add to cache
182: if ((maxEntrySizeInBytes <= 0)
183: || (cacheCopy.getSize() <= maxEntrySizeInBytes)) {
184: if (LOG.isTraceEnabled()) {
185: LOG
186: .trace("caching results for invocation '"
187: + cacheKey + "'");
188: }
189: cache.putInCache(cacheKey, cacheCopy);
190:
191: // adding, not updating
192: cancelUpdate = false;
193: } else {
194: if (LOG.isTraceEnabled()) {
195: LOG
196: .trace("rejecting oversized methodResult ("
197: + cacheCopy.getSize()
198: + " bytes) for invocation '"
199: + cacheKey + "'");
200: }
201:
202: // size limit exceeded: remove existing (expired) cache entry, if any
203: if (oldContent != null) {
204: if (LOG.isTraceEnabled()) {
205: LOG
206: .trace("flushing previous value for oversized invocation '"
207: + cacheKey + "'");
208: }
209:
210: cache.cancelUpdate(cacheKey);
211: cache.flushEntry(cacheKey);
212:
213: // already canceled the update, don't need to cancel the update again
214: cancelUpdate = false;
215: }
216: }
217: } catch (Exception cacheException) {
218: LOG
219: .error("unable to cache methodResult: caught exception invoking putInCache: '"
220: + cacheException);
221: throw cacheException;
222: }
223: } else {
224: LOG
225: .error("unable to cache nonSerializable result type for invocation '"
226: + cacheKey + "'");
227: }
228: } finally {
229: // it is imperative that you call cancelUpdate if you aren't going to update the cache entry
230: if (cancelUpdate) {
231: cache.cancelUpdate(cacheKey);
232: }
233: }
234:
235: return methodResult;
236: // end Kuali Foundation modification
237: }
238:
239: // begin Kuali Foundation modification
240: /**
241: * @param invocation MethodInvocation being handled
242: * @return cache key: className.methodName(paramClass=argValue[,paramClass=argValue...])
243: */
244: private String buildCacheKey(MethodInvocation invocation) {
245: return buildCacheKey(invocation.getStaticPart().toString(),
246: invocation.getArguments());
247: }
248:
249: /**
250: * @param className
251: * @param methodName
252: * @param paramTypes
253: * @param argValues
254: * @return cache key: className.methodName(paramClass=argValue[,paramClass=argValue...])
255: */
256: public String buildCacheKey(String methodSignature,
257: Object[] argValues) {
258: StringBuffer cacheKey = new StringBuffer(methodSignature);
259: cacheKey.append(": ");
260: if (argValues != null) {
261: for (int i = 0; i < argValues.length; i++) {
262: if (i > 0) {
263: cacheKey.append(",");
264: }
265: // handle weird cache bug:
266: // if you call a one-arg method foo with a null arg i.e. foo(null),
267: // and then call it with an argument whose toString evaluates to "null",
268: // OSCache gets stuck in an infinite wait() call because it somehow thinks
269: // another thread is already updating this cache entry
270: //
271: // workaround: change so that args which are actually null literal have
272: // some weird, unlikely-to-be-encountered String representation
273: if (argValues[i] == null) {
274: cacheKey.append("<literal null>");
275: } else {
276: cacheKey.append(argValues[i]);
277: }
278: }
279: }
280: return cacheKey.toString();
281: }
282:
283: /**
284: * @param key
285: * @return true if the cache contains an entry with the given key
286: */
287: public boolean containsCacheKey(String key) {
288: boolean contains = false;
289:
290: try {
291: cache.getFromCache(key);
292: contains = true;
293: } catch (NeedsRefreshException e) {
294: // it is imperative that you call cancelUpdate if you aren't going to update the cache entry that caused the
295: // NeedsRefreshException above
296: cache.cancelUpdate(key);
297: contains = false;
298: }
299:
300: return contains;
301: }
302:
303: /**
304: * Removes a method cache if one exists for the given key.
305: * @param cacheKey - key for method signature and parameters - see buildCacheKey
306: */
307: public void removeCacheKey(String cacheKey) {
308: if (!containsCacheKey(cacheKey)) {
309: return;
310: }
311:
312: LOG.debug("removing method cache for key: " + cacheKey);
313: cache.cancelUpdate(cacheKey);
314: cache.flushEntry(cacheKey);
315: }
316:
317: // Kuali Foundation modification: removed getCacheKey(String, String, Object[])
318: // end Kuali Foundation modification
319: }
|