【JAVA定长文本转换工具:fixedformat4j】

工具简介

该工具是由一名国外工程师jeyben 发布到github上。目前star是41,属于非常小众。但是功能非常具体,十分好用。推荐给大家!

工具用途

用于定长的文本做format成具体的java DTO,以及java DTO做parse成为固定长度的文本,文本中各个属性又是定长的。

工具核心

一、重要的注解

  1. @Record 用于标识需要做转化的类,进行扫描;
  2. @Feild 用于标识哪些字段需要做转化,并且有两个重要属性:offset(标识字段位于文本的第N个位置)、length(标识当前字段的长度)、align(标识字段从左到右解析还是从右到左解析,接收LEFT和RIGHT枚举值)、paddingChar(标识字段由什么字符来做填充,一般用于数字以及字符,例如数字填充0,字符填充空格等等)
  3. @FixedFormatPattern 用于时间格式的Pattern,其他类型没有做尝试。

二、支持的类型

类型
String
Character / char
Long / long
Integer / int
Double / double
Float /float
Boolean / boolean
Date
BigDecimal

三、代码亮点

  1. 大量结合注解+反射实现对不同类型的load和parse
/**
   * @inheritDoc
   */
  public <T> T load(Class<T> fixedFormatRecordClass, String data) {
    HashMap<String, Object> foundData = new HashMap<String, Object>();
    HashMap<String, Class<?>> methodClass = new HashMap<String, Class<?>>();
    //assert the record is marked with a Record
    getAndAssertRecordAnnotation(fixedFormatRecordClass);

    //根据传入class解析为对应实例
    T instance = createRecordInstance(fixedFormatRecordClass);

    //根据当前类的所有方法做循环,切割出对应的field的string做后续的解析
    Method[] allMethods = fixedFormatRecordClass.getMethods();
    for (Method method : allMethods) {
      String methodName = stripMethodPrefix(method.getName());
      Field fieldAnnotation = method.getAnnotation(Field.class);
      Fields fieldsAnnotation = method.getAnnotation(Fields.class);
      if (fieldAnnotation != null) {
        readFieldData(fixedFormatRecordClass, data, foundData, methodClass, method, methodName, fieldAnnotation);
      } else if (fieldsAnnotation != null) {
        //@Feilds 集合的特殊判空处理
        if (fieldsAnnotation.value() == null || fieldsAnnotation.value().length == 0) {
          throw new FixedFormatException(format("%s annotation must contain minimum one %s annotation", Fields.class.getName(), Field.class.getName()));
        }
        readFieldData(fixedFormatRecordClass, data, foundData, methodClass, method, methodName, fieldsAnnotation.value()[0]);
      }
    }
    //将所有找到的切片string做循环
    Set<String> keys = foundData.keySet();
    for (String key : keys) {
      String setterMethodName = "set" + key;

      Object foundDataObj = foundData.get(key);
      if (foundDataObj != null) {
        Class datatype = methodClass.get(key);
        Method method;
        try {
          method = fixedFormatRecordClass.getMethod(setterMethodName, datatype);
        } catch (NoSuchMethodException e) {
          throw new FixedFormatException(format("setter method named %s.%s(%s) does not exist", fixedFormatRecordClass.getName(), setterMethodName, datatype));
        }
        try {
          //反射设置属性
          method.invoke(instance, foundData.get(key));
        } catch (Exception e) {
          throw new FixedFormatException(format("could not invoke method %s.%s(%s)", fixedFormatRecordClass.getName(), setterMethodName, datatype), e);
        }
      }

    }
    return instance;
  }
/**
   * 这里使用到了递归export中for循环调用exportDataAccordingFieldAnnotation
   * @inheritDoc
   */
  public <T> String export(String template, T fixedFormatRecord) {
    StringBuffer result = new StringBuffer(template);
    Record record = getAndAssertRecordAnnotation(fixedFormatRecord.getClass());

    Class fixedFormatRecordClass = fixedFormatRecord.getClass();
    HashMap<Integer, String> foundData = new HashMap<Integer, String>(); 
    //通过传入的类,反向解析offset,realvalue这些数据,将数据填充进foundData这个offset做key的集合
    Method[] allMethods = fixedFormatRecordClass.getMethods();
    for (Method method : allMethods) {
      Field fieldAnnotation = method.getAnnotation(Field.class);
      Fields fieldsAnnotation = method.getAnnotation(Fields.class);
      if (fieldAnnotation != null) {
        String exportedData = exportDataAccordingFieldAnnotation(fixedFormatRecord, method, fieldAnnotation);
        foundData.put(fieldAnnotation.offset(), exportedData);
      } else if (fieldsAnnotation != null) {
        Field[] fields = fieldsAnnotation.value();
        for (Field field : fields) {
          String exportedData = exportDataAccordingFieldAnnotation(fixedFormatRecord, method, field);
          foundData.put(field.offset(), exportedData);
        }
      }
    }
    //读取foundData,将map转化为最终返回的文本
    Set<Integer> sortedoffsets = foundData.keySet();
    for (Integer offset : sortedoffsets) {
      String data = foundData.get(offset);
      appendData(result, record.paddingChar(), offset, data);
    }

    if (record.length() != -1) { //pad with paddingchar
      while (result.length() < record.length()) {
        result.append(record.paddingChar());
      }
    }
    return result.toString();
  }


