Hot questions for Using AspectJ in android

Question:

I have a lib that use aspects and is available via maven, now I'm trying to use that lib in an android application.

If I include this plug-in in the app gradle file, everything works fine, but my goal is to extract the classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.+' and the apply plugin: 'android-aspectj' (required by the plugin) to the my.lib gradle file instead of declaring in my app.

Is that possible?

app gradle file:

classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.+'

apply plugin: 'android-aspectj'

dependencies { 
  compile 'my.lib:example:1.0.0'
}

GOAL:

app gradle file:

dependencies { 
  compile 'my.lib:example:1.0.0'
}

my.lib gradle file:

classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.+'

apply plugin: 'android-aspectj'

dependencies { 
  compile 'org.aspectj:aspectjrt:1.7.3'
}

Answer:

I had the same problem. This is all I did to solve it.

Root/Main Project

In your root project add the AspectJ tools that contains the ajc compiler that is necessary for weaving your classes. ( You can also add this to your library's build.gradle file but it's better to add it here as the gradle plugin that you will be creating to accommodate your library will be using the ajc.

buildscript {
    repositories {
        jcenter()


    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'
        classpath 'org.aspectj:aspectjtools:1.8.5'
    }

Library Project

In your library's build.gradle file ensure that it looks like this similar to this. The main additions are the import statements at the top and the code beneath the android build properties.

import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

apply plugin: 'com.android.library'


dependencies {
    compile 'org.aspectj:aspectjrt:1.8.5'
}
android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

android.libraryVariants.all { variant ->
    LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = [
                "-showWeaveInfo",
                "-1.5",
                "-inpath", javaCompile.destinationDir.toString(),
                "-aspectpath", javaCompile.classpath.asPath,
                "-d", javaCompile.destinationDir.toString(),
                "-classpath", javaCompile.classpath.asPath,
                "-bootclasspath", android.bootClasspath.join(File.pathSeparator)
        ]

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler)

        def log = project.logger
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

So what's happening is when the project is being compiled the ajc(AspectJ's weaver) command compiles and weaves AspectJ and Java source and .class files, producing .class files compliant with any Java VM.

For this to take place the task needs arguments about your library. That's the reason for creating the args variable.

 String[] args = [
                    "-showWeaveInfo",
                    "-1.5",
                    "-inpath", javaCompile.destinationDir.toString(),
                    "-aspectpath", javaCompile.classpath.asPath,
                    "-d", javaCompile.destinationDir.toString(),
                    "-classpath", javaCompile.classpath.asPath,
                    "-bootclasspath", android.bootClasspath.join(File.pathSeparator)
            ]

Then the message handler that's created is simply being passed to ajc to accumulate messages of events that are taking place while ajc is compiling/weaving the classes. Then it's being passed to a project logger that then outputs any important errors or warnings that the ajc produced. For example if a pointcut can't be referenced by an advice it will be detected and shown in the gradle console.

So all of what was describe above is basically taking place right here. Where the args and message handler are being passed to the Main function of the ajc(AspectJ compiler).

 MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler)

        def log = project.logger
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown

Gradle Plugin

The pointcuts/advices of your library weren't being triggered because you were targeting the app module while Aspects were only being woven into your library's module with the com.uphyca.gradle:gradle-android-aspectj-plugin AspectJ plugin. So in order for the Aspects of your library to be woven into your App's module you have to create a gradle plugin for your project. So what you have defined as your goal is your question is not possible, this is the only way it can be done.

This is how the plugin should look. (Plugin is done in groovy).

Plugin's build.gradle

apply plugin: 'groovy'

targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7

dependencies {
  compile gradleApi()
  compile localGroovy()
  compile 'com.android.tools.build:gradle:1.1.0-rc3'
  compile 'org.aspectj:aspectjtools:1.8.5'
  compile 'org.aspectj:aspectjrt:1.8.5'
}

Then the actual class.

