问题背景
有如下的一个类,属性有很多,每个字段按照出现在 excel(xsl) 中的固定顺序解析成不同的数据类型,如字符串、BigDecimal、Integer 等
@NotBlank
private String title; // 商品标题
@NotBlank
private String l1Category; // 一级品类
...
另外当某一条数据没有校验通过时,需要提供详细的哪一行哪一列的数据有问题,如下所示的信息
【R3 : 第 3 行, 第 18 列】:成本价 不是一个有效的人民币数字
此条信息中有两个点需要处理:
- 推导出某个字段在第几列:数据行信息,在解析文件的时候能获取到,但是列信息,有可能获取不到,比如:要求 5 个字段,但是只填写了第一个字段,后面的都是空的,所以只能根据定好的字段顺序,去推导出来,在第几列;
- 将第 n 列中的数字 n 转换为 excel 中的字母列
比如:规定出现在文件中的顺序为 title,name,....
想要找到该字段是第几列最好的方式是事先定义好,它的固定顺序, 方式有很多种,第一种能想到的就是如下方式
public static final Map<String, Integer> fieldIndexs = new HashMap<>();
static {
fieldIndexs.put("title", 0);
fieldIndexs.put("name", 1);
....
}
在使用的地方也是这样手动的写上固定的字符串,但是这样有一个缺点:当字段很多,而且有可能字段名还没有调整好的时候,或则新增字段时,需要修改很多地方的字符串
你可能会说,可以把所有的字段都写成常量,这也有一个问题,需要同步数据对象和常量字符串。
笔者在这里发现 tkmapper 中有一个工具类,可以使用 jdk8 的方法引用 + 反射获取到方法的名称,下面来看看是如何实现的
解决方案
推导出某个字段在第几列
先来看看调用方法
public static String extractFieldName(Fn<BatchImportParseProductRow, Object> fn) {
return Reflections.fnToFieldName(fn);
}
public static void main(String[] args) {
// title
final String s = extractFieldName(BatchImportParseProductRow::getTitle);
BatchImportParseProductRow 是你的数据对象,通过方法引用 BatchImportParseProductRow::getTitle
传递给了工具类,下面来看看这个工具类
package tk.mybatis.mapper.weekend.reflection;
import tk.mybatis.mapper.weekend.Fn;
import java.beans.Introspector;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* @author Frank
*/
public class Reflections {
private static final Pattern GET_PATTERN = Pattern.compile("^get[A-Z].*");
private static final Pattern IS_PATTERN = Pattern.compile("^is[A-Z].*");
private Reflections() {
}
public static String fnToFieldName(Fn fn) {
try {
Method method = fn.getClass().getDeclaredMethod("writeReplace");
method.setAccessible(Boolean.TRUE);
SerializedLambda serializedLambda = (SerializedLambda) method.invoke(fn);
String getter = serializedLambda.getImplMethodName();
if (GET_PATTERN.matcher(getter).matches()) {
getter = getter.substring(3);
} else if (IS_PATTERN.matcher(getter).matches()) {
getter = getter.substring(2);
}
// 这个类是 jdk 自带的将首字母变成小写的方法
// java.beans.Introspector#decapitalize
return Introspector.decapitalize(getter);
} catch (ReflectiveOperationException e) {
throw new ReflectionOperationException(e);
}
}
}
这里最主要的是这个 writeReplace
method 对象,我猜想应该是实现了 Serializable 接口才会出现这个方法,Fn 的定义如下
package tk.mybatis.mapper.weekend;
import java.io.Serializable;
import java.util.function.Function;
/**
* @author Frank
*/
public interface Fn<T, R> extends Function<T, R>, Serializable {
}
下面利用这个工具类来实现我们的需求:先定义两个方法,通过方法引用获取字段名称、通过方法引用获取出现在文件中的固定顺序
/**
* 获取字段的位置,BatchImportParseProductRow 为你的数据对象
*
* @param fn
* @return
*/
public static int position(Fn<BatchImportParseProductRow, Object> fn) {
return fieldIndexs.get(extractFieldName(fn));
}
public static String extractFieldName(Fn<BatchImportParseProductRow, Object> fn) {
return Reflections.fnToFieldName(fn);
}
再通过 map 形式定义好字段出现的顺序
public static final Map<String, Integer> fieldIndexs = new HashMap<>();
static {
fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getTitle), 0);
fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getL1Category), 1);
fieldIndexs.put(extractFieldName(BatchImportParseProductRow::getL2Category), 2);
以后修改了这个数据对象的的名称,通过 IDEA 重构方式,可以一处修改,多处生效,使用也比较方便
数字与 Excel 的列字母互转
这个功能是百度找的一段代码,这里直接贴出来
/**
* Excel 列的下标 和 字母互转
*
* @author Stephen.Huang
* @version 2015-7-8
*/
public class ExcelColumnUtil {
public static void main(String[] args) {
String colstr = "AA";
int colIndex = excelColStrToNum(colstr, colstr.length());
System.out.println("'" + colstr + "' column index of " + colIndex);
colIndex = 26;
colstr = excelColIndexToStr(colIndex);
System.out.println(colIndex + " column in excel of " + colstr);
colstr = "AAAA";
colIndex = excelColStrToNum(colstr, colstr.length());
System.out.println("'" + colstr + "' column index of " + colIndex);
colIndex = 466948;
colstr = excelColIndexToStr(colIndex);
System.out.println(colIndex + " column in excel of " + colstr);
}
public static int excelColStrToNum(String colStr) {
return excelColStrToNum(colStr, colStr.length());
}
/**
* 列字母转数字
* <pre>
*
* 注意:Excel column index 从 1 开始
*
* </pre>
*
* @param colStr
* @param length
* @return
*/
public static int excelColStrToNum(String colStr, int length) {
int num = 0;
int result = 0;
for (int i = 0; i < length; i++) {
char ch = colStr.charAt(length - i - 1);
num = (int) (ch - 'A' + 1);
num *= Math.pow(26, i);
result += num;
}
return result;
}
/**
* 将列转成字母
*
* @param columnIndex 注意:Excel column index 从 1 开始
* @return
*/
public static String excelColIndexToStr(int columnIndex) {
if (columnIndex <= 0) {
return null;
}
String columnStr = "";
columnIndex--;
do {
if (columnStr.length() > 0) {
columnIndex--;
}
columnStr = ((char) (columnIndex % 26 + (int) 'A')) + columnStr;
columnIndex = (int) ((columnIndex - columnIndex % 26) / 26);
} while (columnIndex > 0);
return columnStr;
}
}