All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jcabi.aspects.aj.MethodCacher Maven / Gradle / Ivy

There is a newer version: 0.26.0
Show newest version
/**
 * Copyright (c) 2012-2014, jcabi.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the jcabi.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jcabi.aspects.aj;

import com.jcabi.aspects.Cacheable;
import com.jcabi.aspects.Loggable;
import com.jcabi.log.Logger;
import com.jcabi.log.VerboseRunnable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;

/**
 * Cache method results.
 *
 * 

It is an AspectJ aspect and you are not supposed to use it directly. It * is instantiated by AspectJ runtime framework when your code is annotated * with {@link Cacheable} annotation. * *

The class is thread-safe. * * @author Yegor Bugayenko ([email protected]) * @version $Id$ * @since 0.8 * @todo #14 Split this class into smaller ones so it will have less * responsibility and remove PMD.GodClass suppressed warning. */ @Aspect @SuppressWarnings({ "PMD.DoNotUseThreads", "PMD.TooManyMethods", "PMD.GodClass" }) public final class MethodCacher { /** * Calling tunnels. * @checkstyle LineLength (2 lines) */ private final transient ConcurrentMap tunnels = new ConcurrentHashMap(0); /** * Service that cleans cache. */ private final transient ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor( new NamedThreads( "cacheable", "automated cleaning of expired @Cacheable values" ) ); /** * Public ctor. */ public MethodCacher() { this.cleaner.scheduleWithFixedDelay( new VerboseRunnable( new Runnable() { @Override public void run() { MethodCacher.this.clean(); } } ), 1, 1, TimeUnit.SECONDS ); } /** * Call the method or fetch from cache. * *

Try NOT to change the signature of this method, in order to keep * it backward compatible. * * @param point Joint point * @return The result of call * @throws Throwable If something goes wrong inside * @checkstyle IllegalThrows (4 lines) */ @Around("execution(* *(..)) && @annotation(com.jcabi.aspects.Cacheable)") public Object cache(final ProceedingJoinPoint point) throws Throwable { final Key key = new MethodCacher.Key(point); Tunnel tunnel; synchronized (this.tunnels) { tunnel = this.tunnels.get(key); if (tunnel == null || tunnel.expired()) { tunnel = new MethodCacher.Tunnel(point, key); this.tunnels.put(key, tunnel); } } return tunnel.through(); } /** * Flush cache. * @param point Join point * @return Value of the method * @since 0.7.14 * @deprecated Since 0.7.17, and preflush() should be used * @throws Throwable If something goes wrong inside * @checkstyle IllegalThrows (3 lines) */ @Deprecated public Object flush(final ProceedingJoinPoint point) throws Throwable { this.preflush(point); return point.proceed(); } /** * Flush cache. * *

Try NOT to change the signature of this method, in order to keep * it backward compatible. * * @param point Joint point * @since 0.7.14 */ @Before( // @checkstyle StringLiteralsConcatenation (3 lines) "execution(* *(..))" + " && (@annotation(com.jcabi.aspects.Cacheable.Flush)" + " || @annotation(com.jcabi.aspects.Cacheable.FlushBefore))" ) public void preflush(final JoinPoint point) { this.flush(point, "before the call"); } /** * Flush cache after method execution. * *

Try NOT to change the signature of this method, in order to keep * it backward compatible. * * @param point Joint point * @since 0.7.18 */ @After( // @checkstyle StringLiteralsConcatenation (2 lines) "execution(* *(..))" + " && @annotation(com.jcabi.aspects.Cacheable.FlushAfter)" ) public void postflush(final JoinPoint point) { this.flush(point, "after the call"); } /** * Flush cache. * @param point Joint point * @param when When it happens * @since 0.7.18 */ private void flush(final JoinPoint point, final String when) { synchronized (this.tunnels) { for (final MethodCacher.Key key : this.tunnels.keySet()) { if (!key.sameTarget(point)) { continue; } final Tunnel removed = this.tunnels.remove(key); final Method method = MethodSignature.class .cast(point.getSignature()) .getMethod(); if (MethodCacher.enabled( key.getLevel(), method.getDeclaringClass() )) { MethodCacher.log( key.getLevel(), method.getDeclaringClass(), "%s: %s:%s removed from cache %s", Mnemos.toText(method, point.getArgs(), true, false), key, removed, when ); } } } } /** * Clean cache. */ private void clean() { synchronized (this.tunnels) { for (final Key key : this.tunnels.keySet()) { if (this.tunnels.get(key).expired()) { final Tunnel tunnel = this.tunnels.remove(key); MethodCacher.log( key.getLevel(), this, "%s:%s expired in cache", key, tunnel ); } } } } /** * Log one line. * @param level Level of logging * @param log Destination log * @param message Message to log * @param params Message parameters * @checkstyle ParameterNumberCheck (3 lines) */ private static void log(final int level, final Object log, final String message, final Object... params) { if (level == Loggable.TRACE) { Logger.trace(log, message, params); } else if (level == Loggable.DEBUG) { Logger.debug(log, message, params); } else if (level == Loggable.INFO) { Logger.info(log, message, params); } else if (level == Loggable.WARN) { Logger.warn(log, message, params); } else if (level == Loggable.ERROR) { Logger.error(log, message, params); } } /** * Log level is enabled? * @param level Level of logging * @param log Destination log * @return TRUE if enabled */ private static boolean enabled(final int level, final Object log) { boolean enabled; if (level == Loggable.TRACE) { enabled = Logger.isTraceEnabled(log); } else if (level == Loggable.DEBUG) { enabled = Logger.isDebugEnabled(log); } else if (level == Loggable.INFO) { enabled = Logger.isInfoEnabled(log); } else if (level == Loggable.WARN) { enabled = Logger.isWarnEnabled(log); } else { enabled = true; } return enabled; } /** * Mutable caching/calling tunnel, it is thread-safe. */ private static final class Tunnel { /** * Proceeding join point. */ private final transient ProceedingJoinPoint point; /** * Key related to this tunnel. */ private final transient Key key; /** * Was it already executed? */ private transient boolean executed; /** * When will it expire (moment in time). */ private transient long lifetime; /** * Cached value. */ private transient Object cached; /** * Public ctor. * @param pnt Joint point * @param akey The key related to it */ protected Tunnel(final ProceedingJoinPoint pnt, final Key akey) { this.point = pnt; this.key = akey; } @Override public String toString() { return Mnemos.toText(this.cached, true, false); } /** * Get a result through the tunnel. * @return The result * @throws Throwable If something goes wrong inside * @checkstyle IllegalThrows (5 lines) */ @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") public synchronized Object through() throws Throwable { if (!this.executed) { final long start = System.currentTimeMillis(); this.cached = this.point.proceed(); final Method method = MethodSignature.class .cast(this.point.getSignature()) .getMethod(); final Cacheable annot = method.getAnnotation(Cacheable.class); String suffix; if (annot.forever()) { this.lifetime = Long.MAX_VALUE; suffix = "valid forever"; } else if (annot.lifetime() == 0) { this.lifetime = 0; suffix = "invalid immediately"; } else { final long msec = annot.unit().toMillis(annot.lifetime()); this.lifetime = start + msec; suffix = Logger.format("valid for %[ms]s", msec); } final Class type = method.getDeclaringClass(); if (MethodCacher.enabled(this.key.getLevel(), type)) { MethodCacher.log( this.key.getLevel(), type, "%s: %s cached in %[ms]s, %s", Mnemos.toText( method, this.point.getArgs(), true, false ), Mnemos.toText(this.cached, true, false), System.currentTimeMillis() - start, suffix ); } this.executed = true; } return this.key.through(this.cached); } /** * Is it expired already? * @return TRUE if expired */ public boolean expired() { return this.executed && this.lifetime < System.currentTimeMillis(); } } /** * Key of a callable target. */ private static final class Key { /** * When instantiated. */ private final transient long start = System.currentTimeMillis(); /** * How many times the key was already accessed. */ private final transient AtomicInteger accessed = new AtomicInteger(); /** * Method. */ private final transient Method method; /** * Object callable (or class, if static method). */ private final transient Object target; /** * Arguments. */ private final transient Object[] arguments; /** * Log level. */ private final int level; /** * Public ctor. * @param point Joint point */ protected Key(final ProceedingJoinPoint point) { this.method = MethodSignature.class .cast(point.getSignature()).getMethod(); this.target = MethodCacher.Key.targetize(point); this.arguments = point.getArgs(); if (this.method.isAnnotationPresent(Loggable.class)) { this.level = this.method.getAnnotation(Loggable.class).value(); } else { this.level = Loggable.DEBUG; } } @Override public String toString() { return Mnemos.toText(this.method, this.arguments, true, false); } @Override public int hashCode() { return this.method.hashCode(); } @Override public boolean equals(final Object obj) { boolean equals; if (this == obj) { equals = true; } else if (obj instanceof MethodCacher.Key) { final MethodCacher.Key key = MethodCacher.Key.class.cast(obj); equals = key.method.equals(this.method) && this.target.equals(key.target) && Arrays.deepEquals(key.arguments, this.arguments); } else { equals = false; } return equals; } /** * Send a result through, with necessary logging. * @param result The result to send through * @return The same result/object */ public Object through(final Object result) { final int hit = this.accessed.getAndIncrement(); final Class type = this.method.getDeclaringClass(); if (hit > 0 && MethodCacher.enabled(this.level, type)) { MethodCacher.log( this.level, type, "%s: %s from cache (hit #%d, %[ms]s old)", this, Mnemos.toText(result, true, false), hit, System.currentTimeMillis() - this.start ); } return result; } /** * Is it related to the same target? * @param point Proceeding point * @return True if the target is the same */ public boolean sameTarget(final JoinPoint point) { return MethodCacher.Key.targetize(point).equals(this.target); } /** * Calculate its target. * @param point Proceeding point * @return The target */ private static Object targetize(final JoinPoint point) { Object tgt; final Method method = MethodSignature.class .cast(point.getSignature()).getMethod(); if (Modifier.isStatic(method.getModifiers())) { tgt = method.getDeclaringClass(); } else { tgt = point.getTarget(); } return tgt; } /** * Get log level. * @return Log level of current method. */ private int getLevel() { return this.level; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy