问题背景

有如下的一个类,属性有很多,每个字段按照出现在 excel(xsl) 中的固定顺序解析成不同的数据类型,如字符串、BigDecimal、Integer 等

  1. @NotBlank
  2. private String title; // 商品标题
  3. @NotBlank
  4. private String l1Category; // 一级品类
  5. ...

另外当某一条数据没有校验通过时,需要提供详细的哪一行哪一列的数据有问题,如下所示的信息

  1. R3 : 3 行, 18 列】:成本价 不是一个有效的人民币数字

此条信息中有两个点需要处理:

  1. 推导出某个字段在第几列:数据行信息,在解析文件的时候能获取到,但是列信息,有可能获取不到,比如:要求 5 个字段,但是只填写了第一个字段,后面的都是空的,所以只能根据定好的字段顺序,去推导出来,在第几列;
  2. 将第 n 列中的数字 n 转换为 excel 中的字母列

比如:规定出现在文件中的顺序为 title,name,....
想要找到该字段是第几列最好的方式是事先定义好,它的固定顺序, 方式有很多种,第一种能想到的就是如下方式

  1. public static final Map<String, Integer> fieldIndexs = new HashMap<>();
  2. static {
  3. fieldIndexs.put("title", 0);
  4. fieldIndexs.put("name", 1);
  5. ....
  6. }

在使用的地方也是这样手动的写上固定的字符串,但是这样有一个缺点:当字段很多,而且有可能字段名还没有调整好的时候,或则新增字段时,需要修改很多地方的字符串

你可能会说,可以把所有的字段都写成常量,这也有一个问题,需要同步数据对象和常量字符串。

笔者在这里发现 tkmapper 中有一个工具类,可以使用 jdk8 的方法引用 + 反射获取到方法的名称,下面来看看是如何实现的

解决方案

推导出某个字段在第几列

先来看看调用方法

  1. public static String extractFieldName(Fn<BatchImportParseProductRow, Object> fn) {
  2. return Reflections.fnToFieldName(fn);
  3. }
  4. public static void main(String[] args) {
  5. // title
  6. final String s = extractFieldName(BatchImportParseProductRow::getTitle);

BatchImportParseProductRow 是你的数据对象,通过方法引用 BatchImportParseProductRow::getTitle 传递给了工具类,下面来看看这个工具类

  1. package tk.mybatis.mapper.weekend.reflection;
  2. import tk.mybatis.mapper.weekend.Fn;
  3. import java.beans.Introspector;
  4. import java.lang.invoke.SerializedLambda;
  5. import java.lang.reflect.Method;
  6. import java.util.regex.Pattern;
  7. /**
  8. * @author Frank
  9. */
  10. public class Reflections {
  11. private static final Pattern GET_PATTERN = Pattern.compile("^get[A-Z].*");
  12. private static final Pattern IS_PATTERN = Pattern.compile("^is[A-Z].*");
  13. private Reflections() {
  14. }
  15. public static String fnToFieldName(Fn fn) {
  16. try {
  17. Method method = fn.getClass().getDeclaredMethod("writeReplace");
  18. method.setAccessible(Boolean.TRUE);
  19. SerializedLambda serializedLambda = (SerializedLambda) method.invoke(fn);
  20. String getter = serializedLambda.getImplMethodName();
  21. if (GET_PATTERN.matcher(getter).matches()) {
  22. getter = getter.substring(3);
  23. } else if (IS_PATTERN.matcher(getter).matches()) {
  24. getter = getter.substring(2);
  25. }
  26. // 这个类是 jdk 自带的将首字母变成小写的方法
  27. // java.beans.Introspector#decapitalize
  28. return Introspector.decapitalize(getter);
  29. } catch (ReflectiveOperationException e) {
  30. throw new ReflectionOperationException(e);
  31. }
  32. }
  33. }

这里最主要的是这个 writeReplace method 对象,我猜想应该是实现了 Serializable 接口才会出现这个方法,Fn 的定义如下

  1. package tk.mybatis.mapper.weekend;
  2. import java.io.Serializable;
  3. import java.util.function.Function;
  4. /**
  5. * @author Frank
  6. */
  7. public interface Fn<T, R> extends Function<T, R>, Serializable {
  8. }

下面利用这个工具类来实现我们的需求:先定义两个方法,通过方法引用获取字段名称、通过方法引用获取出现在文件中的固定顺序

  1. /**
  2. * 获取字段的位置,BatchImportParseProductRow 为你的数据对象
  3. *
  4. * @param fn
  5. * @return
  6. */
  7. public static int position(Fn<BatchImportParseProductRow, Object> fn) {
  8. return fieldIndexs.get(extractFieldName(fn));
  9. }
  10. public static String extractFieldName(Fn<BatchImportParseProductRow, Object> fn) {
  11. return Reflections.fnToFieldName(fn);
  12. }

再通过 map 形式定义好字段出现的顺序

  1. public static final Map<String, Integer> fieldIndexs = new HashMap<>();
  2. static {
  3. fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getTitle), 0);
  4. fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getL1Category), 1);
  5. fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getL2Category), 2);