/*
**单个feild 的读取
*/
@SuppressWarnings({"unchecked"})
  private <T> String exportDataAccordingFieldAnnotation(T fixedFormatRecord, Method method, Field fieldAnno) {
    String result;
    Class datatype = getDatatype(method, fieldAnno);

    FormatContext<T> context = getFormatContext(datatype, fieldAnno);
    FixedFormatter formatter = getFixedFormatterInstance(context.getFormatter(), context);
    FormatInstructions formatdata = getFormatInstructions(method, fieldAnno);
    Object valueObject;
    try {
      valueObject = method.invoke(fixedFormatRecord);
    } catch (Exception e) {
      throw new FixedFormatException(format("could not invoke method %s.%s(%s)", fixedFormatRecord.getClass().getName(), method.getName(), datatype), e);
    }

    //recursivly follow if the valueObject is annotated as a record
    if (valueObject != null && valueObject.getClass().getAnnotation(Record.class) != null) {
      result = export(valueObject);
    } else {
      result = formatter.format(valueObject, formatdata);
    }
    if (LOG.isDebugEnabled()) {
      LOG.debug(format("exported %s ", result));
    }
    return result;
  }
  1. 巧妙的设计思路
    目录结构
    ①定义注解,功能性和使用性的巧妙设计
    ②多重继承,值得学习
    ③主类FixedFormatManagerImpl的各个方法的设计
    ④广泛的使用泛型
    ⑤提供拓展的接口,目前只支持上述一些类型。像是java8的datetime这些目前好像还不支持,可以方便大家去扩充。毕竟这个工具项目已经有10多年历史了。

工具使用

官网使用教程

public class TestFixedFormatManagerImpl extends TestCase {

  private static final Log LOG = LogFactory.getLog(TestFixedFormatManagerImpl.class);

  private static String STR = "some text ";

  public static final String MY_RECORD_DATA = "some text 0012320080514CT001100000010350000002056-0012 01200000002056";
  public static final String MULTIBLE_RECORD_DATA = "some      2008101320081013                       0100";
  public static final String MULTIBLE_RECORD_DATA_X_PADDED = "some      2008101320081013xxxxxxxxxxxxxxxxxxxxxxx0100";