import com.android.build.gradle.AppPlugin
import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
import org.gradle.api.Plugin
import org.gradle.api.Project

public class YourPlugin implements Plugin<Project> {
    @Override void apply(Project project) {
        def hasApp = project.plugins.withType(AppPlugin)
        def hasLib = project.plugins.withType(LibraryPlugin)
        if (!hasApp && !hasLib) {
            throw new IllegalStateException("'android' or 'android-library' plugin required.")
        }

        final def log = project.logger
        final def variants
        if (hasApp) {
            variants = project.android.applicationVariants
        } else {
            variants = project.android.libraryVariants
        }

        project.dependencies {
            compile 'com.name:example:1.0'
            // TODO this should come transitively
            compile 'org.aspectj:aspectjrt:1.8.5'
        }

        variants.all { variant ->

            variant.dex.doFirst {
                String[] args = [
                        "-showWeaveInfo",
                        "-1.5",
                        "-inpath", javaCompile.destinationDir.toString(),
                        "-aspectpath", javaCompile.classpath.asPath,
                        "-d", javaCompile.destinationDir.toString(),
                        "-classpath", javaCompile.classpath.asPath,
                        "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)
                ]
                log.debug "ajc args: " + Arrays.toString(args)

                MessageHandler handler = new MessageHandler(true);
                new Main().run(args, handler);
                for (IMessage message : handler.getMessages(null, true)) {
                    switch (message.getKind()) {
                        case IMessage.ABORT:
                        case IMessage.ERROR:
                        case IMessage.FAIL:
                            log.error message.message, message.thrown
                            break;
                        case IMessage.WARNING:
                            log.warn message.message, message.thrown
                            break;
                        case IMessage.INFO:
                            log.info message.message, message.thrown
                            break;
                        case IMessage.DEBUG:
                            log.debug message.message, message.thrown
                            break;
                    }
                }
            }
        }
    }
}

I know this might seem like a lot but it's alot of copy and pasting because the solution remains the same. If you look closely at the class the same things being done in your library module is now being applied to your app's module. The main modification you would do to this is add your library module to the project's dependencies via the plugin that's done here.

 project.dependencies {
                compile 'com.letz:example-library:1.0'
                // TODO this should come transitively
                compile 'org.aspectj:aspectjrt:1.8.5'
            }

For your library to be available to your plugin while developing you have to ensure that it's being deployed to your local maven repository. This can be done by applying this plugin(https://github.com/dcendents/android-maven-gradle-plugin) to your library module and running the gradle install task.

Final Steps

Once all of that is done you can then apply it to a sample app for testing by adding this to it's build.gradle file

buildscript {
    repositories {
        mavenCentral()

        //Only necessary when developing locally.
        mavenLocal()
    }

    dependencies {             

        classpath 'com.letz:example-plugin:1.0'
    }
}
apply plugin: 'example-plugin'

Once that's done your library will be available to the app because it's being added to the project once the plugin has been applied.

If things are still confusing you are in good luck because the project I implemented this solution is on Github so you can fork it, copy the plugin's project and make the necessary changes.

The project is called Flender and it's used to annotate methods that require connectivity checking. Here's the link https://github.com/jd-alexander/flender

Hope this answer helps.

Question:

I am trying to inject some boilerplate code before a method is called, I have been using AOP for it.

I successfully made one Custom Annotation and now to manage the flow I need to make another and manage the flow. But the Second One throws a NoSuchMethodFound error. I need to annotate an Overrided Method. I tried to annotate a simple method too, but failed to do so. Here's the Aspect Class

@Aspect
public class AnnotationAspect {
private static final String TAG = "AnnotationAspect";

private static final String POINTCUT_METHOD =
        "execution(@org.android10.gintonic.annotation.MyAnnotation * *(..))";

@Pointcut(POINTCUT_METHOD)
public void methodToAnnotate() {

}
@Around("methodToAnnotate()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.d(TAG, "weaveJoinPoint: Inside the AnnotationAspect Class");
    joinPoint.proceed();
    return null;
}
}

