/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.concurrency.caching; import java.io.Serializable; import java.lang.annotation.AnnotationFormatError; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.management.InstanceAlreadyExistsException; import javax.management.MBeanRegistrationException; import javax.management.MalformedObjectNameException; import javax.management.NotCompliantMBeanException; import javax.management.ObjectName; import javax.servlet.http.HttpServletRequest; import net.sf.ehcache.hibernate.management.impl.EhcacheHibernateMbeanNames; import org.apereo.portal.url.IPortalRequestUtils; import org.apereo.portal.utils.ConcurrentMapUtils; import org.apereo.portal.utils.cache.CacheKey; import org.apereo.portal.utils.web.PortalWebUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jmx.export.MBeanExportOperations; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; /** * Aspect that caches the results of a method invocation in the current {@link RequestAttributes} * */ @Aspect @Component("requestCacheAspect") public class RequestCacheAspect implements InitializingBean { private static final String CACHE_MAP = RequestCacheAspect.class.getName() + ".CACHE_MAP"; private static final Object NULL_PLACEHOLDER = new Object(); protected final Logger logger = LoggerFactory.getLogger(getClass()); private final ConcurrentMap<String, CacheStatistics> methodStats = new ConcurrentHashMap<String, CacheStatistics>(); private final CacheStatistics overallStats = new CacheStatistics(); private IPortalRequestUtils portalRequestUtils; private MBeanExportOperations mBeanExportOperations; @Autowired public void setPortalRequestUtils(IPortalRequestUtils portalRequestUtils) { this.portalRequestUtils = portalRequestUtils; } @Autowired(required = false) public void setmBeanExportOperations(MBeanExportOperations mBeanExportOperations) { this.mBeanExportOperations = mBeanExportOperations; } @Override public void afterPropertiesSet() throws Exception { if (this.mBeanExportOperations != null) { final ObjectName name = new ObjectName( "uPortal:section=Cache,RequestCache=RequestCache,name=OverallStatistics"); registerMbean(this.overallStats, name); } } @Pointcut(value = "execution(public * *(..))") public void anyPublicMethod() {} @Around("anyPublicMethod() && @annotation(requestCache)") public Object cacheRequest(ProceedingJoinPoint pjp, RequestCache requestCache) throws Throwable { final long start = System.nanoTime(); final CacheKey cacheKey = createCacheKey(pjp, requestCache); final HttpServletRequest currentPortalRequest; try { currentPortalRequest = this.portalRequestUtils.getCurrentPortalRequest(); } catch (IllegalStateException e) { logger.trace("No current portal request, will not cache result of: {}", cacheKey); //No current request, simply proceed return pjp.proceed(); } final CacheStatistics cacheStatistics = this.getCacheStatistics(pjp, requestCache); //Check in the cache for a result final ConcurrentMap<CacheKey, Object> cache = PortalWebUtils.getMapRequestAttribute(currentPortalRequest, CACHE_MAP); Object result = cache.get(cacheKey); //Return null if placeholder was cached if (requestCache.cacheNull() && result == NULL_PLACEHOLDER) { final long time = System.nanoTime() - start; cacheStatistics.recordHit(time); overallStats.recordHit(time); logger.debug("Found cached null for invocation of: {}", cacheKey); return null; } //Rethrow if exception was cached if (requestCache.cacheException() && result instanceof ExceptionHolder) { final long time = System.nanoTime() - start; cacheStatistics.recordHit(time); overallStats.recordHit(time); logger.debug("Found cached exception for invocation of: {}", cacheKey); throw ((ExceptionHolder) result).getThrowable(); } //Return cached result if (result != null) { final long time = System.nanoTime() - start; cacheStatistics.recordHit(time); overallStats.recordHit(time); logger.debug("Found cached result for invocation of: {}", cacheKey); return result; } try { //Execute the annotated method result = pjp.proceed(); final long time = System.nanoTime() - start; cacheStatistics.recordMissAndLoad(time); overallStats.recordMissAndLoad(time); if (result != null) { //Cache the not-null result cache.put(cacheKey, result); logger.debug("Cached result for invocation of: {}", cacheKey); } else if (requestCache.cacheNull()) { //If caching nulls cache the placeholder cache.put(cacheKey, NULL_PLACEHOLDER); logger.debug("Cached null for invocation of: {}", cacheKey); } return result; } catch (Throwable t) { final long time = System.nanoTime() - start; cacheStatistics.recordMissAndException(time); overallStats.recordMissAndException(time); if (requestCache.cacheException()) { //If caching exceptions wrapp the exception and cache it cache.put(cacheKey, new ExceptionHolder(t)); logger.debug("Cached exception for invocation of: {}", cacheKey); } throw t; } } protected void registerMbean(Object object, ObjectName name) throws InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException { this.mBeanExportOperations.registerManagedResource(object, name); } protected final CacheStatistics getCacheStatistics( ProceedingJoinPoint pjp, RequestCache requestCache) { final Signature signature = pjp.getSignature(); final String signatureString = signature.toString(); CacheStatistics cacheStatistics = this.methodStats.get(signatureString); if (cacheStatistics == null) { final CacheStatistics newStats = new CacheStatistics(); cacheStatistics = ConcurrentMapUtils.putIfAbsent(this.methodStats, signatureString, newStats); if (this.mBeanExportOperations != null && cacheStatistics == newStats) { final String nameString = "uPortal:section=Cache,RequestCache=RequestCache,name=" + EhcacheHibernateMbeanNames.mbeanSafe(signatureString); try { final ObjectName name = new ObjectName(nameString); registerMbean(cacheStatistics, name); } catch (MalformedObjectNameException e) { logger.warn( "Failed to create ObjectName {} the corresponding CacheStatistics will not be registered with JMX", nameString, e); } catch (NullPointerException e) { logger.warn( "Failed to create ObjectName {} the corresponding CacheStatistics will not be registered with JMX", nameString, e); } catch (InstanceAlreadyExistsException e) { logger.warn( "ObjectName {} is already registered, the corresponding CacheStatistics will not be registered with JMX", nameString, e); } catch (MBeanRegistrationException e) { logger.warn( "Failed to register ObjectName {} the corresponding CacheStatistics will not be registered with JMX", nameString, e); } catch (NotCompliantMBeanException e) { logger.warn( "Failed to register ObjectName {} the corresponding CacheStatistics will not be registered with JMX", nameString, e); } } } return cacheStatistics; } protected CacheKey createCacheKey(ProceedingJoinPoint pjp, RequestCache requestCache) { final Signature signature = pjp.getSignature(); final Class<?> declaringType = signature.getDeclaringType(); final String signatureLongString = signature.toLongString(); final boolean[] keyMask = requestCache.keyMask(); final Object[] args = pjp.getArgs(); final Object[] keyArgs; if (keyMask.length == 0) { keyArgs = args; } else if (keyMask.length != args.length) { throw new AnnotationFormatError( "RequestCache.keyMask has an invalid length on: " + signature.toLongString()); } else { keyArgs = new Object[args.length]; for (int i = 0; i < args.length; i++) { if (keyMask[i]) { keyArgs[i] = args[i]; } } } return CacheKey.build(signatureLongString, declaringType, keyArgs); } private static class ExceptionHolder implements Serializable { private static final long serialVersionUID = 1L; private final Throwable t; public ExceptionHolder(Throwable t) { this.t = t; } public Throwable getThrowable() { return this.t; } } }