问题
I am new with AspectJ annotation for Java, and I am wondering if it is possible to put pointcut on a cross thread invocation.
Here is the code:
public class App {
public static void main( String[] args ) {
new Connector().getStart("testtest");
}
}
public class Connector {
public void getStart(String s1) {
Handler h = new Handler(s1);
h.start();
}
}
public class Handler extends Thread {
String s1;
public Handler(String s1) {
this.s1 = s1;
}
public void run() {
new Plain().getValue(s1);
}
}
public class Plain {
public void getValue(String s1) {
System.out.println("Plain getValue: " + s1);
}
}
I would like to have a pointcut that only triggers when Plain.getValue()
is called by Connector.getStart()
.
Is it possible? Thanks.
回答1:
You are making a mistake believing that Plain.getValue(..)
is called by Connector.getStart(..)
because in a multi-threaded environment it is not. Let me prove it with a little tweak to the getValue(..)
method, printing a stack trace:
package de.scrum_master.app;
public class Plain {
public void getValue(String s1) {
System.out.println("Plain getValue: " + s1);
new Exception().printStackTrace(System.out);
}
}
By the way, I have moved all your classes to package de.scrum_master.app
because using the default package is discouraged in Java and also AspectJ does not like it when trying to match pointcuts.
Console log (multi-threaded):
Plain getValue: testtest
java.lang.Exception
at de.scrum_master.app.Plain.getValue(Plain.java:4)
at de.scrum_master.app.Handler.run(Handler.java:9)
See? There is no trace of Connector.getStart(..)
in the log. If we also tweak getStart(..)
so as to call the thread's run()
method directly (i.e. not starting a new thread but executing in the same thread) instead of start()
, the situation changes:
package de.scrum_master.app;
public class Connector {
public void getStart(String s1) {
Handler h = new Handler(s1);
h.run();
}
}
Console log (single-threaded):
Plain getValue: testtest
java.lang.Exception
at de.scrum_master.app.Plain.getValue(Plain.java:4)
at de.scrum_master.app.Handler.run(Handler.java:9)
at de.scrum_master.app.Connector.getStart(Connector.java:4)
at de.scrum_master.app.App.main(App.java:3)
In this situation we could use AspectJ's dynamic cflow()
(control flow) pointcut like this:
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 SingleThreadAspect {
@Before("execution(* de.scrum_master.app.Plain.getValue(..)) && cflow(execution(* de.scrum_master.app.Connector.getStart(..)))")
public void interceptControlFlow(JoinPoint thisJoinPoint) {
System.out.println(thisJoinPoint);
}
}
The advice would be triggered just as you wish. But for the reason explained at the beginning of my answer cflow()
does not (and cannot) work across threads because there is no such thing as a direct control flow across threads. Each thread's control flow starts with its run()
method, no earlier. That is the whole concept of multi-threading.
So if you really want to emulate something like a cross-thread control flow for whatever doubtful reason, you need to do some manual bookkeeping.
But first let us revert the tweaked h.run()
back to the original h.start()
so as to reinstate the multi-threading situation. Let us also remove the printStackTrace(..)
line from Plain.getStart(..)
.
Solution:
Disclaimer: I do not like annotation-style @AspectJ syntax, so I am switching over to native syntax. It is much more expressive and we can achieve what we want more easily in terms of ITD (inter-type definition) because
- in native syntax we can just declare an additional instance member variable for a given class while
- in @AspectJ syntax we would have to declare the target class to implement an interface with a default implementation which in turn would carry the member variable for our manual bookkeeping.
Let us modify App
so as to also start a Handler
thread directly. This is our negative test case because we do not want to trigger our advice there as the thread is started outside of Plain.getValue(..)
:
package de.scrum_master.app;
public class App {
public static void main(String[] args) throws InterruptedException {
// The aspect should ignore this thread
new Handler("foo").start();
// Wait a little while so as not to mess up log output
Thread.sleep(250);
new Connector().getStart("testtest");
}
}
Console log without aspect:
Plain getValue: foo
Plain getValue: testtest
Aspect:
package de.scrum_master.aspect;
import de.scrum_master.app.*;
public aspect CrossThreadAspect {
// Declare a new instance member for our bookkeeping
private boolean Handler.cflowConnectorGetStart = false;
// If handler thread is started from Connector.getStart(..), set a mark
before(Handler handler) :
call(void Handler.start()) &&
cflow(execution(* Connector.getStart(..))) &&
target(handler)
{
System.out.println(thisJoinPoint + "\n doing bookkeeping");
handler.cflowConnectorGetStart = true;
}
// If current thread is a marked Handler, log it
before() :
execution(* Plain.getValue(..)) &&
if(Thread.currentThread() instanceof Handler) &&
if(((Handler) Thread.currentThread()).cflowConnectorGetStart)
{
System.out.println(thisJoinPoint + "\n triggered from parent thread via Connector.getStart(..)");
}
}
Console log with aspect:
As you can see, the Handler
thread started from App.main(..)
is ignored by the aspect as expected. The Handler
started from Connector.getStart(..)
triggers the aspect.
Plain getValue: foo
call(void de.scrum_master.app.Handler.start())
doing bookkeeping
execution(void de.scrum_master.app.Plain.getValue(String))
triggered from parent thread via Connector.getStart(..)
Plain getValue: testtest
来源:https://stackoverflow.com/questions/41649883/aspectj-cross-thread-pointcut