Here's the Annotation Class

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface MyAnnotation {
}

And Here's how I am calling it. I also tried the same annotation by calling it on a overrided method like onResume.

@MyAnnotation
public void myMethod() {
    Toast.makeText(this, "Hello myMethod", Toast.LENGTH_SHORT).show();
}

Here's the link to full GITHUB repository Github

The Error block

java.lang.NoSuchMethodError: No static method aspectOf()Lorg/android10/gintonic/aspect/AnnotationAspect; in class Lorg/android10/gintonic/aspect/AnnotationAspect; or its super classes (declaration of 'org.android10.gintonic.aspect.AnnotationAspect' appears in /data/app/android10.org.viewgroupperformance-2/split_lib_slice_9_apk.apk)
             at org.android10.viewgroupperformance.activity.LinearLayoutTestActivity.myMethod(LinearLayoutTestActivity.java:51)
             at org.android10.viewgroupperformance.activity.LinearLayoutTestActivity.onCreate(LinearLayoutTestActivity.java:45)
             at android.app.Activity.performCreate(Activity.java:6582)
             at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1113)
             at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2532)
             at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2667)
             at android.app.ActivityThread.-wrap11(ActivityThread.java)
             at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1494)
             at android.os.Handler.dispatchMessage(Handler.java:111)
             at android.os.Looper.loop(Looper.java:207)
             at android.app.ActivityThread.main(ActivityThread.java:5776)
             at java.lang.reflect.Method.invoke(Native Method)
             at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
             at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)

Answer:

Sorry for writing this as an answer, but it is the only way to share code. It is just meant to help you on the way, not to answer the question in the first iteration.

So I have recreated your situation in pure Java + AspectJ outside of the Android world so as to see whether the problem is your code or something other, such as compilation, packaging, deployment, runtime classpath on the target device.

Marker annotation:

I used the package name given in your pointcut.

package org.android10.gintonic.annotation;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target({ METHOD, CONSTRUCTOR })
public @interface MyAnnotation {}

Driver application:

package de.scrum_master.app;

import org.android10.gintonic.annotation.MyAnnotation;

public class Application {
  @MyAnnotation
  public void myMethod() {
    System.out.println("Hi there!");
  }

  public static void main(String[] args) {
    new Application().myMethod();
  }
}

Aspect:

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AnnotationAspect {
  private static final String POINTCUT_METHOD =
    "execution(@org.android10.gintonic.annotation.MyAnnotation * *(..))";

  @Pointcut(POINTCUT_METHOD)
  public void methodToAnnotate() {}

  @Around("methodToAnnotate()")
  public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println(joinPoint);
    joinPoint.proceed();
    return null;
  }
}

Console log:

execution(void de.scrum_master.app.Application.myMethod())
Hi there!

So the intermediate result is that your annotation and aspect both look okay. The problem must be elsewhere. My first guess, as mentioned in my previous comments, is that aspectjrt.jar is missing on the classpath on the target device.


Update: Maybe you should use the Gradle AspectJ plugin mentioned in my old answer here which I just remembered. The problem you have and the error message seem to be the same.

Question:

I have android application with 2 modules. First module contains Activity class defined like so: MyActivity extends AppCompatActivity

Second module contains aspect class, where I want to create @Pointcut to MyActivity.onCreate method.

It works if defined like so: @Pointcut("execution(* *.onCreate(..))")

Just don't want ANY onCreate call, but MyActivity.onCreate or AppCompatActivity.onCreate.

Tried @Pointcut(execution(* MyActivity.onCreate(..))), but it doesn't work.

So, how can I reference class from another module with @Pointcut ?

How extended classes behave with aspects ? For example creating @Pointcut to AppCompatActivity also works at MyActivity, beacuse it is it's child ?

Thanks for any responses :)


