是否可以在运行时将方法调用重定向到同一对象的另一个实例?

时间:2015-11-21 11:10:24

标签: java methods call instance

情况:我有不同实例所代表的同一对象的多个状态(使用深拷贝制作)。现在我想确保无论访问哪些分组实例,执行修改的所有操作都会重定向到最年轻的 [1]

实施例 [2]

//Let's create an object
MyObject mObj = new MyObject(...);
//Let's create a list of past states
List<MyObject> pastStates = new ArrayList<MyObject>();

//doing some operations on mObj ....
mObj.modify(...);

//done modifying mObj, now let's save it's state and then create a copy to begin again
pastStates.add(mObj.copy());

//more of this...
mObj.modify(...);
pastStates.add(mObj.copy());

//let's compare some old states for whatever reason (e.g. part of an algorithm)
compare(MyObject o1, MyObject o2) {
    if(o1.getA() == o2.getA()) {
        o2.modify(...); //wait, we modified an old state...
    }

现在这是一个相当明显的例子,可能是程序员错误的经典案例。他们修改了一些明显被宣传为过去状态的东西......但是说我们仍然想要很好并尝试帮助,从而拦截方法调用并在正确的实例上执行即最年轻/主要实例。 [3]

问题: 有没有办法用标准java做到这一点? 奖金: 有没有一种方式对性能没有可怕的影响?

背景:我正在尝试使用不同的方法来制作库/引擎,我写的很有趣,更难以被最终用户滥用。因为我将在内部需要这些状态(对于某些背景功能及时快照),我想让它们也可供最终用户使用,这样他们就可以从我的状态管理中获利,例如:用于分析算法。

[1] 对象的多个实例组可能彼此不相关;可能会通过与最年轻的实例的单向链接保持关系,这种关联根本不会改变。

[2] 这段代码就是一个例子,很明显,在编写代码时,最终用户可以更加注意这个错误。

[3] 现在,阻止修改的一种简单方法是将对象包装成一个不可变的版本,在尝试修改它时抛出异常&gt;但是我们自己不写这个对象,如果我们不需要那么强迫它在最终用户上写下自己对象的两个版本......

3 个答案:

答案 0 :(得分:1)

可以使用 around advice 通过AOP完成方法拦截。 AspectJ是解决此类问题的好工具。对性能的影响也应该没问题。

around advice 中,在大多数情况下,您调用 proceed 来执行目标对象上的目标方法,但您也可以阻止方法执行,而是执行方法打电话给另一个对象。

答案 1 :(得分:1)

我可能会创建两个类:&#34; inner&#34;一个是不可变的,一个&#34;外部&#34;一个维护内部列表的人。 (注意:我不是指JLS意义上的内部类,只是一个完全由其包装器控制的对象。)

这样的事情:

public final class Outer {
    private final List<Inner> history = new ArrayList<>(); //history is inverted for brevity, 0 is the latest one

    public Outer(int x) {
       this.history.add(new Inner(x));
    } 
    public void add(int x) {
      history.add( 0, new Inner(history.get(0).x+x);
    }

    public Inner current() {
       return history.get(0);
    }

    public static final class Inner {
       private final int x;

       private Inner(int x) {
          this.x = x;
       }

       public int getX() {
         return x;
       }
    }
}

通过此设置,客户端只能实例化Outer,只能改变Outer,但可以访问所有过去状态的只读副本。没有办法意外修改过去的状态。不需要单独的分组逻辑,因为Outer的每个实例自然只记录自己的历史记录。

答案 2 :(得分:0)

是的,可以使用字节码修改。

实际上,如果它是由AspectJ或其他库完成的,它将使用代理或字节码修改来实现。但我不确定Aspect编程库API是否可以执行此特定任务。

您可以在this repo找到适合您任务的工作示例。

来自存储库的此测试工作正常:

    //Let's create an object
    MyObject mObj = new MyObject();
    MyObjectActiveRepository.INSTANCE.putToGroup(mObj, "group1");
    MyObjectActiveRepository.INSTANCE.registerActiveForItsGroup(mObj);
    //Let's create a list of past states
    List<MyObject> pastStates = new ArrayList<MyObject>();

    //doing some operations on mObj ....
    mObj.modify("state1");

    //done modifying mObj, now let's save it's state and then create a copy to begin again
    pastStates.add(mObj.copy());

    //more of this...
    mObj.modify("state2");
    pastStates.add(mObj.copy());

    mObj.modify("state3");

    assertEquals("state1", pastStates.get(0).getState());
    assertEquals("state2", pastStates.get(1).getState());
    assertEquals("state3", mObj.getState());

    pastStates.get(0).modify("stateNew");
    assertEquals("state1", pastStates.get(0).getState());
    assertEquals("state2", pastStates.get(1).getState());
    assertEquals("stateNew", mObj.getState());

很快 -

  1. 我使用ByteBuddy(字节码生成和修改工具)在加载之前重新定义类字节码:

    • 从课程中删除最终成绩(如果有)
    • 添加字段以保存MyObject的“组”以解决您的(1)注释
    • 拦截对copy的调用(我们需要另外复制“group”字段)和modify(重新调用目标)
    • 替换classloader中的类代码
  2. 
    TypePool typePool = TypePool.Default.ofClassPath();
    new ByteBuddy()
        .rebase(typePool.describe("MyObject").resolve(), ClassFileLocator.ForClassLoader.ofClassPath())
        .modifiers(TypeManifestation.PLAIN) //our class can be final and we have no access to it - so remove final
        .defineField("group", String.class, Visibility.PUBLIC)
        .method(named("modify")).intercept(MethodDelegation.to(typePool.describe("Interceptors").resolve()))
        .method(named("copy")).intercept(MethodDelegation.to(typePool.describe("Interceptors").resolve()))
        .make()
        .load(InterceptorsInitializer.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);
    
    1. 已实施MyObjectActiveRepository,其中包含有关群组的活动对象和“群组”字段相关功能的信息。Interceptors具有简单copy重新定义,可添加“群组”设置和modify ,这使我们重新定位。
    2. 我认为它应该是精简代码,最昂贵的部分是在创建对象后对组到对象分配的setter的反射调用(这部分可以改进;如果我们使用ByteBuddy - 我们可以用实现新接口替换反射在生成字节代码期间使用getGroup()setGroup(String)方法将它们委派给FieldAccessor.ofField(“group”),因此我们将使用精细有效的invokevirtual thru接口)。 modify()应该具有接近相同的性能,因为它不使用反射,只有完全生成的字节码。我没有进行任何基准测试。