How to advise/pointcut setters in JPA entities using AOP?

家住魔仙堡 提交于 2019-12-11 18:47:34

问题


I have the need to log any changes to fields in an entity - whether it is a String change, or addition/deletion to a collection/map.

Given a JPA entity with a bunch of primitive fields, it is fairly trivial to write an pointcut that will intercept any set(..) methods on the fields.

However, where I am stuck is how to write the pointcut to handle Collections/Sets/Embedded/etc.

Given the following entity:

@Entity
public class Provider implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;


    private String name;

    @Column(name="type", nullable=false)
    @Enumerated(EnumType.STRING)
    private ProviderType providerType;


    @ManyToMany
    private List<Contact> contacts;

    @Embedded
    private Validity validity;

   // setters and getters omitted for brevity

}

where Contact is a simple entity with a bunch of primitive fields and Validity is a non-entity object with some primitive fields.

The following pointcut will intercept all set() methods in the class:

pointcut fieldSetter() : set(!static !final !transient * *.Provider) && args(val) && target(o);

to which I can write a before/after/around advice.

before( Object val, Object o) : fieldSetter{
  String fieldName = thisJoinPointStaticPart.getSignature().getName();
  System.out.println( "Object being changed: " + o.toString() );
  System.out.println( "New Value for: " + fieldname + " is: " + v.toString() );
}

But how do I handle that case for an Embedded object or a Collection? For an Embedded object, if I just put my advice around the setter method in the object, how do I know which is the parent object that is actually being modified/persisted?

And in the case of collections/sets/maps/etc, how do I advise against the add/remove methods? What I need to end up doing is advising the getCollection().add() methods as well as the getCollection.remove() methods. But I can't seem to figure out a good way.


回答1:


This cannot be done directly, only with manual bookkeeping, because a collection or map does not change its identity, only its internal state when you call methods upon it, i.e. there is no set() joinpoint to be intercepted, only method calls. Thus, you need to maintain a mapping between collections/maps assigned to object members of your interest and track their changes, which is quite tedious. Here is some sample code with a proof of concept working for Collection.add() and Map.put(). You would have to extend it for all methods changing internal state, e.g. remove(), clear() etc. Basically it works like this:

Driver class:

This is a sample Person class with two primitive members, two collections and one map. It

  • assigns default values to all Person members,
  • changes them,
  • unassigns existing collection/map Person members, saving them in local variables,
  • changes the collection/map objects again (which should not yield any logging output because the objects are not currently assigned to Person members),
  • reassigns the collection/map objects to Person members,
  • changes them again (which now should yield log output again).
package de.scrum_master.app;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Person {
    int id;
    String name;
    List<Object> objects = new ArrayList<>();
    Set<Integer> numbers = new HashSet<>();
    Map<String, Object> properties = new HashMap<>();

    public Person(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person[id=" + id + ", name=" + name + "]";
    }

    public static void main(String[] args) {
        System.out.println("Creating Person object");
        Person person = new Person(2, "Werner Heisenberg");

        System.out.println("\nChanging member object states");
        person.id = 1;
        person.name = "Albert Einstein";
        person.objects.add("foo");
        person.objects.add(11);
        person.objects.add(new Object());
        person.numbers.add(11);
        person.numbers.add(22);
        person.numbers.add(33);
        person.properties.put("year of birth", 1879);
        person.properties.put("year of death", 1955);
        person.properties.put("well known for", new String[] { "Photoelectric Effect", "Special Relativity", "General Relativity" });

        System.out.println("\nUnassigning member objects");
        List<Object> objects = person.objects;
        person.objects = null;
        Set<Integer> numbers = person.numbers;
        person.numbers = null;
        Map<String, Object> properties = person.properties;
        person.properties = null;

        System.out.println("\nChanging non-member object states");
        objects.add("bar");
        objects.add(22);
        objects.add(new Object());
        numbers.add(44);
        numbers.add(55);
        numbers.add(66);
        properties.put("Nobel Prize year", 1921);

        System.out.println("\nReassigning member objects");
        person.objects = objects;
        person.numbers = numbers;
        person.properties = properties;

        System.out.println("\nChanging member object states again");
        person.objects.add("zot");
        person.objects.add(33);
        person.objects.add(new Object());
        person.numbers.add(77);
        person.numbers.add(88);
        person.numbers.add(99);
        person.properties.put("Time Person of the Century year", 1999);
    }
}

