sobota, 23 lutego 2013

Various @Aspects of Java (Spring AOP)

Intro

The easiest way of learning and implementing aspects is with Spring-AOP. Regarding configuration, as always, you've got two options: XML and annotations. In this article I'll show a small example using annotations:

First, I'll present very simple xml configuration




    
    
    
    
    
    

Now, very simple class defining our pointcut and its implementation:

package com.mp.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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.stereotype.Component;
import org.springframework.util.StopWatch;

@Component
@Aspect
public class ProfilingAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(ProfilingAspect.class);

    @Pointcut("execution( * com.mp.intercepted.jvmproxy..*(..) )")
    public void profilingPointcut() {}

    @Around("profilingPointcut()")
    public String logProfileInfo(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        String result = "";
        try {
            result = (String)joinPoint.proceed();
        } catch (Throwable throwable) {
            LOGGER.error(String.format("Error when executing original method [%s]", methodName), throwable);
            stopWatch.stop();
            LOGGER.debug(String.format("Elapsed time when error occured executing method[%s] : [%s]", methodName, stopWatch));
            if(throwable instanceof Error){
                throw (Error)throwable;
            }
        }
        stopWatch.stop();
        LOGGER.debug(String.format("Elapsed time executing method [%s]: [%s]", methodName, stopWatch));
        return "Intercepted: " + result;
    }

}

Pointcut expression "execution( * com.mp.intercepted.jvmproxy..*(..) )" instructs Spring to intercept any method call from package com.mp.intercepted.jvmproxy.

Weaving cheat - JVM Proxy

In original, non-spring AOP, apart from pointuct definitions and classes to be intercepted, we need a weaver - an agent that will weave our code either at compile time, load time or in runtime. With spring AOP it is a bit simpler:

JVM Proxy

If we define an interface for a bean:

package com.mp.intercepted.jvmproxy;

public interface InterceptedWithSpringAopJvmProxy {
    String methodToBeIntercepted();
}

... and an implementation:

package com.mp.intercepted.jvmproxy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;


@Component
public class InterceptedWithSpringAopImpl implements InterceptedWithSpringAopJvmProxy {
    private static final Logger LOGGER = LoggerFactory.getLogger(InterceptedWithSpringAopImpl.class);

    public String methodToBeIntercepted(){
        LOGGER.debug("Entering method to be intercepted");
        return "Not intercepted";
    }
}

... then Spring itself will generate dynamic proxy when creating our bean. The easiest way to find this out is to put a breakpoint on original method and analyze the stack-trace

   at com.mp.intercepted.jvmproxy.InterceptedWithSpringAopImpl.methodToBeIntercepted(InterceptedWithSpringAopImpl.java:20)
   at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:601)
   at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:318)
   at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
   at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
   at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:80)
   at com.mp.aspect.ProfilingAspect.logProfileInfo(ProfilingAspect.java:34)
   at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:601)
   at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:621)
   at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:610)
   at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:65)
   at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
   at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:90)
   at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
   at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
   at $Proxy12.methodToBeIntercepted(Unknown Source:-1)

You can see at lines 21 and 22 in the above snippet that indeed $Proxy... is used.

Instrumentation with CGLIB

Dynamic proxy mechanism is used only when there is an interface that the JDK can use to produce the proxy. If we remove the interface, then we will get following exception:

 ... 
Caused by: java.lang.ClassNotFoundException: org.aspectj.weaver.reflect.ReflectionWorld$ReflectionWorldException
 at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
 at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
 at java.security.AccessController.doPrivileged(Native Method)
 at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
 at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
 ... 52 more

The above exception informs us that we have no aspectj weaver in our depencencies. Adding it still does not solve the problem, as we get following exception:

        ...
Caused by: org.springframework.aop.framework.AopConfigException: Cannot proxy target class because CGLIB2 is not available. Add CGLIB to the class path or specify proxy interfaces.
 at org.springframework.aop.framework.DefaultAopProxyFactory.createAopProxy(DefaultAopProxyFactory.java:67)
 at org.springframework.aop.framework.ProxyCreatorSupport.createAopProxy(ProxyCreatorSupport.java:104)
 at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:112)
 at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:476)
 at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:362)
 at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:322)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:407)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1461)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:519)
 ... 40 more

After putting cglib2 dependency into pom classes not implementing any interface get correctly weaved with cglib and aspects get fired as expected

<!--CGLIB-->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>

Limitations

As shown in previous two examples, implemeting aspects with spring AOP is very easy. In case of dynamic JVM proxies, we don't even need AspectJ weaver to be present, as actually no weaving takes place. However, this approach has some serious limitations:

  1. You cannot intercept static methods
  2. You cannot intercept method calls on objects that are not spring managed beans

In the next post I'll describe how to overcome these limitations with AspectJ

Source code location

You can find the source code related to this post in my github repository

PS: Problem with self-invocation

Let's assume we have following class:
package com.mp.nestedintercepted;

import com.mp.intercepted.jvmproxy.InterceptedWithSpringAopJvmProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class InterceptedWithSpringAopNested  {
    private static final Logger LOGGER = LoggerFactory.getLogger(InterceptedWithSpringAopNested.class);

    public Integer nested(){
        LOGGER.debug("Entering method to be intercepted");
        return nested("") + 1;

    }

    public Integer nested(String arg){
        return 1;
    }
}

and following pointcut

@Pointcut("execution( * com.mp.nestedintercepted..nested(..) )")

"Implementation" of this pointuct is as follows:

    @Around("nestedPointcut()")
    public int addOneToOriginalResult(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        int result = 0;
        try{
            result = (Integer)joinPoint.proceed();
            result += 1;
        } catch (Throwable t){
            LOGGER.error(String.format("Error when executing original method [%s]", methodName), t);
            if(t instanceof Error){
                throw (Error)t;
            }
        }
        return result;
    }

You could expect, that both method call to nested() and to nested(String arg) will be intercepted, so any method call to nested() should return 4. Nevertheless, because interception (or weaving), in case of Spring AOP, is implemented using proxies, and the original class InterceptedWithSpringAopNested knows nothing of the proxy, the call to nested(String arg) is just a regular method call. If you download source code, you can check this behaviour running test InterceptedWithSpringAopNested.