package org.perf4j.aop;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.context.HashMapContext;
import org.perf4j.LoggingStopWatch;
import org.perf4j.helpers.Perf4jProperties;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* This AgnosticTimingAspect class contains all the logic for executing a profiled method with appropriate timing calls,
* but in an AOP-framework-agnostic way. You may choose to either extend or wrap this class to create an aspect in
* your desired framework. For example, if you look at the {@link org.perf4j.aop.AbstractTimingAspect}, you can see
* that it delegates all functionality to this class - it just includes the necessary AspectJ annotations and wraps
* the AspectJ-specific ProceedingJoinPoint as an {@link org.perf4j.aop.AbstractJoinPoint}.
*
* @author Marcin ZajÄ…czkowski, Alex Devine
*/
public class AgnosticTimingAspect {
/**
* This Map is used to cache compiled JEXL expressions. While theoretically unbounded, in reality the number of
* possible keys is equivalent to the number of unique JEXL expressions created in @Profiled annotations, which
* will have to be loaded in memory anyway when the class is loaded.
*/
private Map<String, Expression> jexlExpressionCache = new ConcurrentHashMap<String, Expression>(64, .75F, 16);
/**
* This method actually executes the profiled method. Your AOP-framework-specific class should delegate to this
* method to proceed with execution.
*
* @param joinPoint The AOP join point - usually this will just be a simple wrapper around the
* AOP-framework-specific join point.
* @param profiled The Profiled annotation that was set on the method being profiled.
* @param stopWatch This LogginStopWatch should be started JUST before this method is called.
* @return The return value from the profiled method.
* @throws Throwable Exception thrown by the profiled method will bubble up.
*/
public Object runProfiledMethod(AbstractJoinPoint joinPoint, Profiled profiled, LoggingStopWatch stopWatch)
throws Throwable {
//if we're not going to end up logging the stopwatch, just run the wrapped method
if (!stopWatch.isLogging()) {
return joinPoint.proceed();
}
stopWatch.setTimeThreshold(profiled.timeThreshold());
stopWatch.setNormalAndSlowSuffixesEnabled(profiled.normalAndSlowSuffixesEnabled());
Object retVal = null;
Throwable exceptionThrown = null;
try {
return retVal = joinPoint.proceed();
} catch (Throwable t) {
throw exceptionThrown = t;
} finally {
String tag = getStopWatchTag(profiled, joinPoint, retVal, exceptionThrown);
String message = getStopWatchMessage(profiled, joinPoint, retVal, exceptionThrown);
if (profiled.logFailuresSeparately()) {
tag = (exceptionThrown == null) ? tag + ".success" : tag + ".failure";
}
stopWatch.stop(tag, message);
}
}
/**
* Helper method gets the tag to use for StopWatch logging. Performs JEXL evaluation if necessary.
*
* @param profiled The profiled annotation that was attached to the method.
* @param joinPoint The AbstractJoinPoint encapulates the method around which this aspect advice runs.
* @param returnValue The value returned from the execution of the profiled method, or null if the method
* returned void or an exception was thrown.
* @param exceptionThrown The exception thrown, if any, by the profiled method. Will be null if the method
* completed normally.
* @return The value to use as the StopWatch tag.
*/
protected String getStopWatchTag(Profiled profiled,
AbstractJoinPoint joinPoint,
Object returnValue,
Throwable exceptionThrown) {
String tag;
if (Profiled.DEFAULT_TAG_NAME.equals(profiled.tag())) {
// look for properties-based default
// if the tag name is not explicitly set on the Profiled annotation,
final StringBuilder sb = new StringBuilder("tag.").append(joinPoint.getDeclaringClass().getName())
.append('.').append(joinPoint.getMethodName());
tag = Perf4jProperties.INSTANCE.getProperty(sb.toString());
if (tag == null) {
// fall back to using the name of the method being annotated.
tag = joinPoint.getMethodName();
} else if (tag.indexOf("{") >= 0) {
tag = evaluateJexl(tag,
joinPoint.getMethodName(),
joinPoint.getParameters(),
joinPoint.getExecutingObject(),
joinPoint.getDeclaringClass(),
returnValue,
exceptionThrown);
}
} else if (profiled.el() && profiled.tag().indexOf("{") >= 0) {
tag = evaluateJexl(profiled.tag(),
joinPoint.getMethodName(),
joinPoint.getParameters(),
joinPoint.getExecutingObject(),
joinPoint.getDeclaringClass(),
returnValue,
exceptionThrown);
} else {
tag = profiled.tag();
}
return tag;
}
/**
* Helper method get the message to use for StopWatch logging. Performs JEXL evaluation if necessary.
*
* @param profiled The profiled annotation that was attached to the method.
* @param joinPoint The AbstractJoinPoint encapulates the method around which this aspect advice runs.
* @param returnValue The value returned from the execution of the profiled method, or null if the method
* returned void or an exception was thrown.
* @param exceptionThrown The exception thrown, if any, by the profiled method. Will be null if the method
* completed normally.
* @return The value to use as the StopWatch message.
*/
protected String getStopWatchMessage(Profiled profiled,
AbstractJoinPoint joinPoint,
Object returnValue,
Throwable exceptionThrown) {
String message;
if (profiled.message().length() == 0) {
// look for properties-based default
// if the message name is not explicitly set on the Profiled annotation,
final StringBuilder sb = new StringBuilder("message.").append(joinPoint.getDeclaringClass().getName())
.append('.').append(joinPoint.getMethodName());
message = Perf4jProperties.INSTANCE.getProperty(sb.toString());
if (message == null) {
// may be null, that's OK
return message;
} else {
if (message.indexOf("{") >= 0) {
message = evaluateJexl(message,
joinPoint.getMethodName(),
joinPoint.getParameters(),
joinPoint.getExecutingObject(),
joinPoint.getDeclaringClass(),
returnValue,
exceptionThrown);
}
}
} else if (profiled.el() && profiled.message().indexOf("{") >= 0) {
message = evaluateJexl(profiled.message(),
joinPoint.getMethodName(),
joinPoint.getParameters(),
joinPoint.getExecutingObject(),
joinPoint.getDeclaringClass(),
returnValue,
exceptionThrown);
if ("".equals(message)) {
message = null;
}
} else {
message = profiled.message();
}
return message;
}
/**
* Helper method is used to parse out {expressionLanguage} elements from the text and evaluate the strings using
* JEXL.
*
* @param text The text to be parsed.
* @param methodName The name of the method that was annotated.
* @param args The args that were passed to the method to be profiled.
* @param annotatedObject The value of the object whose method was profiled. Will be null if a class method was
* profiled.
* @param annotatedClass The declaring class of the method that was annotated.
* @param returnValue The value returned from the execution of the profiled method, or null if the method
* returned void or an exception was thrown.
* @param exceptionThrown The exception thrown, if any, by the profiled method. Will be null if the method
* completed normally.
* @return The evaluated string.
* @see Profiled#el()
*/
@SuppressWarnings("unchecked")
protected String evaluateJexl(String text,
String methodName,
Object[] args,
Object annotatedObject,
Class<?> annotatedClass,
Object returnValue,
Throwable exceptionThrown) {
StringBuilder retVal = new StringBuilder(text.length());
//create a JexlContext to be used in all evaluations
JexlContext jexlContext = new HashMapContext();
for (int i = 0; i < args.length; i++) {
jexlContext.getVars().put("$" + i, args[i]);
}
jexlContext.getVars().put("$methodName", methodName);
jexlContext.getVars().put("$this", annotatedObject);
jexlContext.getVars().put("$class", annotatedClass);
jexlContext.getVars().put("$return", returnValue);
jexlContext.getVars().put("$exception", exceptionThrown);
// look for {expression} in the passed in text
int bracketIndex;
int lastCloseBracketIndex = -1;
while ((bracketIndex = text.indexOf('{', lastCloseBracketIndex + 1)) >= 0) {
retVal.append(text.substring(lastCloseBracketIndex + 1, bracketIndex));
lastCloseBracketIndex = text.indexOf('}', bracketIndex + 1);
if (lastCloseBracketIndex == -1) {
//if there wasn't a closing bracket index just go to the end of the string
lastCloseBracketIndex = text.length();
}
String expressionText = text.substring(bracketIndex + 1, lastCloseBracketIndex);
if (expressionText.length() > 0) {
try {
Object result = getJexlExpression(expressionText).evaluate(jexlContext);
retVal.append(result);
} catch (Exception e) {
//we don't want to propagate exceptions up
retVal.append("_EL_ERROR_");
}
}
}
//append the final part
if (lastCloseBracketIndex < text.length()) {
retVal.append(text.substring(lastCloseBracketIndex + 1, text.length()));
}
return retVal.toString();
}
/**
* Helper method gets a compiled JEXL expression for the specified expression text, either from the cache or by
* creating a new compiled expression.
*
* @param expressionText The JEXL expression text
* @return A compiled JEXL expression representing the expression text
* @throws Exception Thrown if there was an error compiling the expression text
*/
protected Expression getJexlExpression(String expressionText) throws Exception {
Expression retVal = jexlExpressionCache.get(expressionText);
if (retVal == null) {
//Don't need synchronization here - if we end up calling createExpression in 2 separate threads, that's fine
jexlExpressionCache.put(expressionText, retVal = ExpressionFactory.createExpression(expressionText));
}
return retVal;
}
}