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 编码

MIT 许可证

89KB
1.5K SLoC

Rust 1.5K SLoC // 0.0% comments Java 217 SLoC // 0.1% comments

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",
    ],
}

FromJavaOption<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的实现,以便例如ArrayListHashMap可以读取为Rust中的等效VecHashMap类型。
  • 可能与Serde结合。我尚未研究serde数据模型的工作方式,但这似乎是一种访问Java数据的有用方式。
  • 减少所需的数据复制量。由于Java流包含对先前对象在流中的引用,因此读取的对象的实际数据无法传递给调用者。目前,在解析器实例上调用read会在构建下一个对象时克隆下一个对象引用的数据,并返回该数据。这可能导致相同的数据被克隆多次。最好是保留一个读取对象的内部池,并返回由该池的引用构建的对象。这意味着在返回的对象的引用仍然存在时,不能调用read,但如果需要,可以在客户端进行复制。

开发状态

目前仍处于开发阶段。我正在为另一个我正在开发的应用程序编写此代码,因此我预计在短期内功能和方法API会有很多变化,因为需求会变得明显。随着事情逐渐稳定,我希望它们将变得更加稳定。

贡献

由于该项目仍处于预alpha状态,我想象在一段时间内东西会相当不稳定。话虽如此,如果您发现任何明显的问题,或者有我认为遗漏的有用功能,请提出问题。我建议在问题讨论之前避免打开PR,因为当前的repo状态可能落后于开发。

依赖关系

~320–800KB
~19K SLoC