文档撰写人员:张家骏,林平钏
团队开发人员:张红洁,张家骏,覃以才,于石,林平钏
背景:项目为CRM类金融系统,很多时候都需要将客户提交的Excel文件进行解析。在项目的开发中,我们团队意识到,对于Excel的解析是可以进行解耦出来,作为独立的工具类。
案例分析
我们可以清楚地意识到,标题其实是Bean中的属性。这意味着,Excel中,从第二行开始,每一行都是一个Bean的实例。
那么我们能不能通过某一个具体的Bean类,从Excel中的内容中获取所有的Bean的实例。
以上这就是我们团队的灵感来源。
设计
以下是团队提供的解决方案:
1.怎么设计Bean类
- 提供标题栏与Bean类的属性的元组。若没有,则转下一步;
- Bean类的属性有无提供标题栏的注解。若有,组成上一步的元组,若没有,则转下一步;
- 直接按照标题栏与Bean类的属性的顺序,一一对应的获取,这里将不会组成元组。
2.如何获取Bean的实例
-
有元组模式
获取sheet的第一行作为标题栏,然后将元组的所有标题与sheet的所有标题,进行取交集。
然后将这些交集的值,一一赋值给Bean的实例。
举例来说:
元组里面含有 【标题2,标题1,这是一个意想不到的】
标题栏含有【标题2,标题1,我的标题3,我的日期,超大的整数,这是提高】
取得交集【标题2,标题1】 没有元组模式:
直接获取从第二栏开始的内容,进行一一赋值。
实现
Bean类的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CellTitle {
public String value();
}
解析Sheet的标题栏的元组
public final class SheetToken {
// excel的标题
private String title;
// Bean的属性名字
private String property;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
Sheet的解析器
public class SheetLexer {
private static Logger log = LoggerFactory.getLogger(SheetLexer.class);
// 要解析的sheet
private Sheet sheet;
// sheet的token列表
private List tokens;
// 是否使用严格模式,若使用严格模式,则一旦出错,将不再进行解析,直接抛出异常。默认不用严格模式
private AtomicBoolean isStrict = new AtomicBoolean(false);
public SheetLexer() {
super();
}
/**
* 如果没有给tokens赋值,则默认是为按照sheet的列的顺序进行解析 不推荐,因为此方式很容易出错,不进行解析
*
* @param sheet
*/
public SheetLexer(Sheet sheet) {
super();
this.sheet = sheet;
}
public SheetLexer(Sheet sheet, AtomicBoolean isStrict) {
super();
this.sheet = sheet;
this.isStrict = isStrict;
}
/**
* 推荐给tokens赋值,不按照sheet的列顺序进行解析
*
* @param sheet
* @param tokens
*/
public SheetLexer(Sheet sheet, List tokens) {
super();
this.sheet = sheet;
this.tokens = tokens;
}
/**
* 推荐不使用严格模式。若使用严格模式,则一旦出错,将不再进行解析,直接抛出异常
*
* @see SheetLexer(HSSFSheet sheet, List tokens)
* @param sheet
* @param tokens
* @param isStrict
*/
public SheetLexer(Sheet sheet, List tokens, AtomicBoolean isStrict) {
super();
this.sheet = sheet;
this.tokens = tokens;
this.isStrict = isStrict;
}
public AtomicBoolean getIsStrict() {
return isStrict;
}
public void setIsStrict(AtomicBoolean isStrict) {
this.isStrict = isStrict;
}
public Sheet getSheet() {
return sheet;
}
public void setSheet(Sheet sheet) {
this.sheet = sheet;
}
public List getTokens() {
return tokens;
}
public void setTokens(List tokens) {
this.tokens = tokens;
}
/**
* 默认第一行作为列的标题。在严格模式下,列的长度必须等于Bean的属性长度。
*
* @param
*
* @param clazz
* @return
*/
private Map convertFrom(Class clazz) {
Map map = new ConcurrentHashMap<>();
if (ObjectUtil.nonNull(sheet)) {
final int titleIndex = 0;
if (sheet.getLastRowNum() >= titleIndex) {
Row sheetTitles = sheet.getRow(titleIndex);
if ((sheetTitles.getLastCellNum() - sheetTitles.getFirstCellNum()) != clazz.getDeclaredFields().length
&& isStrict.get()) {
throw new RuntimeException("columns length cannot equals to field size in the strict mode");
}
for (int i = sheetTitles.getFirstCellNum(); i < sheetTitles.getLastCellNum(); i++) {
Cell cell = sheetTitles.getCell(i);
if (ObjectUtil.nonNull(cell) && StringUtils.isNotBlank(cell.getStringCellValue())) {
if (CollectionUtils.isNotEmpty(tokens)) {
for (SheetToken token : tokens) {
if (cell.getStringCellValue().equals(token.getTitle())) {
map.put(cell.getStringCellValue(), cell.getColumnIndex());
}
}
} else {
map.put(cell.getStringCellValue(), cell.getColumnIndex());
}
}
}
}
}
return map;
}
private void initTokens(Class clazz) {
if (CollectionUtils.isEmpty(tokens)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(CellTitle.class)) {
if (CollectionUtils.isEmpty(tokens))
tokens = new ArrayList();
SheetToken token = new SheetToken();
token.setProperty(field.getName());
token.setTitle(field.getAnnotation(CellTitle.class).value());
tokens.add(token);
}
}
}
}
/**
* 实例化属性值
*
* @param field
* @param cell
* @param t
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private void instantiateField(Field field, Cell cell, T t)
throws IllegalArgumentException, IllegalAccessException {
if (ObjectUtil.nonNull(field) && ObjectUtil.nonNull(cell) && ObjectUtil.nonNull(t)) {
try {
boolean flag = field.isAccessible();
field.setAccessible(true);
if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
field.set(t, cell.getStringCellValue());
} else if (cell.getCellType() == Cell.CELL_TYPE_BOOLEAN)
field.set(t, cell.getBooleanCellValue());
else if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
if (field.getType() == Integer.TYPE) {
field.setInt(t, (int) cell.getNumericCellValue());
} else if (field.getType() == Date.class) {
field.set(t, cell.getDateCellValue());
} else if (field.getType() == BigDecimal.class) {
field.set(t, new BigDecimal(cell.getNumericCellValue()));
} else if (field.getType() == String.class) {
if (cell.getNumericCellValue() == ((int) cell.getNumericCellValue()))
field.set(t, String.valueOf((int) cell.getNumericCellValue()));
else
field.set(t, String.valueOf(cell.getNumericCellValue()));
} else {
field.set(t, cell.getNumericCellValue());
}
}
field.setAccessible(flag);
} catch (Exception e) {
log.error("{} happen when instantiate field {} from sheet ", e.getMessage(), field.getName());
if (isStrict.get())
throw new RuntimeException(e.getMessage());
}
}
}
/**
*
* 按照所给出模型的property中的名字和属性,然后通过反射找出所有列。
*
*
* 如果property与列存在着差异,则取两者的交集。
*
*
* @param row
* @param clazz
* @return
* @throws IllegalAccessException
* @throws InstantiationException
* @throws SecurityException
* @throws NoSuchFieldException
*/
public T nextRow(int row, Class clazz)
throws InstantiationException, IllegalAccessException, NoSuchFieldException, SecurityException {
T t = clazz.newInstance();
if (ObjectUtil.nonNull(sheet)) {
Map map = convertFrom(clazz);
// 获取的行数必须小于等于总行数
if (row <= sheet.getLastRowNum()) {
Row rowContent = sheet.getRow(row);
if (ObjectUtil.nonNull(tokens)) {
for (Entry entry : map.entrySet()) {
Cell cell = rowContent.getCell(entry.getValue());
Field field = null;
if (ObjectUtil.nonNull(tokens)) {
for (SheetToken token : tokens) {
try {
if (entry.getKey().equals(token.getTitle()))
field = clazz.getDeclaredField(token.getProperty());
} catch (Exception e) {
log.error("no such field {} in the excel titles", token.getProperty());
if (isStrict.get())
throw e;
}
}
}
instantiateField(field, cell, t);
}
} else {
Field[] fields = clazz.getDeclaredFields();
if (isStrict.get()
&& fields.length != (rowContent.getLastCellNum() - rowContent.getFirstCellNum())) {
throw new RuntimeException("columns length cannot equals to field size in the strict mode");
}
for (int i = rowContent.getFirstCellNum(); i < rowContent.getLastCellNum(); i++) {
instantiateField(fields[i], rowContent.getCell(i), t);
}
}
}
}
return t;
}
/**
*
* 获取行数,(begin,end]
*
*
* @param begin
* @param end
* @param clazz
* @return
*/
public List obtainRows(int begin, int end, Class clazz) {
List list = new ArrayList<>();
for (int i = begin; i <= end; i++) {
try {
list.add(nextRow(i, clazz));
} catch (Exception e) {
log.error("{} happen when instantiate {} from sheet ", e.getMessage(), clazz.getName());
if (isStrict.get()) {
String message = e.getMessage();
if (e instanceof NoSuchFieldException) {
message = "no such field named " + e.getMessage() + " in the bean named " + clazz.getName();
}
throw new RuntimeException(message);
}
}
}
return list;
}
/**
*
* 获取sheet的所有行
*
*
* @param clazz
* @return
*/
public List allRows(Class clazz) {
initTokens(clazz);
List list = new ArrayList();
if (ObjectUtil.nonNull(sheet)) {
list = obtainRows(sheet.getFirstRowNum() + 1, sheet.getLastRowNum(), clazz);
}
return list;
}
}
测试案例
测试实体类
public class TestToken {
private int first;
private double second;
@CellTitle("我的标题3")
private String thrid;
private Date fourth;
private BigDecimal bigNum;
public BigDecimal getBigNum() {
return bigNum;
}
public void setBigNum(BigDecimal bigNum) {
this.bigNum = bigNum;
}
public int getFirst() {
return first;
}
public void setFirst(int first) {
this.first = first;
}
public double getSecond() {
return second;
}
public void setSecond(double second) {
this.second = second;
}
public String getThrid() {
return thrid;
}
public void setThrid(String thrid) {
this.thrid = thrid;
}
public Date getFourth() {
return fourth;
}
public void setFourth(Date fourth) {
this.fourth = fourth;
}
}
测试方法
public static void main(String[] args) throws FileNotFoundException, IOException {
Workbook wb = null;
try (FileInputStream input = new FileInputStream(new File("C:/Users/thinkive/Desktop/test.xlsx"))) {
try {
wb = new XSSFWorkbook(input);
} catch (Exception e) {
POIFSFileSystem fs = new POIFSFileSystem(input);
wb = new HSSFWorkbook(fs);
}
} catch (Exception e) {
}
Sheet sheet = wb.getSheetAt(0);
{
List testTokens = new SheetLexer(sheet).allRows(TestToken.class);
System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(testTokens));
}
}
[
{
"first": 0,
"second": 0.0,
"thrid": "123.1234"
},
{
"first": 0,
"second": 0.0,
"thrid": "我轻轻地,尝一口,你说的爱我"
}
]