Answer:

In your pointcut definition, whenever using a class, the compiler needs to know which class you're refering to unambigously. To do so, you should use the canonical name of your class.

For instance, if your activity is in package com.company.project, then your pointcut should be :

@Pointcut(execution(* com.company.project.MyActivity.onCreate(..)))

Question:

I just updated to using Android Studio 3.0 Canary 2. Upon opening my project Android Studio suggested I update the gradle version to 3.0.0-alpha2. My goal is to use the "Enable advanced profiling" Run Configuration so I can run a realtime memory-analysis. However the instant my gradle version was updated, my project fails to build. I followed the update instructions here.

The only changes made were to my top-level build.gradle file and the gradle-wrapper.properties file.

My top-level build.gradle:

buildscript {
    repositories {
        jcenter()
        mavenCentral()
        maven { url "https://jitpack.io" }
        maven { url 'https://maven.google.com' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0-alpha2'
        classpath 'com.github.Archinamon:GradleAspectJ-Android:2.3.0'
        classpath 'me.tatarka:gradle-retrolambda:3.5.0'
    }
}

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
        maven { url 'https://repo.adobe.com/nexus/content/repositories/releases/' }
        maven { url 'http://maven.localytics.com/public' }
    }
}

And I updated the gradle-wrapper.properties distributionURL to:

distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-milestone-1-all.zip

The error I get is:

Failed to apply plugin [id'com.archinamon.aspectJ']

And here is the offending part of my app-level build.gradle file:

import java.text.SimpleDateFormat

apply plugin: 'com.android.application'
apply plugin: 'com.archinamon.aspectj'
aspectj {
    includeAspectsFromJar 'Android_MTAgent'
}
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
....
}

So the problem seems to be with the aspectJ plugin. If I remove the plugin for aspectJ and the related aspectJ block (both shown above) then it compiles (I get a dimen error then though, but I already saw that mentioned elsewhere, so I guess that can be solved.)

I'd appreciate any pointers/ideas in regard to the above issue.


Answer:

Change your project build.gradle:

dependencies {
    classpath 'com.android.tools.build:gradle:3.0.0-alpha2'
}

to

dependencies {
    classpath 'com.android.tools.build:gradle:3.0.0-alpha3'
}

and update your Archinamon classpath reference in the same build.gradle file to:

classpath 'com.github.Archinamon:GradleAspectJ-Android:3.0.2'

Question:

I am new to AOP (using AspectJ / ajc) and have searched / googled the internet high and low searching for an answer to my puzzle. Hopefully, someone here might have it.

As I was given to understand by the documentation, AspectJ is suppose to inject code. From my experience, however, it seems like it is mostly adding code (and simply makes an exchange of method calls).

For example, if I have the method:

private static int foo() {
    System.out.println("Hello world");
    return 1;
}

And I define the following around advice for it (with a dummy random in order to manipulate proceed() vs. some other return value):

pointcut foo() : call(int com.mytest.aspects.HelloWorld.foo(..));
int around() : foo() {
    System.out.println("around()");
    if (System.currentTimeMillis() % 2 == 0)
        return proceed();
    return 0;
}

I get the following after decompiling using jd-gui:

  private static final int foo_aroundBody0()
  {
    return foo();
  }

  public static void main(String[] args)
  {
    foo_aroundBody1$advice(HelloAspect.aspectOf(), null);
  }

  private static final int foo_aroundBody1$advice(HelloAspect ajc$aspectInstance, AroundClosure ajc$aroundClosure)
  {
    System.out.println("around()");
    if (System.currentTimeMillis() % 2L == 0L)
    {
      AroundClosure localAroundClosure = ajc$aroundClosure;return foo_aroundBody0();
    }
    return 0;
  }

  private static int foo()
  {
     System.out.println("Hello world");
     return 1;
  }

If that right? Am I perhaps doing something wrong?

