rudolfv
rudolfv

Reputation: 817

AspectJ annotation on field that triggers @Before advice

I have already written AspectJ aspects that perform @Around advice triggered by method annotations. Now I want to do the same, but where fields are annotated instead of methods. So with each method invocation of the class below, it must set the accountSummary field to the correct implementation. Is there a way to accomplish this? I presume using @Before advice would be the best way of going about it. Using CDI is not an option - the solution must use AspectJ.

public class PoolableBusinessLogic {
   @InjectServiceClientAdapter(legacy=LegacyAccountSummary.class,new=NewAccountSummary.class)
   private AccountSummary accountSummary;

   public void foo() {
      // use correct accountSummary impl, decided in @Before code
   }

   public void bar() {
      // use correct accountSummary impl, decided in @Before code
   }
}

Upvotes: 0

Views: 1958

Answers (2)

rudolfv
rudolfv

Reputation: 817

For Option A: dynamic injection in kriegaex's answer, the annotation-style aspect will look like this:

@Aspect
public class InjectServiceClientAdapterAspect {
    @Pointcut("get(* *) && @annotation(injectAnnotation)")
    public void getServiceClientAdapter(InjectServiceClientAdapter injectAnnotation) {
    }

    @Around("getServiceClientAdapter(injectAnnotation)")
    public Object injectServiceClientAdapter(final ProceedingJoinPoint joinPoint, final InjectServiceClientAdapter injectAnnotation) {      
       // injection code goes here                                          
}

Upvotes: 0

kriegaex
kriegaex

Reputation: 67377

I am not sure what exactly you want to achieve, so I am presenting two alternative solutions.

First, let us create some application classes in order to have a fully compileable sample:

package de.scrum_master.app;

public interface AccountSummary {
    void doSomething();
}
package de.scrum_master.app;

public class LegacyAccountSummary implements AccountSummary {
    @Override
    public void doSomething() {
        System.out.println("I am " + this);
    }
}
package de.scrum_master.app;

public class NewAccountSummary implements AccountSummary {
    @Override
    public void doSomething() {
        System.out.println("I am " + this);
    }
}
package de.scrum_master.app;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface InjectServiceClientAdapter {
    Class<?> legacyImpl();
    Class<?> newImpl();
}
package de.scrum_master.app;

public class PoolableBusinessLogic {
    @InjectServiceClientAdapter(legacyImpl = LegacyAccountSummary.class, newImpl = NewAccountSummary.class)
    private AccountSummary accountSummary;

    public void foo() {
        accountSummary.doSomething();
    }

    public void bar() {
        System.out.println("Account summary is " + accountSummary);
    }
}

Now we need an entry point:

package de.scrum_master.app;

public class Application {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            PoolableBusinessLogic businessLogic = new PoolableBusinessLogic();
            businessLogic.foo();
            businessLogic.bar();
            System.out.println();
        }
    }
}

Obviously this yields an error because the member accountSummary has not been initialised:

Exception in thread "main" java.lang.NullPointerException
    at de.scrum_master.app.PoolableBusinessLogic.foo(PoolableBusinessLogic.java:8)
    at de.scrum_master.app.Application.main(Application.java:7)

Now we have two options, depending on what you want to achieve:

Option A: dynamic injection

Scenario: For each field access (even in the same PoolableBusinessLogic instance) decide dynamically what type of object instance to return. Here in this example I will just be randomising in order to simulate another if-else criterion.

BTW, I hope it is okay that I use the more expressive native AspectJ syntax. You can easily convert the aspect to annotation style.

package de.scrum_master.aspect;

import java.util.Random;
import org.aspectj.lang.SoftException;
import de.scrum_master.app.InjectServiceClientAdapter;

public aspect DynamicInjectionAspect {
    private static final Random RANDOM = new Random();

    Object around(InjectServiceClientAdapter adapterAnn) :
        get(* *) && @annotation(adapterAnn)
    {
        try {
            Class<?> implClass = RANDOM.nextBoolean() ? adapterAnn.legacyImpl() : adapterAnn.newImpl();
            return implClass.newInstance();
        } catch (Exception e) {
            throw new SoftException(e);
        }
    }
}

This yields the following output:

I am de.scrum_master.app.LegacyAccountSummary@4d9cfefb
Account summary is de.scrum_master.app.NewAccountSummary@7e28388b

I am de.scrum_master.app.NewAccountSummary@2986e62
Account summary is de.scrum_master.app.LegacyAccountSummary@6576e542

I am de.scrum_master.app.NewAccountSummary@60c58418
Account summary is de.scrum_master.app.LegacyAccountSummary@4763754a

I am de.scrum_master.app.NewAccountSummary@52a971e3
Account summary is de.scrum_master.app.NewAccountSummary@7274187a

I am de.scrum_master.app.LegacyAccountSummary@23f32c4a
Account summary is de.scrum_master.app.LegacyAccountSummary@31e0c0b6

As you can see, within each of the five output groups (i.e. for each PoolableBusinessLogic instance) there are different account summary object IDs and sometimes (not always) even different class names.

Option B: static injection

Scenario: Per PoolableBusinessLogic instance decide dynamically what type of object instance to statically assign to the annotated member if its value is null. After that, do not overwrite the member anymore but return the previously initialised value. Again I will just be randomising in order to simulate another if-else criterion.

Attention: Do not forget to deactivate the first aspect, e.g. by prepending if(false) && to its pointcut. Otherwise the two aspects will be conflicting with each other.

package de.scrum_master.aspect;

import java.lang.reflect.Field;
import java.util.Random;
import org.aspectj.lang.SoftException;
import de.scrum_master.app.InjectServiceClientAdapter;

public aspect StaticInjectionAspect {
    private static final Random RANDOM = new Random();

    before(InjectServiceClientAdapter adapterAnn, Object targetObj) :
        get(* *) && @annotation(adapterAnn) && target(targetObj)
    {
        try {
            Field field = targetObj.getClass().getDeclaredField(thisJoinPoint.getSignature().getName());
            field.setAccessible(true);
            if (field.get(targetObj) != null)
                return;
            Class<?> implClass = RANDOM.nextBoolean() ? adapterAnn.legacyImpl() : adapterAnn.newImpl();
            field.set(targetObj,implClass.newInstance());
        } catch (Exception e) {
            throw new SoftException(e);
        }
    }
}

This is a bit uglier because it involves using reflection for finding the member field. Because it might be (and in our example really is) private we need to make it accessible before doing anything with it.

This yields the following output:

I am de.scrum_master.app.NewAccountSummary@20d1fa4
Account summary is de.scrum_master.app.NewAccountSummary@20d1fa4

I am de.scrum_master.app.NewAccountSummary@2b984909
Account summary is de.scrum_master.app.NewAccountSummary@2b984909

I am de.scrum_master.app.LegacyAccountSummary@1ae3043b
Account summary is de.scrum_master.app.LegacyAccountSummary@1ae3043b

I am de.scrum_master.app.LegacyAccountSummary@2e2acb47
Account summary is de.scrum_master.app.LegacyAccountSummary@2e2acb47

I am de.scrum_master.app.LegacyAccountSummary@7b87b9fe
Account summary is de.scrum_master.app.LegacyAccountSummary@7b87b9fe

Now the output looks different: Within each of the five output groups (i.e. for each PoolableBusinessLogic instance) both output lines show exactly the same object ID.

Upvotes: 1

Related Questions