枚举在hessian序列化和反序列化中的问题

转自:http://yangbolin.cn/2016/05/22/enum-probolems-in-hessian/

一.概述

最近在拆分一个枚举类,但是该枚举类使用在一个RPC接口上,枚举类使用在RPC接口上,必然要考虑序列化和反序列化的问题,需要确保自己对枚举的拆分不会导致序列化和反序列化的问题。

原来的代码为

public enum xxxEnum {
  X("a11","a22"),
  Y("a111","a222"); 
  private String a1;
  private String a2;
  public String getA1(){ return a1;}
  public String getA2(){ return a2;}
 }

修改后的代码为

public enum xxxEnum {
  X("b11","b22"),
  Y("b111","b222"); 
  private String b1;
  private String b2;
  public String getB1(){ return b1;}
  public String getB2(){ return b2;}
 }

变更了两个成员变量的名字。这样修改反序列化会不会有问题?

二.问题分析

要想知道序列化和反序列化会不会有问题,得先看看序列化和反序列化的源码,关于hessian的序列化和反序列化有很内容,这里就拿枚举这一个点来分析。

先看看枚举序列化的代码

public class EnumSerializer extends AbstractSerializer {
    private Method _name;

    public EnumSerializer(Class cl) {
        // hessian/32b[12], hessian/3ab[23]
        if (!cl.isEnum() && cl.getSuperclass().isEnum())
            cl = cl.getSuperclass();

        try {
      // 通过反射来获取枚举类的name方法
            _name = cl.getMethod("name", new Class[0]);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {
        if (out.addRef(obj))
            return;

        Class cl = obj.getClass();

        if (!cl.isEnum() && cl.getSuperclass().isEnum())
            cl = cl.getSuperclass();

        String name = null;
        try {
      //调用枚举类的name方法来生成枚举序列化的值
            name = (String) _name.invoke(obj, (Object[]) null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        out.writeMapBegin(cl.getName());
        out.writeString("name");
        out.writeString(name);
        out.writeMapEnd();
    }
}

关于枚举的序列化可以总结为调用枚举类的name方法来生成序列化的字符串。


再看看枚举反序列化的代码

public class EnumDeserializer extends AbstractDeserializer {
  private Class _enumType;
  private Method _valueOf;
  
  public EnumDeserializer(Class cl)
  {
    // hessian/33b[34], hessian/3bb[78]
    if (cl.isEnum())
      _enumType = cl;
    else if (cl.getSuperclass().isEnum())
      _enumType = cl.getSuperclass();
    else
      throw new RuntimeException("Class " + cl.getName() + " is not an enum");

    try {
      // 反射获取枚举类的valueOf方法
      _valueOf = _enumType.getMethod("valueOf",
           new Class[] { Class.class, String.class });
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  public Class getType()
  {
    return _enumType;
  }
  
  public Object readMap(AbstractHessianInput in)
    throws IOException
  {
    String name = null;
    
    while (! in.isEnd()) {
      String key = in.readString();

      if (key.equals("name"))
        name = in.readString();
      else
  in.readObject();
    }

    in.readMapEnd();

    Object obj = create(name);
    
    in.addRef(obj);

    return obj;
  }
  
  @Override
  public Object readObject(AbstractHessianInput in, Object []fields)
    throws IOException
  {
    String []fieldNames = (String []) fields;
    String name = null;

    for (int i = 0; i < fieldNames.length; i++) {
      if ("name".equals(fieldNames[i]))
        name = in.readString();
      else
  in.readObject();
    }

    Object obj = create(name);

    in.addRef(obj);

    return obj;
  }

  private Object create(String name)
    throws IOException
  {
    if (name == null)
      throw new IOException(_enumType.getName() + " expects name.");

    try {
      //反射调用枚举类的valueOf方法
      return _valueOf.invoke(null, _enumType, name);
    } catch (Exception e) {
      throw new IOExceptionWrapper(e);
    }
  }
}

枚举的反序列化可以总结为反射调用枚举的valueOf方法来获取最终的的枚举值。

有了上面对枚举序列化反序列化源码的分析,现在我们看看相关的问题。(假定服务端做的序列化,客户端做的是反序列化,方便描述)

1.服务端枚举多了一个枚举值
假如服务端的枚举类为

public enum A {
  X,
  Y,
  Z;
}

客户端的枚举类为

public enum A {
  X,
  Y;
}

如果服务端返回一个A.Z给客户端,此时hessian反序列化调用枚举类的valueOf方法来获取反序列化,但是客户端的枚举类中没有Z,那么客户端反序列化直接跑异常。

2.服务端枚举ordinal值以及枚举类成员变量值和客户端不一致
假设服务端的枚举类为

public enum A {
  X("aaa"),
  Y("bbbb");//此时Y的ordinal为1,对应的value为bbb
  String value;
  A(String value) {this.value=value}
}

客户端的枚举类为

public enum A {
  X("aaa"),
  Z("ccc"),
  Y("ddd");// 此时Y的ordinal为2对应的value为ddd
  String value;
  A(String value) {this.value=value;}
}

假如入服务端传递给客户端的是A.Y,此时客户端拿到的A.Y对应的ordinal为2,对应的value为ddd。
上面这个点非常重要。

3.枚举是单例的

public enum TestEnum {
    
    XX("xx");
    
    TestEnum(String value) {
        this.value = value;
    }
    
    String value;

    
    public String getValue() {
        return value;
    }

    
    public void setValue(String value) {
        this.value = value;
    }
}
public class Test {
    
    public static void main(String[] rgs) {
        TestEnum testEnum1 = TestEnum.XX;
        TestEnum testEnum2 = TestEnum.XX;
        
        testEnum1.setValue("XX");
        testEnum2.setValue("YY");
        System.out.println(testEnum1.value); // 输出 YY
        System.out.println(testEnum2.value); // 输出 YY
    }
}

testEnum1和testEnum2其实指向了同一个枚举引用。每次修改的都是同一个对象,所以前一个set的值被后面的set给覆盖了。

三.总结

  • 还是不要在RPC的接口中直接使用枚举类了,直接使用String就行
  • 在枚举类中使用字符串时直接使用name()就行,不要再做过度封装,尽量保持枚举类的简洁
  • 枚举类使用在RPC接口上的时候就一定要小心,重构的时候要注意保持ordinal
  • 枚举在序列化和反序列化的时候,除了name值,其他啥都不带的
  • 禁止给枚举提供set方法,没用的

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值