Logging aspect for direct/indirect member changes:

This aspect intercepts

  • direct member changes (set() pointcut targetting Person objects),
  • calls to Collection+.add(),
  • calls to Map+.put().

The aspect also keeps a rather complicated data structure in its members property: a Map<Object, Set<Entry<Person, String>>> using collections/maps as keys and pairs of Person and String (field name) elements as values. Why such a complicated data structure? Because the same collection/map could be assigned to multiple Person members or even to multiple members of the same Person object, depending on the types of collections/maps you use. So the data structure is pretty generic. Feel free to extend the driver class to play with multiple Person objects and/or having multiple members of the same type in the Person class. I have not tested that, but it should work.

Update:

  • The ugly getOldFieldValue() helper method is necessary because AspectJ does not expose the old value in a set() pointcut, only the new value. Thus it needs to be determined via reflection.
  • Because the JDK does not have a generic pair/tuple class and I do not want to use a list/vector for keeping a value pair, I use AbstractMap.SimpleEntry. Furthermore, its equals() method is guaranteed to regard pairs with equal keys and values as equal, thus I can just create a new instance and use it in my Map.remove() call - no need to search for existing values via iteration. Just in case you wondered.
package de.scrum_master.aspect;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;

import org.aspectj.lang.Signature;
import org.aspectj.lang.SoftException;

import de.scrum_master.app.Person;

public aspect MemberChangeLogger {
    private Map<Object, Set<Entry<Person, String>>> members =
        Collections.synchronizedMap(
            new IdentityHashMap<Object, Set<Entry<Person, String>>>()
        );

    private Object getOldFieldValue(Signature signature, Person person) {
        Field field;
        try {
            field = signature.getDeclaringType().getDeclaredField(signature.getName());
        }
        catch (Exception e) { throw new SoftException(e); }
        field.setAccessible(true);
        try {
            return field.get(person); 
        }
        catch (Exception e) { throw new SoftException(e); }
    }

    pointcut directMemberChange(Person person, Object newValue) :
        set(* Person.*) &&
        args(newValue) &&
        target(person);

    pointcut collectionChange(Collection collection, Object newElement) :
        !cflow(adviceexecution()) &&
        call(* Collection+.add(*)) &&
        args(newElement) &&
        target(collection);

    pointcut mapChange(Map map, Object key, Object value) :
        !cflow(adviceexecution()) &&
        call(* Map+.put(*, *)) &&
        args(key, value) &&
        target(map);

    before(Person person, Object newValue) : directMemberChange(person, newValue) {
        String fieldName = thisJoinPointStaticPart.getSignature().getName();
        System.out.println(
            "Direct field change: " +
            person + " -> " + fieldName + " = " + newValue
        );
        Object oldValue = getOldFieldValue(thisJoinPoint.getSignature(), person);
        if (!(
            newValue instanceof Collection || newValue instanceof Map ||
            oldValue instanceof Collection || oldValue instanceof Map
        ))
            return;
        if (oldValue != null && members.get(oldValue) != null) {
            members.get(oldValue).remove(new SimpleEntry<Person, String>(person, fieldName));
            if (members.get(oldValue).size() == 0)
                members.remove(oldValue);
        }
        if (newValue == null)
            return;
        if (members.get(newValue) == null)
            members.put(newValue, new HashSet<Map.Entry<Person, String>>());
        members.get(newValue).add(new SimpleEntry<Person, String>(person, fieldName));
    }

    before(Collection collection, Object newElement) : collectionChange(collection, newElement) {
        if (members.get(collection) == null)
            return;
        for (Entry<Person, String> entry : members.get(collection)) {
            System.out.println(
                "Indirect field change: " +
                entry.getKey() + " -> " + entry.getValue() +
                " -> adding element " + newElement + " to " + collection
            );
        }
    }

    before(Map map, Object key, Object value) : mapChange(map, key, value) {
        if (members.get(map) == null)
            return;
        for (Entry<Person, String> entry : members.get(map)) {
            System.out.println(
                "Indirect field change: " +
                entry.getKey() + " -> " + entry.getValue() +
                " -> putting entry (" + key + "=" + value + ") into " + map
            );
        }
    }
}

