7

I have a Java6 project that is being migrated to Java8. We used aspectj to log some of users actions, like button clicking.

So there are listeners like this:

    button.addClickListener(new Button.ClickListener() {
        @Override
        public void buttonClick(Button.ClickEvent clickEvent) {
            doSth();
        }
    });

And poincut:

@Pointcut("execution(public void Button.ClickListener.buttonClick(Button.ClickEvent))")
public void buttonClick() {};

But since we will use Java8, listeners will be like this:

button.addClickListener(clickEvent -> doSth());

Is there any way to write aspectj pointcut, so that it handles new listeners?

Lete
  • 73
  • 1
  • 7
  • I think your `pointcut` should still be working because you have modified the `addClickListener` to use `Lambda` and your `Button.ClickListener.buttonClick` still ramains the same. Did you modified that also ? – Shirishkumar Bari Jun 11 '15 at 11:55
  • It's definitely not working. I only modified addClickListener to use lambda. Yes, it still uses Button.ClickListener.buttonClick but function that sould be executed before pointcut (marked as: @Before("buttonClick()") is never called. It is called when non-lambda definition is used. – Lete Jun 11 '15 at 12:05
  • So this implies that AspectJ is not capable of patching the methods of the listener class generated by the `LambdaMetaFactory` at runtime. But can it modify `default` methods of `interface`s? That would lead to a possible solution… – Holger Jun 11 '15 at 13:29

2 Answers2

4

I guess the problem is that lambdas do not seem to actually implement/override any interface methods with a corresponding name, but create an anonymous method. Look at this example:

Dummy button class, replicating the parts of Vaadin we need here:

package de.scrum_master.app;

public class Button {
    private ClickListener listener;

    public void addClickListener(ClickListener listener) {
        this.listener = listener;
    }

    public void click() {
        System.out.println("Clicking button");
        listener.buttonClick(new ClickEvent());
    }

    public static class ClickEvent {}

    public static interface ClickListener {
        public void buttonClick(ClickEvent clickEvent);
    }
}

Driver application:

package de.scrum_master.app;

public class Application {
    protected static void doSomething() {}

    public static void main(String[] args) {
        Button button = new Button();
        button.addClickListener(new Button.ClickListener() {
            @Override
            public void buttonClick(Button.ClickEvent clickEvent) {
                doSomething();
            }
        });
        button.click();

        button = new Button();
        button.addClickListener(clickEvent -> doSomething());
        button.click();
    }
}

As you can see, two button instances are created, one with a classical anonymous class listener, one with a lambda listener. Both buttons get clicked, so consequently the console log looks like this:

Clicking button
Clicking button

Aspect:

package de.scrum_master.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class ButtonClickLogger {
    @Before("execution(public void *..Button.ClickListener.buttonClick(*..Button.ClickEvent))")
    public void logButtonClick(JoinPoint thisJoinPoint) {
        System.out.println("Caught button click: " + thisJoinPoint);
    }

    @Before("within(*..Application)")
    public void logAllInListener(JoinPoint thisJoinPoint) {
        System.out.println("  " + thisJoinPoint);
    }

    @Before("execution(void *..lambda*(*..Button.ClickEvent))")
    public void logButtonClickLambda(JoinPoint thisJoinPoint) {
        System.out.println("Caught button click (lambda): " + thisJoinPoint);
    }
}

The first advice uses a pointcut similar to yours. It can only intercept classical listener declarations.

The second advice is for debugging purposes and logs all joinpoints within the driver application in order to show what the heck is going on here.

Last, but not least, the third advice shows a workaround for intercepting lambda-based listeners, relying on the knowledge about Java compiler naming for lambdas acquired from the debug output. This is not very nice, but for the moment it works. Please note that the lambda version of buttonClick(..) is not public, so it is just void *..lambda*, not public void *..lambda*.

Console output with AspectJ:

  staticinitialization(de.scrum_master.app.Application.<clinit>)
  execution(void de.scrum_master.app.Application.main(String[]))
  call(de.scrum_master.app.Button())
  call(de.scrum_master.app.Application.1())
  staticinitialization(de.scrum_master.app.Application.1.<clinit>)
  preinitialization(de.scrum_master.app.Application.1())
  initialization(de.scrum_master.app.Application.1())
  initialization(de.scrum_master.app.Button.ClickListener())
  execution(de.scrum_master.app.Application.1())
  call(void de.scrum_master.app.Button.addClickListener(Button.ClickListener))
  call(void de.scrum_master.app.Button.click())
Clicking button
Caught button click: execution(void de.scrum_master.app.Application.1.buttonClick(Button.ClickEvent))
  execution(void de.scrum_master.app.Application.1.buttonClick(Button.ClickEvent))
  call(void de.scrum_master.app.Application.doSomething())
  execution(void de.scrum_master.app.Application.doSomething())
  call(de.scrum_master.app.Button())
  call(void de.scrum_master.app.Button.addClickListener(Button.ClickListener))
  call(void de.scrum_master.app.Button.click())
Clicking button
  execution(void de.scrum_master.app.Application.lambda$0(Button.ClickEvent))
Caught button click (lambda): execution(void de.scrum_master.app.Application.lambda$0(Button.ClickEvent))
  call(void de.scrum_master.app.Application.doSomething())
  execution(void de.scrum_master.app.Application.doSomething())

Update: There is a corresponding Bugzilla issue now for AspectJ. I have just created it. It also points to a recent discussion on the mailing list.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • In case anybody else encounters a similar problem, some lambdas also create static methods, which this answer currently doesn't track. – Simon Eismann Feb 02 '18 at 09:01
  • 1
    Well, there was no question here about static methods in lambdas. Would you mind being more specific and providing sample code? You could create a new question describing your problem with sample code and link to it from here. I would like to take a look for you and help if I can. – kriegaex Feb 02 '18 at 10:56
0

There is one simple solution for this. You can write the pointcut to intercept the click event of the lambda expression.

    @Pointcut("execution(void *..lambda*(android.view.View))")
    public void dealWithLambda() {
    }
 
    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {
    }
 

   @Before("( dealWithNormal() || dealWithLambda())  && args(view)")
   public void beforeClick(View view) {
    Log.e(TAG, "before: " + ((TextView) view).getText().toString();
   }

Currently I used "view" as parameter for beforeClick(View view) but you can JoinPoint or ProceedingJoinPoint as parameter as per your need.

Refere the below article for more reference: https://www.programmersought.com/article/38871580011/

Soft Code
  • 49
  • 5