如何使用AOP为JPA实体提供建议/切入点设置工具? [英] How to advise/pointcut setters in JPA entities using AOP?

查看:150
本文介绍了如何使用AOP为JPA实体提供建议/切入点设置工具?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要记录对实体中字段的任何更改-无论是字符串更改,还是对集合/映射的添加/删除.

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.

鉴于JPA实体具有许多原始字段,编写一个切入点将截取字段上的任何set(..)方法都是相当简单的.

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.

但是,我遇到的问题是如何编写切入点以处理Collections/Sets/Embedded/etc.

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

给出以下实体:

@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

}

其中Contact是具有一堆原始字段的简单实体,Validity是具有一些原始字段的非实体对象.

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

以下切入点将拦截该类中的所有set()方法:

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() );
}

但是,对于嵌入式对象或集合,我该如何处理呢?对于嵌入式对象,如果我只是将建议放在对象中的setter方法周围,那么我怎么知道哪个父对象实际上已被修改/持久化?

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?

如果是collections/sets/maps/etc,我如何建议反对add/remove方法?我最终需要做的是建议getCollection().add()方法以及getCollection.remove()方法.但是我似乎找不到一个好方法.

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.

推荐答案

仅靠人工记账是无法直接完成的,因为集合或映射不会更改其标识,只能更改其内部状态(当您在其上调用方法时),即没有set()连接点要被拦截,只有方法调用.因此,您需要维护分配给您感兴趣的对象成员的集合/映射之间的映射,并跟踪它们的更改,这非常繁琐.这是一些示例代码,带有适用于Collection.add()Map.put()的概念证明.您必须将其扩展为所有更改内部状态的方法,例如remove()clear()等.基本上,它的工作方式如下:

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:

驱动程序类:

这是一个示例Person类,具有两个基本成员,两个集合和一个映射.它

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

  • 将默认值分配给所有Person成员,
  • 更改它们,
  • 不分配现有集合/地图Person成员,将其保存在局部变量中,
  • 再次更改collection/map对象(由于该对象当前未分配给Person成员,因此不应产生任何日志记录输出)
  • 将集合/地图对象重新分配给Person成员,
  • 再次更改它们(现在应该再次产生日志输出).
  • 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);
    }
}

直接/间接成员更改的记录方面:

这方面截取了

  • 直接成员更改(以Person对象为目标的set()切入点)
  • 呼叫Collection+.add()
  • 呼叫Map+.put().
  • direct member changes (set() pointcut targetting Person objects),
  • calls to Collection+.add(),
  • calls to Map+.put().

该方面的members属性中还保留了一个相当复杂的数据结构:一个Map<Object, Set<Entry<Person, String>>>使用集合/映射作为键,并使用PersonString(字段名称)元素对作为值.为什么会有如此复杂的数据结构?因为可以将同一集合/地图分配给多个Person成员,甚至可以分配给同一Person对象的多个成员,这取决于您使用的集合/地图的类型.因此,数据结构非常通用.随意扩展驱动程序类,使其可以与多个Person对象一起玩和/或在Person类中具有多个相同类型的成员.我还没有测试过,但是应该可以.

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:

  • 丑陋的getOldFieldValue()辅助方法是必需的,因为AspectJ不会在set()切入点中公开旧值,而仅公开新值.因此,需要通过反射来确定.
  • 因为JDK没有通用的对/元组类,并且我不想使用列表/向量来保持值对,所以我使用AbstractMap.SimpleEntry.此外,它的equals()方法可以确保将具有相等键和值的对视为相等,因此我可以创建一个新实例,并在我的Map.remove()调用中使用它-无需通过迭代来搜索现有值.以防万一您想知道.
  • 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
            );
        }
    }
}

控制台输出:

如果在编织方面时运行Person.main(),则输出应如下所示:

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}

如您所见,更改非成员对象状态"部分没有输出,与预期的一样.但是在更改成员对象状态"和再次更改成员对象状态"部分中的add()/put()调用记录为间接字段更改:Person [...".基本上,这就是您想要实现的目标,但是我个人认为,除了做一个很好的锻炼之外,它可能有点慢,而且是维护噩梦,但是可行.

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.

这篇关于如何使用AOP为JPA实体提供建议/切入点设置工具?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