以后修改了这个数据对象的的名称,通过 IDEA 重构方式,可以一处修改,多处生效,使用也比较方便

数字与 Excel 的列字母互转

这个功能是百度找的一段代码,这里直接贴出来

  1. /**
  2. * Excel 列的下标 和 字母互转
  3. *
  4. * @author Stephen.Huang
  5. * @version 2015-7-8
  6. */
  7. public class ExcelColumnUtil {
  8. public static void main(String[] args) {
  9. String colstr = "AA";
  10. int colIndex = excelColStrToNum(colstr, colstr.length());
  11. System.out.println("'" + colstr + "' column index of " + colIndex);
  12. colIndex = 26;
  13. colstr = excelColIndexToStr(colIndex);
  14. System.out.println(colIndex + " column in excel of " + colstr);
  15. colstr = "AAAA";
  16. colIndex = excelColStrToNum(colstr, colstr.length());
  17. System.out.println("'" + colstr + "' column index of " + colIndex);
  18. colIndex = 466948;
  19. colstr = excelColIndexToStr(colIndex);
  20. System.out.println(colIndex + " column in excel of " + colstr);
  21. }
  22. public static int excelColStrToNum(String colStr) {
  23. return excelColStrToNum(colStr, colStr.length());
  24. }
  25. /**
  26. * 列字母转数字
  27. * <pre>
  28. *
  29. * 注意:Excel column index 从 1 开始
  30. *
  31. * </pre>
  32. *
  33. * @param colStr
  34. * @param length
  35. * @return
  36. */
  37. public static int excelColStrToNum(String colStr, int length) {
  38. int num = 0;
  39. int result = 0;
  40. for (int i = 0; i < length; i++) {
  41. char ch = colStr.charAt(length - i - 1);
  42. num = (int) (ch - 'A' + 1);
  43. num *= Math.pow(26, i);
  44. result += num;
  45. }
  46. return result;
  47. }
  48. /**
  49. * 将列转成字母
  50. *
  51. * @param columnIndex 注意:Excel column index 从 1 开始
  52. * @return
  53. */
  54. public static String excelColIndexToStr(int columnIndex) {
  55. if (columnIndex <= 0) {
  56. return null;
  57. }
  58. String columnStr = "";
  59. columnIndex--;
  60. do {
  61. if (columnStr.length() > 0) {
  62. columnIndex--;
  63. }
  64. columnStr = ((char) (columnIndex % 26 + (int) 'A')) + columnStr;
  65. columnIndex = (int) ((columnIndex - columnIndex % 26) / 26);
  66. } while (columnIndex > 0);
  67. return columnStr;
  68. }
  69. }