/** * Copyright (c) 2012-2017, 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 java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; 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. * * <p>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. * * <p>The class is thread-safe. * * @author Yegor Bugayenko (yegor@tpc2.com) * @version $Id: 81af9dfb282bbcda540b9148c81ef09cf1cd0199 $ * @since 0.8 */ @Aspect @SuppressWarnings("PMD.TooManyMethods") public final class MethodCacher { /** * Calling tunnels. */ private final transient ConcurrentMap<MethodCacher.Key, MethodCacher.Tunnel> tunnels; /** * Save the keys of caches which need update. */ private final transient BlockingQueue<Key> updatekeys; /** * Public ctor. */ public MethodCacher() { this.tunnels = new ConcurrentHashMap<MethodCacher.Key, MethodCacher.Tunnel>(0); this.updatekeys = new LinkedBlockingQueue<MethodCacher.Key>(); new UpdateMethodCacher(this.tunnels, this.updatekeys).start(); } /** * Call the method or fetch from cache. * * <p>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 MethodCacher.Key key = new MethodCacher.Key(point); MethodCacher.Tunnel tunnel; final Method method = MethodSignature.class .cast(point.getSignature()) .getMethod(); final Cacheable annot = method.getAnnotation(Cacheable.class); synchronized (this.tunnels) { for (final Class<?> before : annot.before()) { final boolean flag = Boolean.class.cast( before.getMethod("flushBefore").invoke(method.getClass()) ); if (flag) { this.preflush(point); } } tunnel = this.tunnels.get(key); if (this.isCreateTunnel(tunnel)) { tunnel = new MethodCacher.Tunnel( point, key, annot.asyncUpdate() ); this.tunnels.put(key, tunnel); } if (tunnel.expired() && tunnel.asyncUpdate()) { this.updatekeys.offer(key); } for (final Class<?> after : annot.after()) { final boolean flag = Boolean.class.cast( after.getMethod("flushAfter").invoke(method.getClass()) ); if (flag) { this.postflush(point); } } } 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. * * <p>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. * * <p>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 MethodCacher.Tunnel removed = this.tunnels.remove(key); final Method method = MethodSignature.class .cast(point.getSignature()) .getMethod(); if (LogHelper.enabled( key.getLevel(), method.getDeclaringClass() )) { LogHelper.log( key.getLevel(), method.getDeclaringClass(), "%s: %s:%s removed from cache %s", Mnemos.toText(method, point.getArgs(), true, false), key, removed, when ); } } } } /** * Whether create a new Tunnel. * @param tunnel MethodCacher.Tunnel * @return Boolean */ private boolean isCreateTunnel(final MethodCacher.Tunnel tunnel) { return tunnel == null || (tunnel.expired() && !tunnel.asyncUpdate()); } /** * Mutable caching/calling tunnel, it is thread-safe. */ protected static final class Tunnel { /** * Proceeding join point. */ private final transient ProceedingJoinPoint point; /** * Key related to this tunnel. */ private final transient MethodCacher.Key key; /** * Whether asynchronous update. */ private final transient boolean async; /** * Was it already executed? */ private transient boolean executed; /** * When will it expire (moment in time). */ private transient long lifetime; /** * Has non-null result? */ private transient boolean hasresult; /** * Cached value. */ @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName") private transient SoftReference<Object> cached; /** * Public ctor. * @param pnt ProceedingJoinPoint * @param akey MethodCacher.Key * @param asy Boolean */ Tunnel(final ProceedingJoinPoint pnt, final MethodCacher.Key akey, final boolean asy) { this.point = pnt; this.key = akey; this.async = asy; } @Override public String toString() { return Mnemos.toText(this.cached.get(), true, false); } /** * Get a new instance. * @return MethodCacher.Tunnel */ public Tunnel copy() { return new Tunnel( this.point, this.key, this.async ); } /** * 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(); final Object result = this.point.proceed(); this.hasresult = result != null; this.cached = new SoftReference<Object>(result); final Method method = MethodSignature.class .cast(this.point.getSignature()) .getMethod(); final Cacheable annot = method.getAnnotation(Cacheable.class); final String suffix; if (annot.forever()) { this.lifetime = Long.MAX_VALUE; suffix = "valid forever"; } else if (annot.lifetime() == 0) { this.lifetime = 0L; suffix = "invalid immediately"; } else { final long msec = annot.unit().toMillis( (long) annot.lifetime() ); this.lifetime = start + msec; suffix = Logger.format("valid for %[ms]s", msec); } final Class<?> type = method.getDeclaringClass(); if (LogHelper.enabled(this.key.getLevel(), type)) { LogHelper.log( this.key.getLevel(), type, "%s: %s cached in %[ms]s, %s", Mnemos.toText( method, this.point.getArgs(), true, false ), Mnemos.toText(this.cached.get(), true, false), System.currentTimeMillis() - start, suffix ); } this.executed = true; } return this.key.through(this.cached.get()); } /** * Is it expired already? * @return TRUE if expired */ public boolean expired() { final boolean expired = this.lifetime < System.currentTimeMillis(); final boolean collected = this.executed && this.hasresult && this.cached.get() == null; return this.executed && (expired || collected); } /** * Whether asynchronous update. * @return TRUE if asynchronous update */ public boolean asyncUpdate() { return this.async; } /** * Soft reference to cached object. * Visible only for testing. Do not use directly. * @return Soft reference to cached object. */ @SuppressWarnings("PMD.DefaultPackage") SoftReference<Object> cached() { return this.cached; } } /** * Key of a callable target. * @checkstyle DesignForExtensionCheck (2 lines) */ protected static class Key { /** * When instantiated. */ private final transient long start; /** * How many times the key was already accessed. */ private final transient AtomicInteger accessed; /** * 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 */ Key(final JoinPoint point) { this.start = System.currentTimeMillis(); this.accessed = new AtomicInteger(); 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; } } /** * Get log level. * @return Log level of current method. */ public final int getLevel() { return this.level; } @Override public final String toString() { return Mnemos.toText(this.method, this.arguments, true, false); } @Override public final int hashCode() { return this.method.hashCode(); } @Override public final boolean equals(final Object obj) { final 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 * @checkstyle DesignForExtensionCheck (2 lines) */ public Object through(final Object result) { final int hit = this.accessed.getAndIncrement(); final Class<?> type = this.method.getDeclaringClass(); if (hit > 0 && LogHelper.enabled(this.level, type)) { LogHelper.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 final 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) { final 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; } } }