I tried using ajc with my android application, but thanks to some jars and SDKs, I got to the dreaded "too many methods" problem.

I am using call pointcuts for most of the time, however it seems that these extra methods are added for each call, even if done within the same class and method, thus increasing my code size and method count significantly.

Any help understanding if this is correct and how it works will be greatly appreciated!


Answer:

Your understanding is about correct. If you want to avoid too many methods being created, use execution() pointcuts instead of call() wherever possible, because then only one synthetic method per callee will be created, not per caller. I.e. if one method is called from 25 different places, only one additional method will be created instead of 25.

Furthermore you can avoid overhead by limiting the weaving scope of your aspects to the really needed joinpoints. Most aspects I see weave into way too many places. Also, if before() or after() is sufficient, avoid around().

Question:

I have been trying to intercept all the third party functions which are part of my application using aspectJ, but somehow am only able to intercept the functions declared by me and not the ones declared by third party libraries.

i am using this aspectJ gradle configuration referenced from this tutorial.

Here's what my aspect looks like :

private static final String POINTCUT_METHOD = "execution(* *(..))";

@Pointcut(POINTCUT_METHOD) 
public void methodAnnotatedWithDebugTrace() {}

@Around("methodAnnotatedWithDebugTrace()") 
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) 
            throws Throwable { 
    // ...
}

Is there any way by which we can start intercepting third party functions as well ??


Answer:

To paraphrase another answer that has been given multiple times :

You can only weave your own code

Basically Aspects with android only works at compile time and will usually weave your own code. If you're using existing code for which you don't have source (like the Android framework for example), the compiler won't have access to modify those. In your case, you can only catch when your code is accessing the third party library.

Meaning that if you want to intercept third party libraries you need to use "call(* *(..))" instead of "execution(* *(..))"

Question:

Hi I want to exclude the annotated method here is the code.

@Aspect
public class ExceptionHandlingAspect {
    private static final String TAG = ExceptionHandlingAspect.class.getName();

   @Pointcut("execution(* android.mobile.peakgames.net.aspectjandroid.AspectActivity.*(..)) " +
        "&& !@annotation(android.mobile.peakgames.net.aspectjandroid.exception.NoTryCatch)")
   public void exceptionEntryPoint() {
   }

    @AfterThrowing(pointcut = "exceptionEntryPoint()", throwing = "throwable")
    public void exceptionMethod(JoinPoint joinPoint, Throwable throwable) {
        Log.e(TAG, "Exception caught : " + throwable + " on method : " + joinPoint.getSignature());
        if (joinPoint.getTarget() instanceof Activity) {
            if (throwable instanceof AuthenticationException) {
                new AlertDialog.Builder((Context) joinPoint.getTarget())
                        .setTitle("Authentication Error")
                        .setMessage("You are not authenticated")
                        .show();
            } else {
                new AlertDialog.Builder((Context) joinPoint.getTarget())
                        .setTitle("Error")
                        .setMessage("Error occurred at : " + joinPoint.getSignature() + " " +
                                "Exception : " + throwable)
                        .show();
            }
        }
    }

    @Around(value = "exceptionEntryPoint()")
    public Object exceptionAroundMethod(ProceedingJoinPoint joinPoint) {
        try {
            return joinPoint.proceed();
        } catch (Throwable ignored) {
        }
        return null;
    }
}

Exclude any method which is annotated with NoTryCatch

Above code does exclude the method which annotated with NoTryCatch but when this method get called with exception it halts the execution of next methods. e.g

@NoTryCatch
void test(){throws NullPointor..}

now I call methods in sequence

test()
test1()

test1() does not run.

If I remove !@annotation(android.mobile.peakgames.net.aspectjandroid.exception.NoTryCatch) the test1() runs


Answer:

Of course test1() does not run if you ignore the exception thrown in test(), i.e. let it escalate. Due to that unhandled exception the next method is never called. I thought this is exactly what your aspect is designed to do. Why are you expecting different behaviour? And if you do expect something else, then please describe it in a comment and I can show you in an edit of my answer how to do it.


