meme查找基序 查找序列_python

最长公共子序列

假设您正在开发一个将对象自动保存到数据库中的框架。 您需要检测两次保存之间所做的更改,以便仅保存修改的字段。 如何检测脏场。 最简单的方法是遍历原始数据和当前数据,并分别比较每个字段。 代码如下:

public static void getDirtyFields(Object obj, Object obj2, Class cls, Map<String, DiffFields> diff)
        throws Exception {
        Field[] flds = cls.getDeclaredFields();
        for (int i = 0; i < flds.length; i++) {
            flds[i].setAccessible(true);
            Object fobj = flds[i].get(obj);
            Object fobj2 = flds[i].get(obj2);
            if (fobj.equals(fobj2)) continue;

            if (checkPrimitive(flds[i].getType())) {
               <!-- add to dirty fields -->
                continue;
            }

            Map<String, DiffFields> fdiffs = new HashMap<String, DiffFields>();
            getDirtyFields(fobj, fobj2, fobj.getClass(), fdiffs);
            <!-- add to dirty fields -->
        }

        if (cls.getSuperclass() != null)
            getDirtyFields(obj, obj2, cls.getSuperclass(), diff);
    }

上面的代码不能处理很多条件,例如null值,字段是集合,映射或数组等。但是,这给出了可以做什么的想法。 如果对象很小并且其中不包含太多层次结构,则效果很好。 当在巨大的层次结构对象中的变化很小时,我们必须一直遍历最后一个对象才能知道差异。 而且,使用equals可能不是检测脏字段的正确方法。 可能尚未实现等于,或者仅可以仅比较几个字段,所以没有进行真正的脏字段检测。 您必须遍历每个字段,而不论是否相等,直到您击中图元来检测脏字段为止。

在这里,我想谈谈检测脏场的另一种方法。 除了使用反射,我们还可以使用序列化来检测脏字段。 我们可以轻松地替换上面代码中的“等于”来序列化对象,并且仅当字节不同时才继续操作。 但这不是最佳选择,因为我们将多次序列化同一对象。 我们需要如下逻辑:

  • 序列化要比较的两个对象
  • 比较两个字节流时,检测要比较的字段
  • 如果字节值不同,则将该字段存储为不同
  • 收集所有不同的字段并返回

因此,一次遍历两个字节流可以生成不同的字段列表。 我们如何实现这种逻辑? 我们可以遍历序列化流并能够识别其中的字段吗? 我们要编写如下代码:

public static void main(String[] args) throws Exception {
        ComplexTestObject obj = new ComplexTestObject();
        ComplexTestObject obj2 = new ComplexTestObject();
        obj2._simple._string = "changed";

        //serialize the first object and get the bytes
        ByteArrayOutputStream ostr = new ByteArrayOutputStream();
        CustomOutputStream str = new CustomOutputStream(ostr);
        str.writeObject(obj);
        str.close();
        byte[] bytes = ostr.toByteArray();

        //serialize the second object and get the bytes
        ostr = new ByteArrayOutputStream();
        str = new CustomOutputStream(ostr);
        str.writeObject(obj2);
        str.close();
        byte[] bytes1 = ostr.toByteArray();       

       //read and compare the bytes and get back a list of differing fields
        ReadSerializedStream check = new ReadSerializedStream(bytes, bytes1);
        Map diff = check.compare();
        System.out.println("Got difference: " + diff);
    }

地图应包含_simple._string,以便我们可以直接转到_string并对其进行处理。

解释序列化格式

有些文章解释了标准序列化字节流的外观。 但是,我们将使用自定义格式。 尽管我们可以读取标准的序列化格式,但是当类的结构已经被我们的类定义时,它就没有必要了。 我们将简化它,并更改序列化的格式以仅写入字段的类型。 字段的类型是必需的,因为类声明可以引用接口,超类等,而所包含的值可以是派生类型。

为了自定义序列化,我们创建了自己的ObjectOutputStream并覆盖了writeClassDescriptor函数。 现在,我们的ObjectOutputStream如下所示:

