5个版本 (3个重大更改)
0.4.0 | 2023年1月29日 |
---|---|
0.3.0 | 2021年5月3日 |
0.2.0 | 2021年3月15日 |
0.1.1 | 2021年2月20日 |
0.1.0 | 2021年2月12日 |
#690 in 编码
89KB
1.5K SLoC
Jaded - Java Deserialization for Rust
Java内置了一个广受诟病(理由充分)的序列化系统。输出是一个二进制流,映射了完整的对象层次结构以及它们之间的关系。
该流还包括类的定义及其层次结构(超类等)。完整的规范定义在此处。
在任何一个新的应用程序中,可能都有更好的方法来序列化数据,从而降低安全风险,但在某些情况下,遗留应用程序正在输出一些内容,我们希望再次读取它。如果我们想要在单独的应用程序中读取它,那么我们就不应该被绑定到Java。
示例
在Java中
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Demo implements Serializable {
private static final long serialVersionUID = 1L;
private String message;
private int i;
public Demo(String message, int count) {
this.message = message;
this.i = count;
}
public static void main(String[] args) throws Exception {
Demo d = new Demo("helloWorld", 42);
try (FileOutputStream fos = new FileOutputStream("demo.obj", false);
ObjectOutputStream oos = new ObjectOutputStream(fos);) {
oos.writeObject(d);
}
}
}
在Rust中
use std::fs::File;
use jaded::{Parser, Result};
fn main() -> Result<()> {
let sample = File::open("demo.obj").expect("File missing");
let mut parser = Parser::new(sample)?;
println!("Read Object: {:#?}", parser.read()?);
Ok(())
}
Rust输出
Read Object: Object(
Object(
ObjectData {
class: "Demo",
fields: {
"i": Primitive(
Int(
42,
),
),
"message": JavaString(
"helloWorld",
),
},
annotations: [],
},
),
)
Rust类型转换
对于大多数使用场景,原始对象表示法并不便于使用。为了便于使用,类型可以实现FromJava
,然后可以直接从流中读取。
在大多数情况下,此实现可以通过启用derive
功能自动推导。
#[derive(Debug, FromJava)]
struct Demo {
message: String,
i: i32,
}
演示对象可以直接由解析器读取
fn main() -> Result<()> {
let sample = File::open("demo.obj").expect("File missing");
let mut parser = Parser::new(sample)?;
let demo: Demo = parser.read_as()?;
println!("Read Object: {:#?}", demo);
Ok(())
}
Rust输出
Read Object: Demo {
message: "helloWorld",
i: 42,
}
具有自定义writeObject方法的对象
通常,类,包括标准库中的许多类,使用writeObject
方法自定义其写入方式,该方法是内置的字段序列化方法的补充。这些数据被写入嵌入的流字节和/或对象。这些数据不能与字段相关联,除非有原始的Java源代码,因此包括在ObjectData
结构的annotations
字段中(在上面的示例中为空)。
由于此流通常包含类的重要数据,因此提供了一个机制,可以通过类似于在Java类本身中使用ObjectInputStream的接口从中读取有用的数据。
Java中自定义序列化的一个示例是ArrayList
。其writeObject
方法的源代码可以在这里看到,但关键是它先写入元素数量,然后逐个写入每个元素。
由于嵌入的自定义流可以包含任何内容,我们必须手动实现从中读取的方法,但这些方法可以由FromJava
的派生实现使用。
在Java中
import java.util.List;
import java.util.ArrayList;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Demo {
public static void main(String[] args) throws Exception {
List<String> keys = new ArrayList<>();
keys.add("one");
keys.add("two");
keys.add("three");
try (FileOutputStream fos = new FileOutputStream("demo.obj", false);
ObjectOutputStream oos = new ObjectOutputStream(fos);) {
oos.writeObject(keys);
}
}
}
在 Rust 中
use std::fs::File;
use jaded::{Parser, Result, FromJava, FromJava, AnnotationIter, ConversionResult};
#[derive(Debug, FromJava)]
struct ArrayList<T> {
// Size is written as a 'normal' field
size: i32,
// values are written to the custom stream so need attributes
#[jaded(extract(read_values))]
values: Vec<T>,
}
// extraction method must be callable as
// function(&mut AnnotationIter) -> ConversionResult<S> where S: Into<T>
// Where T is the type of the field being assigned to.
fn read_values<T>(annotations: &mut AnnotationIter) -> ConversionResult<Vec<T>>
where
T: FromJava
{
(0..annotations.read_i32()?)
.into_iter()
.map(|_| annotations.read_object_as())
.collect()
}
fn main() -> Result<()> {
let sample = File::open("demo.obj").expect("File missing");
let mut parser = Parser::new(sample)?;
let array: ArrayList<String> = parser.read_as()?;
println!("{:#?}", array);
Ok(())
}
这会得到预期的数组列表
ArrayList {
size: 3,
values: [
"one",
"two",
"three",
],
}
FromJava
为Option<T>
和Box<T>
实现了,以便可以反序列化递归结构,并处理序列化类中的空字段。注意,如果字段为空,则转换将失败,除非该字段被指定为Option<T>
。如果序列化的列表中有空字符串,上面的示例将失败。将values
更改为Vec<Option<T>>
将允许它仍然被读取。
重命名字段
在 Java 习惯中,字段名使用camelCase
,而 Rust 字段名使用snake_case
。默认情况下,derive 宏会在 Rust 中查找与映射字段名称相同的字段,因此为了防止 Rust 结构需要使用 camelCase,字段可以赋予属性以使用 Java 类中的不同字段。
#[derive(FromJava)]
struct Demo {
#[jaded(field = "fooBar")]
foo_bar: String,
}
如果所有字段都需要重命名,可以将 'rename' 属性赋予结构。这将在从 Java 读取之前将所有字段名称转换为 camelCase。如果需要,仍可以覆盖个别字段。
#[derive(FromJava)]
#[jaded(rename)]
struct Demo {
foo_bar: String,
}
多态
在 Java 中,一个字段可以声明为接口,具体的实现直到运行时才知道。Jaded
可以通过使用内置的 derive 宏与枚举一起使用,在某种程度上反序列化这些字段。
枚举的每个变体都可以分配一个具体实现,正在读取的对象的完全限定类名(FQCN)将确定返回哪个变体。
例如,要读取声明为列表的字段,您可能定义一个枚举如下
#[derive(FromJava)]
enum List<T> {
#[jaded(class = "java.util.ArrayList")] // without generics
ArrayList(
#[extract(read_list)] // See above for read method
Vec<T>
),
#[jaded(class = "java.util.Collections$EmptyList")]
Empty,
#[jaded(class = "java.util.Arrays$ArrayList")]
// result of using Arrays.asList in Java
Array {
a: Vec<T>, // Array is written to field called a
},
}
结合from
字段属性和From<List>
实现,这允许将 Java 中声明为List
的字段读取为 Rust 中的Vec<T>
。
虽然这有助于支持多态,但它仍然要求预先知道所有潜在实现。对于许多用例,这应该是足够的。
功能
derive
允许自动推导 FromJava
限制
Java 多态
在 Java 中,一个字段可以声明为接口,具体的实现可以是任何东西。这意味着在 Rust 中,除非我们知道流将要使用特定的实现,否则我们无法可靠地将读取的对象转换为结构。虽然将反序列化到枚举将涵盖大多数常见情况,但无法阻止某些客户端代码创建具有完全不同序列化表示的CustomList
并在 Rust 中读取的类中使用它。
模糊的序列化
很遗憾,没有原始代码创建的串行字节流,我们所能做的事情也有局限性。上面链接的协议列出了四种对象类型。其中之一是实现了java.lang.Externalizable
并使用PROTOCOL_VERSION_1(自v1.2以来不再是默认版本)的类,除了编写它们的类之外,其他任何东西都无法读取,因为它们的数据不过是一系列字节。
在剩下的三种类型中,我们只能可靠地反序列化两种。
-
实现
java.lang.Serializable
而没有写Object方法的“普通”类这些可以按上述方式读取
-
实现Externalizable并使用更新的PROTOCOL_VERSION_2的类
这些可以读取,尽管它们的数据完全由ObjectData结构体的注解字段持有,而get_field方法仅返回None。
-
实现writeObject的Serializable类
这些对象更难处理。上述规范建议,它们的字段首先被写入为“普通”类,然后可选地随后写入注解。实际上并非如此,字段仅在类在writeObject方法中将defaultWriteObject作为第一个调用时才写入。这在规范中作为一项要求被提及,因此我们可以假设对于标准库中的类这是正确的,但这是在反序列化用户类时需要注意的事项。
这导致的结果是,一旦我们发现了一个无法读取的类,就很难回到正轨,因为这需要在大量自定义数据中找到表示下一个对象开始的标记。
未来计划
- 为常见的Java和Rust类型添加
FromJava
的实现,以便例如ArrayList
和HashMap
可以读取为Rust中的等效Vec
和HashMap
类型。 - 可能与Serde结合。我尚未研究serde数据模型的工作方式,但这似乎是一种访问Java数据的有用方式。
- 减少所需的数据复制量。由于Java流包含对先前对象在流中的引用,因此读取的对象的实际数据无法传递给调用者。目前,在解析器实例上调用
read
会在构建下一个对象时克隆下一个对象引用的数据,并返回该数据。这可能导致相同的数据被克隆多次。最好是保留一个读取对象的内部池,并返回由该池的引用构建的对象。这意味着在返回的对象的引用仍然存在时,不能调用read,但如果需要,可以在客户端进行复制。
开发状态
目前仍处于开发阶段。我正在为另一个我正在开发的应用程序编写此代码,因此我预计在短期内功能和方法API会有很多变化,因为需求会变得明显。随着事情逐渐稳定,我希望它们将变得更加稳定。
贡献
由于该项目仍处于预alpha状态,我想象在一段时间内东西会相当不稳定。话虽如此,如果您发现任何明显的问题,或者有我认为遗漏的有用功能,请提出问题。我建议在问题讨论之前避免打开PR,因为当前的repo状态可能落后于开发。
依赖关系
~320–800KB
~19K SLoC