Update after OP's comment:

Well, you are having a home-made problem here: If method void caller() calls @NoTryCatch void callee(), of course the exception in callee() will not be handled, just as designed. Instead it escalates up to caller() which is not annotated and thus the aspect will handle it there. How can the caller know that the exception was ignored by an aspect in the callee? Or how can the aspect know, for that matter? The callee's control flow has already ended when returning control to the caller.

This concept of exception handling is tricky at the very least. I would even call it questionable because the inner-most element of a call chain determines that all the outer elements should ignore an exception. Usually exception handling works just the other way. The caller determines how to handle an exception thrown by the callee, not the callee itself. So I advise you to change your idea and concept of exception handling.

Having said that, I will show you that what I said really happens in your application with a little MCVE. Because I am not an Android developer and want this to run on any Java SE machine, I emulated the relevant parts of the Android API like this with mock-ups:

Android API mock-ups:

package android.content;

public class Context {}
package android.app;

import android.content.Context;

public class Activity extends Context {}

This one emulates an alert dialog by just logging to the console.

package android.app;

import android.content.Context;

public class AlertDialog {
  public AlertDialog() {}

  public static class Builder {
    private String title;
    private String message;

    public Builder(Context target) {}

    public Builder setTitle(String title) {
      this.title = title;
      return this;
    }

    public Builder setMessage(String message) {
      this.message = message;
      return this;
    }

    public void show() {
      System.out.println("ALERT DIALOG: " + title + " -> " + message);
    }
  }
}
package org.apache.http.auth;

public class AuthenticationException extends Exception {
  private static final long serialVersionUID = 1L;

  public AuthenticationException(String message) {
    super(message);
  }
}

Marker annotation:

package android.mobile.peakgames.net.aspectjandroid.exception;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;

@Retention(RUNTIME)
public @interface NoTryCatch {}

Driver application:

package android.mobile.peakgames.net.aspectjandroid;

import org.apache.http.auth.AuthenticationException;

import android.app.Activity;
import android.mobile.peakgames.net.aspectjandroid.exception.NoTryCatch;

public class AspectActivity extends Activity {
  public String doSomething() {
    System.out.println("Doing something");
    return "something";
  }

  @NoTryCatch
  public String doSomethingElse() {
    System.out.println("Doing something else");
    throw new RuntimeException("oops");
  }

  public String doSomethingFancy() throws AuthenticationException {
    System.out.println("Doing something fancy");
    throw new AuthenticationException("uh-oh");
  }

  public void run() throws AuthenticationException {
    doSomething();
    doSomethingElse();
    doSomethingFancy();
  }

  public static void main(String[] args) throws AuthenticationException {
    new AspectActivity().run();
  }
}

OP's aspect, slightly optimised:

Basically this is exactly your aspect with a few optimisations:

  • You split your error handling logic into two advices, one "around" and one "after throwing". This makes it a bit hard to follow the actual control flow because in one advice you log the error, only to later catch and ignore the same error in the other advice. Thus, I decided to pull the logging into the "catch" block of the "around" advice, making it clearer what happens.
  • Your original pointcut only targets methods in class AspectActivity. Thus, it is clear that the joinpoint's target is always an Activity and thus always a Context. Binding the target() to an advice parameter is clearer, more type-safe and gets you rid of ugly casts and instanceof.
  • I split your pointcut into two because we can re-use them both later in iteration 2, see below.
package de.scrum_master.aspect;

import org.apache.http.auth.AuthenticationException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.mobile.peakgames.net.aspectjandroid.AspectActivity;
import android.util.Log;

@Aspect
public class ExceptionHandlingAspect {
  private static final String TAG = ExceptionHandlingAspect.class.getName();

  @Pointcut("execution(* *(..)) && target(activity)")
  public void methodsOfInterest(AspectActivity activity) {}