public class CustomOutputStream extends ObjectOutputStream {
    public CustomOutputStream(OutputStream str)
        throws IOException  {
        super(str);
    }
    @Override
    protected void writeClassDescriptor(ObjectStreamClass desc)
        throws IOException  {
        <b>String name = desc.forClass().getName();
        writeObject(name);</b>
        String ldr = "system";
        ClassLoader l = desc.forClass().getClassLoader();
        if (l != null)  ldr = l.toString();
        if (ldr == null)  ldr = "system";
        writeObject(ldr);
    }
}

让我们编写一个简单的对象进行序列化,并查看字节流的外观:

public class SimpleTestObject implements java.io.Serializable {
    int _integer;
    String _string;
    public SimpleTestObject(int b)  {
        _integer = 10;
        _string = "TestData" + b;
    }
    public static void main(String[] args) throws Exception  {
        SimpleTestObject obj = new SimpleTestObject(0);
        FileOutputStream ostr = new FileOutputStream("simple.txt");
        CustomOutputStream str = new CustomOutputStream(ostr);
        str.writeObject(obj);
        str.close(); ostr.close();
    }
}

运行此类后,调用“ hexdump -C simple.txt”显示以下输出:

00000000  ac ed 00 05 73 72 74 00  10 53 69 6d 70 6c 65 54  |....srt..SimpleT|
00000010  65 73 74 4f 62 6a 65 63   74 74 00 27 73 75 6e 2e  |estObjectt.'sun.|
00000020  6d 69 73 63 2e 4c 61 75  6e 63 68 65 72 24 41 70  |misc.Launcher$Ap|
00000030  70 43 6c 61 73 73 4c 6f   61 64 65 72 40 33 35 63  |pClassLoader@35c|
00000040  65 33 36 78 70 00 00 00  0a 74 00 09 54 65 73 74  |e36xp....t..Test|
00000050  44 61 74 61 30                                                          |Data0|
00000055

按照本文中的格式,我们可以将字节跟踪为:

  • AC ED:STREAM_MAGIC。 指定这是一个序列化协议。
  • 00 05:STREAM_VERSION。 序列化版本。
  • 0×73:TC_OBJECT。 指定这是一个新对象。

现在我们需要阅读类描述符。

  • 0×72:TC_CLASSDESC。 指定这是一个新类。

类描述符是我们编写的,因此我们知道格式。 它已读取两个字符串。

  • 0×74:TC_STRING。 指定对象的类型。
  • 0×00 0×10:字符串的长度,后跟对象类型的16个字符,即SimpleTestObject
  • 0×74:TC_STRING。 指定类加载器
  • 0×00 0×27:字符串的长度,后跟类加载器名称
  • 0×78:TC_ENDBLOCKDATA,对象的可选块数据的结尾。
  • 0×70:TC_NULL,在结束块之后,表示没有超类的事实

此后,将写入类中不同字段的值。 我们的类_integer和_string中有两个字段。 因此我们有4个字节的_integer值,即0×00、0×00、0×00、0x0A,后跟一个格式为字符串的字符串

  • 0×74:TC_STRING
  • 0×00 0×09:字符串的长度
  • 9个字节的字符串数据

比较流并检测脏区

现在我们了解并简化了序列化格式,我们可以开始为流编写解析器并对其进行比较。 首先,我们为原始字段编写标准的读取函数。 例如,如下所示编写getInt以读取整数(示例代码中存在其他整数):

static int getInt(byte[] b, int off) {
        return ((b[off + 3] & 0xFF) << 0) +  ((b[off + 2] & 0xFF) << 8) +
               ((b[off + 1] & 0xFF) << 16) + ((b[off + 0]) << 24);
    }

可以使用以下代码读取类描述符。