  FixedFormatManager manager = null;

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    manager = new FixedFormatManagerImpl();
  }

  public void testLoadRecord() {
    MyRecord loadedRecord = manager.load(MyRecord.class, MY_RECORD_DATA);
    Assert.assertNotNull(loadedRecord);
    Assert.assertEquals(STR, loadedRecord.getStringData());
    Assert.assertTrue(loadedRecord.isBooleanData());
  }

  public void testLoadMultibleFieldsRecord() {
    //when reading data having multible field annotations the first field will decide what data to return
    Calendar someDay = Calendar.getInstance();
    someDay.set(2008, 9, 13, 0, 0, 0);
    someDay.set(Calendar.MILLISECOND, 0);
    MultibleFieldsRecord loadedRecord = manager.load(MultibleFieldsRecord.class, MULTIBLE_RECORD_DATA);
    Assert.assertNotNull(loadedRecord);
    Assert.assertEquals("some      ", loadedRecord.getStringData());
    Assert.assertEquals(someDay.getTime(), loadedRecord.getDateData());
  }

  public void testExportRecordObject() {
    MyRecord myRecord = createMyRecord();
    Assert.assertEquals("wrong record exported", MY_RECORD_DATA, manager.export(myRecord));
  }

  public void testExportNestedRecordObject() {
    MyRecord myRecord = createMyRecord();
    MyOtherRecord myOtherRecord = new MyOtherRecord(myRecord);
    Assert.assertEquals("wrong record exported", MY_RECORD_DATA, manager.export(myOtherRecord));

    myOtherRecord = new MyOtherRecord((MyRecord) null);
    Assert.assertEquals("wrong record exported", "", manager.export(myOtherRecord));
  }

  private MyRecord createMyRecord() {
    Calendar someDay = Calendar.getInstance();
    someDay.set(2008, 4, 14, 0, 0, 0);
    someDay.set(Calendar.MILLISECOND, 0);

    MyRecord myRecord = new MyRecord();
    myRecord.setBooleanData(true);
    myRecord.setCharData('C');
    myRecord.setDateData(someDay.getTime());
    myRecord.setDoubleData(10.35);
    myRecord.setFloatData(20.56F);
    myRecord.setLongData(11L);
    myRecord.setIntegerData(123);
    myRecord.setStringData("some text ");
    myRecord.setBigDecimalData(new BigDecimal(-12.012));
    myRecord.setSimpleFloatData(20.56F);
    return myRecord;
  }

  public void testExportMultibleFieldRecordObject() {
    Calendar someDay = Calendar.getInstance();
    someDay.set(2008, 9, 13, 0, 0, 0);
    someDay.set(Calendar.MILLISECOND, 0);

    MultibleFieldsRecord multibleFieldsRecord = new MultibleFieldsRecord();
    multibleFieldsRecord.setDateData(someDay.getTime());
    multibleFieldsRecord.setStringData("some      ");
    multibleFieldsRecord.setIntegerdata(100);
    manager.export(multibleFieldsRecord);
    Assert.assertEquals("wrong record exported", MULTIBLE_RECORD_DATA, manager.export(multibleFieldsRecord));
  }

  public void testExportIntoExistingString() {
    Calendar someDay = Calendar.getInstance();
    someDay.set(2008, 9, 13, 0, 0, 0);
    someDay.set(Calendar.MILLISECOND, 0);

    MultibleFieldsRecord multibleFieldsRecord = new MultibleFieldsRecord();
    multibleFieldsRecord.setDateData(someDay.getTime());
    multibleFieldsRecord.setStringData("some      ");
    multibleFieldsRecord.setIntegerdata(100);
    String exportedString = manager.export("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", multibleFieldsRecord);
    Assert.assertEquals("wrong record exported", MULTIBLE_RECORD_DATA_X_PADDED, exportedString);
  }

  public void testLoadNonRecordAnnotatedClass() {
    try {
      manager.load(String.class, "some");
    } catch (FixedFormatException e) {
      //expected
    }
  }

  public void testExportAnnotatedNestedClass() {
    MyRecord.MyStaticNestedClass myStaticNestedClass = new MyRecord.MyStaticNestedClass();
    myStaticNestedClass.setStringData("xyz");
    String exportedString = manager.export(myStaticNestedClass);
    Assert.assertEquals("xyz       ", exportedString);

    NoDefaultConstructorClass.MyStaticNestedClass myStaticNestedClass2 = new NoDefaultConstructorClass.MyStaticNestedClass();
    myStaticNestedClass2.setStringData("xyz");
    String exportedString2 = manager.export(myStaticNestedClass2);
    Assert.assertEquals("xyz       ", exportedString2);
  }

  public void testExportAnnotatedInnerClass() {
    MyRecord myRecord = new MyRecord();
    MyRecord.MyInnerClass myInnerClass = myRecord.new MyInnerClass();
    myInnerClass.setStringData("xyz");
    String exportedString = manager.export(myInnerClass);
    Assert.assertEquals("xyz       ", exportedString);
 
    NoDefaultConstructorClass noDefaultConstructorClass = new NoDefaultConstructorClass("foobar");
    NoDefaultConstructorClass.MyInnerClass myInnerClass2 = noDefaultConstructorClass.new MyInnerClass();
    myInnerClass2.setStringData("xyz");
    exportedString = manager.export(myInnerClass2);
    Assert.assertEquals("xyz       ", exportedString);
  }

  public void testImportAnnotatedNestedClass() {
    MyRecord.MyStaticNestedClass staticNested = manager.load(MyRecord.MyStaticNestedClass.class, "xyz       ");
    Assert.assertEquals("xyz", staticNested.getStringData());

    NoDefaultConstructorClass.MyStaticNestedClass staticNested2 = manager.load(NoDefaultConstructorClass.MyStaticNestedClass.class, "xyz       ");
    Assert.assertEquals("xyz", staticNested2.getStringData());
  }

  public void testImportAnnotatedInnerClass() {
    MyRecord.MyInnerClass inner = manager.load(MyRecord.MyInnerClass.class, "xyz       ");
    Assert.assertEquals("xyz", inner.getStringData());


    try {
      manager.load(NoDefaultConstructorClass.MyInnerClass.class, "xyz       ");
      fail(String.format("expected an %s exception to be thrown", FixedFormatException.class.getName()));
    } catch (FixedFormatException e) {
      //expected this
    }
  }

  public void testParseFail() {
    try {
      manager.load(MyRecord.class, "foobarfoobarfoobarfoobar");
      fail("expected parse exception");
    } catch (ParseException e) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("bu", e);
      }
      //expected
    }
  }
}

  1. 如果你只是着急用的话,可以直接读官网上面的样例用完之后,你应该就懂了。
  2. 如果你需要做一些BigDecial四舍五入,Double带有正负数这些,我建议你下载github的项目,项目中test有大量的测试用例,当然如果你需要拓展,建议去看看用例,方便知道如何在内部进行测试。
    测试代码目录

鸣谢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值