情况:我有不同实例所代表的同一对象的多个状态(使用深拷贝制作)。现在我想确保无论访问哪些分组实例,执行修改的所有操作都会重定向到最年轻的 [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;但是我们自己不写这个对象,如果我们不需要那么强迫它在最终用户上写下自己对象的两个版本......
答案 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());
很快 -
我使用ByteBuddy(字节码生成和修改工具)在加载之前重新定义类字节码:
copy
的调用(我们需要另外复制“group”字段)和modify
(重新调用目标)
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);
MyObjectActiveRepository
,其中包含有关群组的活动对象和“群组”字段相关功能的信息。Interceptors
具有简单copy
重新定义,可添加“群组”设置和modify
,这使我们重新定位。我认为它应该是精简代码,最昂贵的部分是在创建对象后对组到对象分配的setter的反射调用(这部分可以改进;如果我们使用ByteBuddy - 我们可以用实现新接口替换反射在生成字节代码期间使用getGroup()
和setGroup(String)
方法将它们委派给FieldAccessor.ofField(“group”),因此我们将使用精细有效的invokevirtual thru接口)。 modify()
应该具有接近相同的性能,因为它不使用反射,只有完全生成的字节码。我没有进行任何基准测试。