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

Is it possible to redirect a method call to another instance of the same object at runtime?

情况:我有由不同实例表示的同一对象的多个状态(使用深层复制制作)。现在我想确保,无论访问这些分组实例中的哪一个,执行修改的所有操作都被重定向到这些实例中最年轻的实例[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...
    }

现在这是一个相当明显的例子,可能是程序员错误的经典案例。他们修改了一些明显被宣传为过去状态的东西......但是说我们仍然想变得友善并尝试提供帮助,因此 拦截方法调用并在正确的实例上执行它即youngest/master实例。[3]

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

背景: 我正在尝试用不同的方法制作 library/engine,我写作是为了好玩,更不容易被最终用户滥用。因为无论如何我都会在内部需要这些状态(某些后台功能的及时快照),我想让最终用户也可以使用它们,这样他们就可以从我的状态保持中获益,例如用于分析算法。

[1]一个对象可以有多个互不相关的实例组;关系可能会通过一种方式保持 link 到最年轻的实例,它永远不会改变。

[2] 此代码仅作为示例,很明显最终用户在编写代码时多加注意可以避免此错误。

[3] 现在 防止 修改的一个简单方法是将对象包装成一个不可变版本试图修改它时抛出异常 > 但我们自己不编写此对象,如果我们不需要的话,也不想强迫最终用户编写他们自己的对象的两个版本...

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

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

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

其实如果是用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(字节码生成和修改工具)重新定义class字节码之前加载到:

    • 从 class 中删除词尾(如果有的话)
    • 添加字段以保存 MyObject 的 "group" 以解决您的 (1) 注释
    • 截取对 copy 的调用(我们需要另外复制 "group" 字段)和 modify (重定向调用)
    • 替换class加载器class中的代码

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,其中包含有关组的活动对象的信息和 "group" 字段相关功能。Interceptors 使用简单的 copy 重新定义,添加 "group" 设置和 modify,这使我们重新定位。

我认为它应该是精简代码,最昂贵的部分是对象创建后对组到对象分配的反射调用 setter(这部分可以改进;如果我们使用 ByteBuddy - 我们可以在字节码生成期间将反射替换为使用 getGroup()setGroup(String) 方法实现新接口并将它们委托给 FieldAccessor.ofField("group"),因此我们将通过接口获得有效的 invokevirtual) . modify() 应该具有接近相同的性能,因为它不使用反射,只使用完全生成的字节码。我没有做任何基准测试。

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

像这样:

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 的每个实例自然只记录自己的历史。