我正在开发一个概念验证对象(反)序列化框架,理想情况下能够序列化任何对象并收集有关类本身的信息。我开始使用反射来实现它,以:
- 访问类型层次结构(超类、接口等)
- 查找该对象上的所有字段,并获取该字段中的所有值
序列化是"容易"的部分,可以递归地将此规则应用于对象,直到我找到null或基元类型。现在,我陷入了困境:反序列化。
从一个简单的对象"Hello World"字符串开始,我有这样的序列化:
<object type="java.lang.String">
<primitive name="count" type="int" value="11 />
<primitive name="hash" type="int" value="0" />
<primitive name="offset" type="int" value="0" />
<array name="value" basetype="char">
<value>H</value>
<value>e</value>
<value>l</value>
...
<value>r</value>
<value>l</value>
<value>d</value>
</array>
</object>
反序列化是可以的,因为String类有一个默认的构造函数,我可以通过Reflection调用它,并且我可以设置所有字段。现在,让我们假设我对一个对象进行了以下序列化:
<object class="some-class-with-no-default-constructor">
<object name="some-attrib-name" class="attrib-1-class">
<primitive name="size" type="int" value="5" />
...
</object>
如果我没有默认构造函数,并且所有其他接收参数的构造函数都不能接受"null"值作为输入,从而引发某种异常,从而无法通过反射实例化类,会发生什么?
问题是:"有没有一种方法可以实例化某个类的‘空对象’,以便在实例化后手动设置其字段,而不调用其构造函数?"。当然,我也愿意讨论其他策略。
谢谢。
编辑
一旦它是一个概念验证环境,因此我不考虑安全限制,我就找到了一种方法,通过Unsafe类实例化任何对象,而无需调用其构造函数。
public final class A {
private final Object o;
private A(final Object o) { if (o == null) throw new Error(); this.o = o; }
public static A a() { return new A(new Object()); }
public Object getO() { return o; }
}
上面显示的这个类是在下面的一个答案中提出的,它可以被实例化,并且它的最终值设置正确(当然,前提是安全限制不适用),使用以下代码:
private static Unsafe getUnsafe() throws Exception {
Field vDeclaredField = Unsafe.class.getDeclaredFields()[0];
vDeclaredField.setAccessible(true);
Unsafe vUnsafe = (Unsafe) vDeclaredField.get(null);
vDeclaredField.setAccessible(false);
return vUnsafe;
}
public static void main(String[] args) throws Exception {
A objectA = (A) getUnsafe().allocateInstance(A.class);
Field fieldO = A.class.getDeclaredField("o");
boolean oldAccessibilityValue = fieldO.isAccessible();
fieldO.setAccessible(true);
Object objectOParameter = Arrays.asList(1,2,3,4); //could be any object
fieldO.set(objectA, objectOParameter);
fieldO.setAccessible(oldAccessibilityValue); //I personally prefer setting it to old value
assert(objectOParameter.equals(objectA.getO()));
}
那么呢?你们能看到任何其他与SecurityManager本身无关的问题吗?
这是不可靠的。
假设您有以下类:
public final class A {
private final Object o;
private A(final Object o) { if (o == null) throw new Error(); this.o = o; }
public static A a() { return new A(new Object()); }
public Object getO() { return o; }
}
首先,您提到了一个关于非默认构造函数的问题,该构造函数接受一个参数,并在给定null
时抛出异常。
其次,构造函数的参数可以(在这种情况下)定义最终实例字段的值,在对象构建后,您无法可靠地控制它(可能是因为最终字段的内存模型语义,如果对象已经发布到其他线程,可能会导致可见性问题,也可能是因为SecurityManager
不允许您修改最终字段)。
最后,构造函数是private(或受保护的,或受包保护的,不管怎样)。如果安装了安全管理器,它可能会完全阻止您在构造函数上尝试setAccessible(true)
,这样您就可以强制调用它。
因此,我要么按照您的建议简单地放弃项目,要么对您的框架可以(反)序列化的对象的特性进行一些限制。
最后,序列化不仅仅是保存和恢复字段的过程。这是在设计一个类的过程中必须仔细计划和实施的事情类必须设计为可序列化。
回复编辑
将您提供的代码称为";纯Java";,因为它使用非标准API";sun.mic.Unsafe";,它存在于Sun的实现中,但不能保证存在于所有实现中。因此,代码依赖于实现。
在您编写的测试代码中,假设有关于类的知识,即使用getDeclaredField("o")。不管怎样,我认为这个问题很容易解决。
然而,我看到了两个问题。
不应序列化系统资源
首先,假设我有一个这样的类:
class StockQuoteProvider {
private QuoteCache cache;
private Thread quoteCacheUpdater;
public StockQuoteProvider() {
this.quoteCacheUpdater = ... // sets up a Thread that will use sockets to connect to Yahoo's stock quote provider and update the cache periodically
this.quoteCacheUpdater.start();
}
public Quote getQuote(final String symbol) { return ... }
}
如何可能序列化Thread
?序列化对象的语义是什么?如果线程处于IO操作的中间,比如从套接字读取,会怎么样?您将如何序列化套接字连接?!这毫无意义。这门课很普通。
即使类完全是线程安全的,也不应在没有同步的情况下共享反序列化实例
让我们忘记语义,回到语言规范,发现您的方法的另一个问题。(编辑:更改了类,使要点更加突出)。考虑下面的类,它表示一个可变的整数范围:
// Represents a range of integers, {a, a+1, ..., b}, in which a < b.
class Range {
private final Object lock = new Object();
private int a;
private int b;
Range(final int a, final int b) { setAB(a, b); }
final int[] getAB() { synchronized(lock) { return new int[]{a, b}; } }
final void setAB(final int a, final int b) {
if (!(a < b)) { throw new IllegalArgumentException("Invalid range"); }
synchronized(lock) { this.a = a; this.b = b; }
}
@Override public String toString() {
int[] ab = getAB();
int a = ab[0];
int b = ab[1];
return a + " < " + b;
}
}
一个非常简单,看起来很天真的班级,对吧?请注意,数组int[]被用作getter的返回类型,因为如果我们使用两个getter,a
和b
的值可能会在对getter的两次调用之间发生变化。
所以,这个类是完全线程安全的。在";正常的";在这种情况下,它不可能处于这样的状态:;a>=b";。
通过使用OP提出的反序列化技术,这种保证消失了。假设OP给了我两种方法;对象串行化(Object o)";,和一个";对象反序列化(Object o)";,其使用所提出的算法。以下伪代码将证明它不起作用:
public class Test {
public static Range r = null;
public static void Main(final String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override void run() { r = deserialize(serialize(new Range(1, 3)); }
});
final Thread t2 = new Thread(new Runnable() {
@Override void run() { System.out.println(r); }
});
t1.start();
t2.start();
}
它将打印什么?首先,如果T2没有看到对字段r
的写入,它可以打印null。为了让事情变得更有趣(并看看这会变得多么微妙),让我们假设T2实际上看到了对字段r
的写入。由于反序列化过程不提供同步,JVM可以随意重新排序对新反序列化的Range
实例中字段的写入。因此,it可以打印";0<0">如果T2没有看到对a
和b
的写入;1<0"(如果它只看到对a的写入)或"strong>";0<3〃或";1<3〃。根据Java语言规范,您不可能预测结果(您唯一的保证是结果必须是这5种可能性中的一种)。
所以,重点是:你不可能让这对每个类都可靠地工作。我总是可以隐藏锁获取,并且你将无法跟踪它(如果没有一些严肃的、核心的(不可能的?)字节码分析),所以类的反序列化版本不会被每个线程平等地看到。。。你能看到可能出现的巨大问题吗?
综上所述
这样的框架是不可能存在的。安全管理器(setAccessible(true)
的使用)、代码的可移植性(sun.misc.Unsafe
的使用)和多线程(class Range
)、毫无意义的、不可用的反序列化实例(class StockQuoteProvider
)都会出现问题。这只是我能想到的前4个问题,如果没有对正在序列化的对象的绝对假设,纯Java代码就无法解决这些问题。
因此,结论是必须限制框架能够序列化的对象。换句话说,对象必须设计为可序列化的对象。
祝你好运。
是否有方法实例化要设置的某个类的"空对象"在实例化而不调用其建设者?
不,没有。这正是为什么基于反射的框架或库通常要求与它们一起工作的类遵守JavaBeans规范的原因,该规范需要默认的构造函数。
克服这一问题的一种方法是,对于没有默认构造函数的类,需要某种类型的元数据(注释、xml),这将告诉您用什么值调用什么构造函数。
使用反射没有可靠的方法,尽管您可以根据声明的构造函数的参数类型进行有根据的猜测。
你可以查看Obgenesis。
此外,您还可以考虑在运行时操作字节码。