0

How to write an aspect or annotation for a method that cannot update any instance variables?

Say for example I have the following java class

public class Foo {

    String name;
    int id;

    public getName() {
       return name;
    }
}

Now say I want to write an aspect or in general, some annotation call it say @readOnly which can enforce getName() method to not modify either name or id so if do

public class Foo {

    String name;
    int id;

    @readOnly
    public getName() {
       name = "hello world";
       id = 7564;
       return name;
    }
}

Any invocation of getName() like above should result in an error since it modifies both name and id

The class like below should be just fine since it just doing read of instance variables.

public class Foo {

    String name;
    int id;

    @readOnly
    public getName() {
       return name + "," + id;
    }
}
user1870400
  • 6,028
  • 13
  • 54
  • 115

1 Answers1

1

How about directly throwing a compile error when there is any member write access in any get*() method?

Marker annotation:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

@Retention(RUNTIME)
@Target({ TYPE, FIELD, METHOD })
public @interface ReadOnly {}

Sample class / driver application:

package de.scrum_master.app;

public class Application {
  private int id = 1;
  private String name = "default";

  @ReadOnly
  public int getId() {
    return id;
  }

  @ReadOnly
  public String getName() {
    name = "hello world";
    id = 7564;
    return name;
  }

  public String getNameWithoutReadOnly() {
    name = "hello world";
    id = 7564;
    return name;
  }

  @ReadOnly
  public String getNameIndirectly() {
    modifyMembers();
    return name;
  }

  private void modifyMembers() {
    name = "hello world";
    id = 7564;
  }

  public static void main(String[] args) {
    Application application = new Application();
    application.getId();
    try { application.getName(); }
    catch (Exception e) { e.printStackTrace(System.out); }
    application.getNameWithoutReadOnly();
    try { application.getNameIndirectly(); }
    catch (Exception e) { e.printStackTrace(System.out); }
  }
}

Aspect declaring compile error:

The following aspect only detects @ReadOnly annotations on methods, not on classes or members. You can extend it if you also need that.

The declare error statement directly throws compile errors when compiling your application with the AspectJ compiler. In Eclipse you would see something like this:

AspectJ declared compile errors

If you also want to detect indirect write access from helper methods called by a getter, you also need the dynamic pointcut with cflow(), but that one only works at runtime, not at compile time, because it inspects the callstack. If you do not need it, just remove it.

package de.scrum_master.aspect;

import de.scrum_master.app.ReadOnly;

public aspect ReadOnlyGetterAspect {
  declare error :
    set(* *) && withincode(public * get*()) && @withincode(ReadOnly) :
      "Setting members from within a getter is forbidden";

  before() : set(* *) && cflow(execution(@ReadOnly public * get*())) {
    throw new IllegalAccessError("Setting members from within a getter is forbidden");
  }
}

BTW, if you want to see the runtime pointcut/advice in action, you need to make the code compile first. So you either need to weaken declare error into declare warning or comment out the two statements causing the compile errors in getName().

If you do the former, your log output will be:

java.lang.IllegalAccessError: Setting members from within a getter is forbidden
    at de.scrum_master.aspect.ReadOnlyGetterAspect.ajc$before$de_scrum_master_aspect_ReadOnlyGetterAspect$1$3e55e852(ReadOnlyGetterAspect.aj:11)
    at de.scrum_master.app.Application.getName(Application.java:14)
    at de.scrum_master.app.Application.main(Application.java:39)
java.lang.IllegalAccessError: Setting members from within a getter is forbidden
    at de.scrum_master.aspect.ReadOnlyGetterAspect.ajc$before$de_scrum_master_aspect_ReadOnlyGetterAspect$1$3e55e852(ReadOnlyGetterAspect.aj:11)
    at de.scrum_master.app.Application.modifyMembers(Application.java:32)
    at de.scrum_master.app.Application.getNameIndirectly(Application.java:27)
    at de.scrum_master.app.Application.main(Application.java:42)

If you do the latter (fix the code), of course you will only see the second exception.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I wish I could give you +100 for this. Thanks a ton!!! Yes compile time errors are even better! – user1870400 Jan 07 '19 at 03:41
  • Also Do you mind uploading this code somewhere? so I can see what it takes to set it up using gradle? Again I am new to this framework. – user1870400 Jan 07 '19 at 05:28
  • If you feel the urge to reward me, feel free to put a bounty on the question. ;-) As for uploading the code, it is complete already, it even includes package names. The only thing missing would be a Gradle file. I am a Maven user, though. This sample project was set up as a pure Eclipse project, but I could show you how to use Maven to build it. With Gradle you would have to ask someone else, though, I am sorry. BTW, does that mean you were asking an AspectJ question without ever having built any AspectJ project by yourself? Maybe you can just google it. – kriegaex Jan 08 '19 at 00:29