Console output:

If you run Person.main() with the aspect woven in, the output should be as follows:

Creating Person object
Direct field change: Person[id=0, name=null] -> objects = []
Direct field change: Person[id=0, name=null] -> numbers = []
Direct field change: Person[id=0, name=null] -> properties = {}
Direct field change: Person[id=0, name=null] -> id = 2
Direct field change: Person[id=2, name=null] -> name = Werner Heisenberg

Changing member object states
Direct field change: Person[id=2, name=Werner Heisenberg] -> id = 1
Direct field change: Person[id=1, name=Werner Heisenberg] -> name = Albert Einstein
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element foo to []
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element 11 to [foo]
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element java.lang.Object@69d30fe7 to [foo, 11]
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 11 to []
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 22 to [11]
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 33 to [22, 11]
Indirect field change: Person[id=1, name=Albert Einstein] -> properties -> putting entry (year of birth=1879) into {}
Indirect field change: Person[id=1, name=Albert Einstein] -> properties -> putting entry (year of death=1955) into {year of birth=1879}
Indirect field change: Person[id=1, name=Albert Einstein] -> properties -> putting entry (well known for=[Ljava.lang.String;@1fb93cf8) into {year of birth=1879, year of death=1955}

Unassigning member objects
Direct field change: Person[id=1, name=Albert Einstein] -> objects = null
Direct field change: Person[id=1, name=Albert Einstein] -> numbers = null
Direct field change: Person[id=1, name=Albert Einstein] -> properties = null

Changing non-member object states

Reassigning member objects
Direct field change: Person[id=1, name=Albert Einstein] -> objects = [foo, 11, java.lang.Object@69d30fe7, bar, 22, java.lang.Object@3a51ce0d]
Direct field change: Person[id=1, name=Albert Einstein] -> numbers = [33, 55, 66, 22, 11, 44]
Direct field change: Person[id=1, name=Albert Einstein] -> properties = {year of birth=1879, Nobel Prize year=1921, year of death=1955, well known for=[Ljava.lang.String;@1fb93cf8}

Changing member object states again
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element zot to [foo, 11, java.lang.Object@69d30fe7, bar, 22, java.lang.Object@3a51ce0d]
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element 33 to [foo, 11, java.lang.Object@69d30fe7, bar, 22, java.lang.Object@3a51ce0d, zot]
Indirect field change: Person[id=1, name=Albert Einstein] -> objects -> adding element java.lang.Object@50aed564 to [foo, 11, java.lang.Object@69d30fe7, bar, 22, java.lang.Object@3a51ce0d, zot, 33]
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 77 to [33, 55, 66, 22, 11, 44]
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 88 to [33, 55, 66, 22, 77, 11, 44]
Indirect field change: Person[id=1, name=Albert Einstein] -> numbers -> adding element 99 to [33, 55, 66, 22, 77, 11, 88, 44]
Indirect field change: Person[id=1, name=Albert Einstein] -> properties -> putting entry (Time Person of the Century year=1999) into {year of birth=1879, Nobel Prize year=1921, year of death=1955, well known for=[Ljava.lang.String;@1fb93cf8}

As you can see, there is no output in section "Changing non-member object states", just as expected. But add()/put() calls in sections "Changing member object states" and "Changing member object states again" are logged as "Indirect field change: Person[...". Basically this is what you wanted to achieve, but personally I think that, apart from being a nice exercise, it is probably a bit slow and a maintenance nightmare, but doable.



来源:https://stackoverflow.com/questions/24786391/how-to-advise-pointcut-setters-in-jpa-entities-using-aop

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!