  @Pointcut("@annotation(android.mobile.peakgames.net.aspectjandroid.exception.NoTryCatch)")
  public void annotationNoTryCatch() {}

  @Around("methodsOfInterest(activity) && !annotationNoTryCatch()")
  public Object exceptionAroundMethod(ProceedingJoinPoint thisJoinPoint, AspectActivity activity) {
    try {
      return thisJoinPoint.proceed();
    } catch (Throwable throwable) {
      String errorMessage = "Error " + throwable + " in method " + thisJoinPoint.getSignature();
      Log.e(TAG, errorMessage);
      Builder builder = new AlertDialog.Builder(activity);
      if (throwable instanceof AuthenticationException)
        builder.setTitle("Authentication Error").setMessage("You are not authenticated").show();
      else
        builder.setTitle("Error").setMessage(errorMessage).show();
      return null;
    }
  }
}

Console log:

Doing something
Doing something else
[de.scrum_master.aspect.ExceptionHandlingAspect] Error java.lang.RuntimeException: oops in method void android.mobile.peakgames.net.aspectjandroid.AspectActivity.run()
ALERT DIALOG: Error -> Error java.lang.RuntimeException: oops in method void android.mobile.peakgames.net.aspectjandroid.AspectActivity.run()

The log clearly shows

  • that the annotated method doSomethingElse() is executed and the error is not handled there,
  • but that the calling method run() triggers the advice instead, thus the error is handled there.
  • Even if you also annotate run(), the error would be handled in main(..).

So what do you need to do in order to avoid annotating the whole call chain? There is only one - quite ugly - way of doing this: manual bookkeeping, i.e. your aspect needs to remember exception instances it has ignored before because the corresponding error-handling advice has never run for that very exception.

Consequently you need to change your aspect like this (ignoring issues like multi-threading and nested exceptions created by manual try-catch etc. so as not to make it even more complicated):

Aspect, iteration 2:

package de.scrum_master.aspect;

import java.util.HashSet;
import java.util.Set;

import org.apache.http.auth.AuthenticationException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.mobile.peakgames.net.aspectjandroid.AspectActivity;
import android.util.Log;

@Aspect
public class ExceptionHandlingAspect {
  private static final String TAG = ExceptionHandlingAspect.class.getName();

  private Set<Throwable> ignoredErrors = new HashSet<>();

  @Pointcut("execution(* *(..)) && target(activity)")
  public void methodsOfInterest(AspectActivity activity) {}

  @Pointcut("@annotation(android.mobile.peakgames.net.aspectjandroid.exception.NoTryCatch)")
  public void annotationNoTryCatch() {}

  @Around("methodsOfInterest(activity) && !annotationNoTryCatch()")
  public Object exceptionAroundMethod(ProceedingJoinPoint thisJoinPoint, AspectActivity activity) throws Throwable {
    try {
      return thisJoinPoint.proceed();
    } catch (Throwable throwable) {
      if (ignoredErrors.contains(throwable))
        throw throwable;
      String errorMessage = "Error " + throwable + " in method " + thisJoinPoint.getSignature();
      Log.e(TAG, errorMessage);
      Builder builder = new AlertDialog.Builder(activity);
      if (throwable instanceof AuthenticationException)
        builder.setTitle("Authentication Error").setMessage("You are not authenticated").show();
      else
        builder.setTitle("Error").setMessage(errorMessage).show();
      return null;
    }
  }

  @AfterThrowing(value = "methodsOfInterest(activity) && annotationNoTryCatch()", throwing = "throwable")
  public void ignoreExceptions(JoinPoint thisJoinPoint, AspectActivity activity, Throwable throwable) {
    ignoredErrors.add(throwable);
  }
}

Console log, iteration 2:

Doing something
Doing something else
Exception in thread "main" java.lang.RuntimeException: oops
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.doSomethingElse(AspectActivity.java:17)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run_aroundBody4(AspectActivity.java:27)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run_aroundBody5$advice(AspectActivity.java:34)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run(AspectActivity.java:1)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.main(AspectActivity.java:32)

As you can see, the exception now escalates, "crashing" the application as you said you wanted it to.

P.S.: InheritableThreadLocal<Throwable> is your friend if you like the aspect to be thread-safe. Feel free to ask about it if you do need that but don't know what I am talking about.

P.P.S.: If you move the @NoTryCatch annotation from doSomethingElse() down to doSomethingFancy, the log changes as follows:

Doing something
Doing something else
[de.scrum_master.aspect.ExceptionHandlingAspect] Error java.lang.RuntimeException: oops in method String android.mobile.peakgames.net.aspectjandroid.AspectActivity.doSomethingElse()
ALERT DIALOG: Error -> Error java.lang.RuntimeException: oops in method String android.mobile.peakgames.net.aspectjandroid.AspectActivity.doSomethingElse()
Doing something fancy
Exception in thread "main" org.apache.http.auth.AuthenticationException: uh-oh
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.doSomethingFancy(AspectActivity.java:22)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run_aroundBody4(AspectActivity.java:28)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run_aroundBody5$advice(AspectActivity.java:34)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.run(AspectActivity.java:1)
    at android.mobile.peakgames.net.aspectjandroid.AspectActivity.main(AspectActivity.java:32)

Question:

More often than not gradle does not show in which class a certain error is.

For instance, I have a class A.java which uses my other class Utils.java. Therefore, A.java has the following import:

import my.package.Utils;

If I go to class A, delete the import above and clean the project, gradle gives me the following message:

Information:25/08/15 9:18 AM - Compilation completed with 1 error and 0 warnings in 22s 800ms
Error:Gradle: Execution failed for task ':app:compileBauDebugAspectj'.
> Utils cannot be resolved

The problem is that message does not say anywhere the problem is in class A. So the only way of me finding that out is by manually opening all classes that uses my Utils class until I find out the one in which the import is missing (in this case, A).

Is there a way to make gradle say exactly which class the error is? I can't believe it throws such generic errors without pinpointing where they are (especially in these basic cases like missing an import).


Answer:

If you are using Android Studio, use Build > Rebuild Project, not Build > Clean Project.

Using Rebuild Project, or command-line Gradle builds, will happily point out the specific location of errors. For example, here is the output of a missing import from the Gradle Console in Android Studio 1.3.1:

Executing tasks: [clean, :app:compileDebugSources, :app:compileDebugAndroidTestSources]

Configuration on demand is an incubating feature.
:app:clean
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:preReleaseBuild UP-TO-DATE
:app:prepareComAndroidSupportSupportV132300Library
:app:prepareComAndroidSupportSupportV42300Library
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig
:app:generateDebugAssets UP-TO-DATE
:app:mergeDebugAssets
:app:generateDebugResValues UP-TO-DATE
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources
:app:processDebugJavaRes UP-TO-DATE
:app:compileDebugJavaWithJavac
/home/mmurphy/stuff/CommonsWare/projects/andprojector/app/src/main/java/com/commonsware/andprojector/MainActivity.java:40: error: cannot find symbol
    EventBus.getDefault().registerSticky(this);
    ^
  symbol:   variable EventBus
  location: class MainActivity
/home/mmurphy/stuff/CommonsWare/projects/andprojector/app/src/main/java/com/commonsware/andprojector/MainActivity.java:45: error: cannot find symbol
    EventBus.getDefault().unregister(this);
    ^
  symbol:   variable EventBus
  location: class MainActivity
Note: /home/mmurphy/stuff/CommonsWare/projects/andprojector/app/src/main/java/com/commonsware/andprojector/MainActivity.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
2 errors

 FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugJavaWithJavac'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 1.971 secs

You get equivalent output from running gradle assembleDebug at the command line.