byte desc = _reading[_readIndex++]; //read TC_CLASSDESC
        byte cdesc = _compareTo[_compareIndex++];
        switch (desc) {
        case TC_CLASSDESC: {
                byte what = _reading[_readIndex++];  byte cwhat = _compareTo[_compareIndex++]; //read the type written TC_STRING
                if (what == TC_STRING) {
                    String[] clsname = readString(); //read the field Type 
                    if (_reading[_readIndex] == TC_STRING) {
                        what = _reading[_readIndex++];  cwhat = _compareTo[_compareIndex++];
                        String[] ldrname = readString(); //read the classloader name
                    }
                    ret.add(clsname[0]);
                    cret.add(clsname[1]);
                }
                byte end = _reading[_readIndex++]; byte cend = _compareTo[_compareIndex++]; //read 0x78 TC_ENDBLOCKDATA
                //we read again so that if there are super classes, their descriptors are also read
                //if we hit a TC_NULL, then the descriptor is read
                readOneClassDesc(); 
            }
            break;
        case TC_NULL:
            //ignore all subsequent nulls 
            while (_reading[_readIndex] == TC_NULL) desc = _reading[_readIndex++];
            while (_compareTo[_compareIndex] == TC_NULL) cdesc = _compareTo[_compareIndex++];
            break;
        }

在这里,我们读取第一个字节,如果它是TC_CLASSDESC,则读取两个字符串。 然后,我们继续阅读,直到达到TC_NULL。 还有其他条件要处理,例如TC_REFERENCE,它是对先前声明的值的引用。 可以在示例代码中找到。

注意:函数同时读取两个字节流(_reading和_compareTo)。 因此,他们两个总是指向下一个必须开始比较的点。 字节作为一个块读取,这确保即使存在值差异,我们也始终会从正确的位置开始。 例如,字符串块的长度指示直到读取的位置,类描述符的末尾指示直到读取的位置,依此类推。

我们尚未编写字段序列。 我们如何知道要阅读哪些字段? 为此,我们可以执行以下操作:

Class cls = Class.forName(clsname, false, this.getClass().getClassLoader());
        ObjectStreamClass ostr = ObjectStreamClass.lookup(cls);
        ObjectStreamField[] flds = ostr.getFields();

这为我们提供了序列化顺序的字段。 如果我们遍历flds,则将按照写入数据的顺序进行。 因此,我们可以如下进行迭代:

Map diffs = new HashMap();
for (int i = 0; i < flds.length; i++) {
    DiffFields dfld = new DiffFields(flds[i].getName());
    if (flds[i].isPrimitive()) { //read primitives
	Object[] read = readPrimitive(flds[i]);
	if (!read[0].equals(read[1])) diffs.put(flds[i].getName(), dfld); //Value is not the same so add as different
    }
    else if (flds[i].getType().equals(String.class)) { //read strings
	byte nxtread = _reading[_readIndex++]; byte nxtcompare = _compareTo[_compareIndex++];
	String[] rstr = readString();
	if (!rstr[0].equals(rstr[1])) diffs.put(flds[i].getName(), dfld); //String not same so add as difference
    }
}

在这里,我仅说明了如何检查类中的原始字段是否存在差异。 但是,可以通过递归调用对象字段类型的相同函数,将逻辑扩展到子类。

可以在此处找到该博客要尝试的示例代码,该代码具有比较子类和超类的逻辑。 在这里可以找到更整洁的实现。

请注意。 此方法存在一些缺点:

  • 此方法只能使用可序列化的对象和字段。 暂态和静态字段之间没有差异。
  • 如果writeObject覆盖默认的序列化,则ObjectStreamClass不能正确反映序列化的字段。 为此,我们将不得不对这些类的读取进行硬编码。 例如,在示例代码中,存在对ArrayList的读取或使用并解析标准序列化格式。

参考: 在我们的JCG合作伙伴Raji Sankar的Reflections博客上, 使用序列化查找对象中的脏区。

翻译自: https://www.javacodegeeks.com/2013/11/using-serialization-to-find-dirty-fields-in-an-object.html

最长公共子序列