First, what do we want to solve in this example? Let us say, we wish to monitor the performance of certain methods and report back to us, in a standardized way, what went on. We decide we want to annotate the methods we want to track, and by passing in a few arguments in the annotation, that will be enough to report back the time, name, etc.
In our CDI AOP example, we saw how powerful yet simple the CDI AOP model is. Spring is even more powerful, and more complex. AspectJ is even more of both. There are so many options with Spring's AOP that I will just go through what I consider a complete example that give's you many of what I believe to be common applications of AOP, and hopefully reduce the amount of time spent piecemealing all the different parts together.
Note: Spring uses AspectJ to handle it's annotations even though you don't have to implement the entire AspectJ environment. You can do everything via the xml files, and then you don't need AspectJ, but I like annotations, so we are going to implement this with AspectJ enabled.
Let's get started. First, we are going to need some additional libraries. Download the AspectJ jar file from http://www.eclipse.org/aspectj/ and extract the libraries directory to wherever you are putting your libraries. Then include them in your project.
Next we need to modify our applicationContext.xml file. I'm going to put here the context file for our unit test, you can see the complete applicationContext at the end of the post: SpringAOPTest.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
">
<context:annotation-config/>
<context:component-scan base-package="com.sample"/>
<aop:aspectj-autoproxy />
<bean id="performanceProfiler" class="com.sample.aop.PerformanceInterceptor" />
</beans>
Now here we do have some items that we've seen in other posts, but let's go over the whole file anyway. First, we declare the name spaces with the xlmns:... Once that is done, we tell spring where to find the definitions for those name spaces with the xsi:schemaLocation. Next we enable spring to be able to use annotations, and what packages to scan for annotations with the two <context..> lines. Enabling annotations is different from enabling AspectJ. AspectJ allows us to use annotations to define our interceptor, which we want to do, but what we did first, was to enable us to scan the java classes and find say @Component to create a bean or @Autowired to inject a bean. Next, we enable AspectJ, which we do with the <aop:aspectj-autoproxy/>. And finally, we define the actual bean that is our AOP interceptor. Now interestingly, the bean id doesn't really matter here. It doesn't need to be the name of our interceptor class, nor the name of our annotation, and we don't inject it. It could just as easily be PerformanceInterceptor, performanceInterceptor, performanceMonitor, or as I put it for demonstration, performanceProfiler.
So far, all we have done is enabled our ability to code / run our interceptor. Now we need the parts that will make our interceptor work. We need a bean that will be intercepted. This is different then the interceptor bean. We need an annotation. This is different then the AspectJ annotations that define our interceptor. We need the interceptor. And we'll need a unit test.
Let's first give a bean to be intercepted. TestPerformanceMonitorBean.java:
package com.sample.beans;
import com.sample.aop.PerformanceMonitor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* @author Thomas Dias
*/
@Component
@Scope("prototype")
@PerformanceMonitor(data="data")
public class TestPerformanceMonitorBean {
@PerformanceMonitor(name="TPMB")
public Object someMethod() {
System.out.println("entered some method");
return new Object();
}
@PerformanceMonitor(name="MyName", db="slfjksfkl")
public void otherMethod(Object t) {
System.out.println("second test");
}
public void thirdMethod() throws Exception {
System.out.println("testing class annotation");
throw new Exception();
}
}
Here we defined our bean with an annotation @Component. Because we enabled annotation configuration / scanning, this will be seen and a bean will be created. We can then inject this bean into our unit test. Now, the next line is @Scope("prototype"). Your scope is going to be defined by your requirements. A Spring Bean by default is a Singleton per application context. That can be very good, and, can be very bad. So here, we demonstrate a bean that gets a new instance everytime someone uses this bean. We could have used request scope in an http environment to accomplish this, but since we are currently only using this for a unit test and I wanted to demonstrate using scope, I just used prototype.
We annotated our class, and our methods with @PerformanceMonitor. You'll see in a minute how we interpreted those annotations, but the way we implemented it, if there is a method annotation, it is used, otherwise it uses the class annotation. Either the class annotation or the method annotation must be present, or our pointcut will not fire the interceptor. Also notice, we have some methods returning values, and some methods throwing exceptions. All of that is handled by the interceptor.
Now that we have our bean, we'll look at our annotation: PerformanceMonitor.java
package com.sample.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Thomas Dias
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface PerformanceMonitor {
public String name() default "";
public String db() default ":";
public String table() default ":";
public String data() default ":";
}
The interceptor will check the annotation on the class or method. To do that, it must be kept around till runtime. We do that with the @Retention(RetentionPolicy.RUNTIME) annotation. Next we tell this annotation where it is allowed (on a class or a method) with the @Target line. We turn this class into an annotation with the @interface annotation, and lastly we define some parameters that may be used with this annotation. For our parameters, we supplied some default values. We could have required that these parameters are supplied, but we wanted the usage of this annotation to be @PerformanceMonitor([name="someName"][,db="someDb"]...) where the attributes are separated by a comma and each attribute is optional.
We have our bean annotated and our annotation defined, now I present the interceptor: PerformanceInterceptor.java
package com.sample.aop;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
/**
* @author Thomas Dias
*/
class PerformanceInfo {
long startTime, endTime;
String message, name, data, table, db;
boolean success;
PerformanceInfo(boolean b) {
startTime = System.currentTimeMillis();
}
void stopTiming() {
endTime = System.currentTimeMillis();
System.out.println(name + " " + (endTime - startTime) + " " + db
+ " " + table + " " + data + " " + success + " " + message);
}
public void setData(String data) {
this.data = data;
}
public void setDb(String db) {
this.db = db;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public void setMessage(String message) {
this.message = message;
}
public void setName(String name) {
this.name = name;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public void setSuccess(boolean success) {
this.success = success;
}
public void setTable(String table) {
this.table = table;
}
}
/**
* class that will perform the aop and intercept methods to monitor and log their performance
* the method to be intercepted must be a public method, must be a bean annotated with:
* class that will perform the aop and intercept methods to monitor and log their performance
* the method to be intercepted must be a public method, must be a bean annotated with:
* @PerformanceMonitor(name="",db="",table="",data="") where the attributes name, db, table, and data are optional
*/
*/
@Aspect
public class PerformanceInterceptor {
@Around("execution(* com.sample..*.*(..)) && (@annotation(PerformanceMonitor) || @target(PerformanceMonitor))")
public Object profile(ProceedingJoinPoint jp) throws Throwable {
String methodName = jp.getSignature().getName();
MethodSignature methodSignature = (MethodSignature)jp.getSignature();
Method method = methodSignature.getMethod();
if (method.getDeclaringClass().isInterface()) {
method = jp.getTarget().getClass().getDeclaredMethod(methodName, method.getParameterTypes());
}
PerformanceMonitor annotation = this.getPerformanceMonitorAnnotation(method);
Throwable throwable = null;
Object result = null;
PerformanceInfo perf = new PerformanceInfo(true);
perf.setSuccess(false);
if(annotation.name().equals("")) {
perf.setName(methodName);
} else {
perf.setName(annotation.name());
}
perf.setDb(annotation.db());
perf.setTable(annotation.table());
perf.setData(annotation.data());
// Execute the method if result is null, fail
try {
result = jp.proceed();
if (result == null) {
perf.setMessage("Result is null");
} else {
perf.setSuccess(true);
}
} catch (Throwable e) {
throwable = e;
perf.setMessage("Error: " + e.getMessage());
}
perf.stopTiming();
// do something with our performance object
// If something was thrown throw it, else return the result
if (throwable != null) {
throw (throwable);
}
return result;
}
public PerformanceMonitor getPerformanceMonitorAnnotation(Method method) {
for (Annotation a : method.getAnnotations()) {
if (a instanceof PerformanceMonitor) {
return (PerformanceMonitor) a;
}
}
for (Annotation a : method.getDeclaringClass().getAnnotations()) {
if (a instanceof PerformanceMonitor) {
return (PerformanceMonitor) a;
}
}
return null;
}
}
Okay, so there is a lot more here then is necessary, but I figured we'd do a little more then just log the elapsed time to a log file. The interceptor is instantiating another class that will be our timer, hold all the data like name, database, etc. and then we can pass that object elsewhere. Our interceptor starts by getting the @Aspect annotation. The PerformanceInterceptor then needs what is called a pointcut. A pointcut is just a fancy name for the definition of what methods will be intercepted. You can create a method that has a separate pointcut, or as we did here, we included the pointcut with the advice. What is the advice? The definition of what type of interceptor we are going to use. Spring allows us several types of interceptors. There are before a method, after a method, if a method throws and exception, if a method returns something, and around a method. The around method combines the other types and is the one we chose for this interceptor. I never mentioned what an interceptor is. It intercepts the call to your method or field and performs some action. Here, we are using it to catch all calls to a method annotated with @PerformanceMonitor and wrap that method in our action. What is our interceptor? Well, it is the method: public Object profile(ProceedingJoinPoint jp) throws Throwable.
Let us disect the pointcut we used for a moment. First, when do we want to intercept? When the method is about to be executed. So, we start with methods at execution time. Next, what is the signature of the methods we are intercepting? Well, any return type, and any method who's package starts with com.sample. Any class name from those packages. Any method name from those classes. And lastly any method with any type of parameters. So, we have: execution(* com.sample..*.*(..))
We see from this example, we have a lot of power in defining what will be intercepted. If we used the pointcut above as is, we intercept all the methods (from package com.sample) executed by any bean, and we didn't want that, so we put in another qualifier. We said, is the method annotated? or is the class annotated? We did that by: @annotation(PerformanceMonitor) || @target(PerformanceMonitor)
Tie those two together with an && (or and) and we have our pointcut: @Around("execution(* com.sample..*.*(..)) && (@annotation(PerformanceMonitor) || @target(PerformanceMonitor))"). Because the method we are intercepting may throw an exception, our interceptor will pass that along by declaring that it may throw an exception. And it needs to return whatever our intercepted method was going to return. So, we return an object.
So, now our interceptor will execute when a method is executed. Now why do I use that term? because in AspectJ, there are also interceptors when a method is called. Spring doesn't do that, but it is out there. Now when a method is intercepted, we continue on to the method by having the interceptor call the JoinPoint.proceed(). With the @Around advice, we use the ProceedingJoinPoint. You'll see the proceed() called toward the bottom of the method, but first, we interpret the annotations and assign values based on them. We get the start time, etc. Then we use the try catch to continue on with proceed(). We stop timing, log the information, throw the exception if one was thrown, or return what was returned. And we're done.
Let's test it with a unit test: PerformanceInterceptorTest.java
Tie those two together with an && (or and) and we have our pointcut: @Around("execution(* com.sample..*.*(..)) && (@annotation(PerformanceMonitor) || @target(PerformanceMonitor))"). Because the method we are intercepting may throw an exception, our interceptor will pass that along by declaring that it may throw an exception. And it needs to return whatever our intercepted method was going to return. So, we return an object.
So, now our interceptor will execute when a method is executed. Now why do I use that term? because in AspectJ, there are also interceptors when a method is called. Spring doesn't do that, but it is out there. Now when a method is intercepted, we continue on to the method by having the interceptor call the JoinPoint.proceed(). With the @Around advice, we use the ProceedingJoinPoint. You'll see the proceed() called toward the bottom of the method, but first, we interpret the annotations and assign values based on them. We get the start time, etc. Then we use the try catch to continue on with proceed(). We stop timing, log the information, throw the exception if one was thrown, or return what was returned. And we're done.
Let's test it with a unit test: PerformanceInterceptorTest.java
package com.sample.aop;
import com.sample.beans.TestPerformanceMonitorBean;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.*;
/**
* @author Thomas Dias
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/SpringAOPTest.xml")
public class PerformanceInterceptorTest {
@Autowired
TestPerformanceMonitorBean instance;
@Test
public void testProfile() throws Throwable {
Object a = instance.someMethod();
assertNotNull(a);
instance.otherMethod(null);
try {
instance.thirdMethod();
fail("method should have thrown an exception");
} catch (Exception e) {
}
}
}
The output
entered some method
TPMB 33 : : : true null
second test
MyName 0 slfjksfkl : : false Result is null
testing class annotation
thirdMethod 0 : : data false Error: null
Reviewing the output, we see that the first line from the interceptor comes after the output from the method we called. The interceptor wrapped the method and started the timer before calling the method, then after the method returned, it output the details. We see the "TPMB" from our annotation, the time it took, the default values for the attributes we didn't supply. Next we have our success value of true because the method returned a non null value result. And finally null as our message, since we never set it. The second method is called, and the output from the interceptor has the name and db attributes set, so they output. 0 is the time which simply means we didn't do enough in our method. The success is false, since nothing was returned. Of course, we notice on further inspection, the method we called was of return type void. And lastly our message was set to say "Result is null". The third method gets its annotation from the class. We didn't supply a name, so the name was set to the called method name. The method threw an exception, so success was set to false. And there we have it. An interceptor based on an annotation to profile whatever method or class we chose. Hopefully I have given enough of a base and answered a few of your questions.
No comments:
Post a Comment