DoneSpeak

为easyexcel设置TimeZone

字数: 3.4k时长: 17 min
2019/11/23 Share

写在前面

导出Excel是系统中经常用到的功能。实现的方案也很多,可以自己去封装Apache Poi,也可以直接使用别人已经封装好的类库。如果需求简单的话,自己做实现也是可以的,所有的bug和feature都将是可控的。使用第三方的类库主要是方便,避免重复造轮子,但不好地方在于如果发现bug或者feature不满足时,会严重受限于类库版本的迭代。

在导出数据中经常会含有时间,在时间格式化时,如果不指定时区,则会使用服务器的时区进行格式化,这样可能导致导出的时间不是希望的时间。因而指定时区是一个很重要的功能。由于EasyExcel没有提供指定时区的功能,因而需要自己进行解决。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.2</version>
</dependency>

实现方案

EasyExcel提供的可拓展功能为用户提供了很大的方便,特别是允许自定义转化器和监听器。这里将使用自定义转化器的功能进行解决。因为一个表中的时间一般都是在同一个时区,所以应该实现全局时区,同时应该支持动态配置,而不是硬编码一个时区到代码中。此外,这里还提供了一个设置类Date类型属性的时区的方法。

如下为最终的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.gitlab.donespeak.tutorial.excel.easyexcel.timezone.DateTimeZoneConverterTest.TheDate
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
public static class TheDate {
@DateTimeFormat("yyyy-MM-dd hh:mm:ss:SSS")
@ExcelProperty(index = 0)
private Date date;

@DateTimeFormat("yyyy-MM-dd hh:mm:ss:SSS")
@DateTimeZone("Asia/Tokyo")
@ExcelProperty(index = 1)
private Date jpDate;
}

这里推荐使用registerConverter方法直接替代ExcelWriterBuilderExcelReaderBuilder中的默认的类型转化器。虽然也可以通过指定ExcelProperty.converter的方法进行配置,但还是会稍显麻烦。

1
2
3
4
5
6
7
8
9
// 用 US/Central 去写入Excel中的时间
EasyExcel.write(file, TheDate.class)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.sheet("theDate").doWrite(listOriginal);

// 用 US/Central 去读取Excel中的时间
List<TheDate> listUsCentralWriteUsCentralRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.head(TheDate.class).sheet().doReadSync();

定义 @DateTimeZone 注解

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DateTimeZone {

/**
* Specific value reference {@link TimeZone#getAvailableIDs()}
*/
String value() default "";
}

该注解指定一个Date属性的时区。

实现时区转化器:Date <-> String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import com.alibaba.excel.converters.date.DateStringConverter;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

import java.text.ParseException;
import java.util.Date;

public class DateTimeZoneStringConverter extends DateStringConverter {

private final String globalTimeZoneId;

public DateTimeZoneStringConverter() {
super();
globalTimeZoneId = null;
}

public DateTimeZoneStringConverter(String timeZoneId) {
super();
globalTimeZoneId = timeZoneId;
}

@Override
public Date convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) throws ParseException {

String timeZoneId = getTimeZoneId(contentProperty);
String timeFormat = getTimeFormat(contentProperty);

// System.out.println(String.format("%s: %s: %s", cellData.getStringValue(), timeFormat, timeZoneId));
Date date = DateUtils.parseDate(cellData.getStringValue(), timeFormat , timeZoneId);
return date;
}

@Override
public CellData convertToExcelData(Date value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {

String timeZoneId = getTimeZoneId(contentProperty);
String timeFormat = getTimeFormat(contentProperty);

// System.out.println(String.format("%s: %s: %s", value, timeFormat, timeZoneId));
String excelValue = DateUtils.format(value, timeFormat, timeZoneId);
return new CellData(excelValue);
}

private String getTimeZoneId(ExcelContentProperty contentProperty) {
if (contentProperty == null) {
return null;
}
return DateTimeZoneUtil.getTimeZone(contentProperty.getField(), globalTimeZoneId);
}

private String getTimeFormat(ExcelContentProperty contentProperty) {
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
return null;
}
return contentProperty.getDateTimeFormatProperty().getFormat();
}
}

com.alibaba.excel.converters.date.DateStringConverter 是EasyExcel定义的用于将Date导出为String的转化器。此外还有将Date转化为Number的转化器com.alibaba.excel.converters.date.DateNumberConverter

为了方便,DateTimeZoneStringConverter直接继承了DateStringConverter,并覆盖用于转化的两个方法convertToJavaData()convertToExcelData()。看起来修改了很多,实际上没有太大的改动,就增加了一个获取时区的方法和在SimpleDateFormat中增加了TimeZone

这里的DateUtils是重写的DateUtils,EasyExcel中的com.alibaba.excel.util.DateUtils的实现没有支持TimeZone。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import com.alibaba.excel.util.StringUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class DateUtils {

public static final String DATE_FORMAT_10 = "yyyy-MM-dd";
public static final String DATE_FORMAT_14 = "yyyyMMddHHmmss";
public static final String DATE_FORMAT_17 = "yyyyMMdd HH:mm:ss";
public static final String DATE_FORMAT_19 = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_FORMAT_19_FORWARD_SLASH = "yyyy/MM/dd HH:mm:ss";
private static final String MINUS = "-";

private DateUtils() {
throw new AssertionError("DateUtils can't be instantiated.");
}

/**
* convert string to date
*/
public static Date parseDate(String dateString, String dateFormat, String timeZone) throws ParseException {
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = switchDateFormat(dateString);
}
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
if(!StringUtils.isEmpty(timeZone)) {
sdf.setTimeZone(TimeZone.getTimeZone(timeZone));
}
return sdf.parse(dateString);
}

/**
* convert string to date
*/
public static Date parseDate(String dateString) throws ParseException {
return parseDate(dateString, switchDateFormat(dateString), null);
}

/**
* switch date format
*/
private static String switchDateFormat(String dateString) {
int length = dateString.length();
switch (length) {
case 19:
if (dateString.contains(MINUS)) {
return DATE_FORMAT_19;
} else {
return DATE_FORMAT_19_FORWARD_SLASH;
}
case 17:
return DATE_FORMAT_17;
case 14:
return DATE_FORMAT_14;
case 10:
return DATE_FORMAT_10;
default:
throw new IllegalArgumentException("can not find date format for:" + dateString);
}
}

/**
* Format date
* <p>
* yyyy-MM-dd HH:mm:ss
*/
public static String format(Date date, String timeZone) {
return format(date, null, timeZone);
}

/**
* Format date
*
* 当dateFormat为空时,默认使用 yyyy-MM-dd HH:mm:ss
*/
public static String format(Date date, String dateFormat, String timeZone) {
if (date == null) {
return "";
}
if (StringUtils.isEmpty(dateFormat)) {
dateFormat = DATE_FORMAT_19;
}
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
if(!StringUtils.isEmpty(timeZone)) {
sdf.setTimeZone(TimeZone.getTimeZone(timeZone));
}
return sdf.format(date);
}
}

单独封装了TimeZone获取的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.alibaba.excel.util.StringUtils;
import java.lang.reflect.Field;

public class DateTimeZoneUtil {

public static String getTimeZone(Field field, String defaultTimeZoneId) {
DateTimeZone dateTimeZone = field.getAnnotation(DateTimeZone.class);
if (dateTimeZone == null) {
// 如果Field没有DateTimeZone注解,则使用全局的
return defaultTimeZoneId;
}
String timeZoneId = dateTimeZone.value();
if (StringUtils.isEmpty(timeZoneId)) {
// 如果Field的DateTimeZone注解的值为空,则使用全局的
return defaultTimeZoneId;
}
return timeZoneId;
}
}

实现时区转化器:Date <-> Number

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import com.alibaba.excel.converters.date.DateNumberConverter;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.DateUtil;

import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

@Slf4j
public class DateTimeZoneNumberConverter extends DateNumberConverter {

private final String globalTimeZoneId;

public DateTimeZoneNumberConverter() {
this(null);
}

public DateTimeZoneNumberConverter(String timeZoneId) {
super();
this.globalTimeZoneId = timeZoneId;
}

@Override
public Date convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {

TimeZone timeZone = getTimeZone(contentProperty);
boolean use1904windowing = getUse1904windowing(contentProperty, globalConfiguration);

return DateUtil.getJavaDate(cellData.getNumberValue().doubleValue(), use1904windowing, timeZone);
}

@Override
public CellData convertToExcelData(Date value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {

TimeZone timeZone = getTimeZone(contentProperty);
Calendar calendar = getCalendar(value, timeZone);

boolean use1904windowing = getUse1904windowing(contentProperty, globalConfiguration);

CellData cellData = new CellData(BigDecimal.valueOf(DateUtil.getExcelDate(calendar, use1904windowing)));

return cellData;
}

private TimeZone getTimeZone(ExcelContentProperty contentProperty) {
if(contentProperty == null) {
return null;
}
String timeZoneId = DateTimeZoneUtil.getTimeZone(contentProperty.getField(), globalTimeZoneId);
return TimeZone.getTimeZone(timeZoneId);
}

private Calendar getCalendar(Date date, TimeZone timeZone) {
Calendar calStart = Calendar.getInstance();
calStart.setTime(date);
if(timeZone != null) {
calStart.setTimeZone(timeZone);
}

return calStart;
}

private boolean getUse1904windowing(ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
return contentProperty.getDateTimeFormatProperty().getUse1904windowing();
} else {
return globalConfiguration.getUse1904windowing();
}
}
}

类似DateTimeZoneStringConverterDateTimeZoneNumberConverter继承了DateNumberConverter,提供了一个DateNumbe之间转化的转化器。

测试

如下的单元测试,对@DateTimeZoneDateTimeZoneStringConverterDateTimeZoneNumberConverter均进行了测试。同时这也是一个完整的使用案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package io.gitlab.donespeak.tutorial.excel.easyexcel.timezone;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.junit.Assert.assertEquals;

/**
* @author DoneSpeak
* @date 2019/11/21 22:01
*/
public class DateTimeZoneConverterTest {
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
public static class TheDate {
@DateTimeFormat("yyyy-MM-dd hh:mm:ss:SSS")
@ExcelProperty(index = 0)
private Date date;

@DateTimeFormat("yyyy-MM-dd hh:mm:ss:SSS")
@DateTimeZone("Asia/Tokyo")
@ExcelProperty(index = 1)
private Date jpDate;
}

@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

/**
* https://www.zeitverschiebung.net/cn/all-time-zones.html
*/
private static final String TIME_ZONE_ID_US_CENTRAL = "US/Central";
private static final String TIME_ZONE_ID_ETC_UTC = "Etc/UTC";
private static final String TIME_ZONE_ID_JP = "Asia/Tokyo"; // UTC+9

public File getTestDirectory() {
// return new File(""); // 使用本地路径,方便生成的文件
return temporaryFolder.getRoot();
}

@Test
public void testDateTimeZoneStringConverter() {
File file = new File(getTestDirectory(), "easyexcel-test-dateTimeZoneStringConverter.xlsx");

if(file.exists()) {
file.delete();
}
List<TheDate> listOriginal = data();

// 用 US/Central 去写入Excel中的时间
EasyExcel.write(file, TheDate.class)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.sheet("theDate").doWrite(listOriginal);

// 用 US/Central 去读取Excel中的时间
List<TheDate> listUsCentralWriteUsCentralRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.head(TheDate.class).sheet().doReadSync();

assertListEquals(listOriginal, listUsCentralWriteUsCentralRead);

// 用 UTC 时区去读取Excel中的时间
List<TheDate> listUsCentralWriteEtcUtcRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_ETC_UTC))
.head(TheDate.class).sheet().doReadSync();

System.out.println(listUsCentralWriteEtcUtcRead);

assertTimeSpan(collectDate(listOriginal, d -> d.getDate()), collectDate(listUsCentralWriteEtcUtcRead, d -> d.getDate()),
TIME_ZONE_ID_US_CENTRAL, TIME_ZONE_ID_ETC_UTC);
assertTimeSpan(collectDate(listOriginal, d -> d.getJpDate()), collectDate(listUsCentralWriteEtcUtcRead, d -> d.getJpDate()),
TIME_ZONE_ID_JP, TIME_ZONE_ID_JP);
}

@Test
public void testDateTimeZoneNumberConverter() {
File file = new File(getTestDirectory(), "easyexcel-test-dateTimeZoneNumberConverter.xlsx");

if(file.exists()) {
file.delete();
}
List<TheDate> listOriginal = data();

// 用 US/Central 去写入Excel中的时间
EasyExcel.write(file, TheDate.class)
.registerConverter(new DateTimeZoneNumberConverter(TIME_ZONE_ID_US_CENTRAL))
.sheet("theDate").doWrite(listOriginal);

// 用 US/Central 去读取Excel中的时间
List<TheDate> listUsCentralWriteUsCentralRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneNumberConverter(TIME_ZONE_ID_US_CENTRAL))
.head(TheDate.class).sheet().doReadSync();

assertListEquals(listOriginal, listUsCentralWriteUsCentralRead);

// 用 UTC 时区去读取Excel中的时间
List<TheDate> listUsCentralWriteEtcUtcRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneNumberConverter(TIME_ZONE_ID_ETC_UTC))
.head(TheDate.class).sheet().doReadSync();

assertTimeSpan(collectDate(listOriginal, d -> d.getDate()), collectDate(listUsCentralWriteEtcUtcRead, d -> d.getDate()),
TIME_ZONE_ID_US_CENTRAL, TIME_ZONE_ID_ETC_UTC);
assertTimeSpan(collectDate(listOriginal, d -> d.getJpDate()), collectDate(listUsCentralWriteEtcUtcRead, d -> d.getJpDate()),
TIME_ZONE_ID_JP, TIME_ZONE_ID_JP);
}

private List<TheDate> data() {
Date now = getTime();

List<TheDate> datas = new ArrayList<>();

TheDate thd = new TheDate();
thd.setDate(now);
thd.setJpDate(now);

datas.add(thd);
return datas;
}

private Date getTime() {
// 这里的时间保留保留位数应该和@DateTimeFormat一致,否则值比较时将会不相等
return new Date();
}

private long getTimeSpan(Date from, Date to) {
return from.getTime() - to.getTime();
}
private long getTimeZoneTimeSpan(String timeZoneIdfrom, String timeZoneIdTo) {
return TimeZone.getTimeZone(timeZoneIdfrom).getRawOffset()
- TimeZone.getTimeZone(timeZoneIdTo).getRawOffset();
}

private void assertListEquals(List<TheDate> listOriginal, List<TheDate> listUsCentral) {
assertEquals(listOriginal.size(), listUsCentral.size());
for(int i = 0; i < listOriginal.size(); i ++) {
TheDate original = listOriginal.get(i);
TheDate usCentral = listUsCentral.get(i);
assertEquals(original, usCentral);
}
}

private void assertTimeSpan(List<Date> dateOriginal, List<Date> dateOperated, String timeZoneWrite,
String timeZoneRead) {

long timeZoneSpanFromUsCentralToEtcUtc = getTimeZoneTimeSpan(timeZoneWrite, timeZoneRead);

for(int i = 0; i < dateOriginal.size(); i ++) {
// 对于同一个时间字符串,A时区 - B时区 = B时区解释 - A时区解释
long span = getTimeSpan(dateOperated.get(i), dateOriginal.get(i));
assertEquals(timeZoneSpanFromUsCentralToEtcUtc, span);
}
}

private List<Date> collectDate( final List<TheDate> list, Function<TheDate, Date> function) {
return list.stream().map(function).collect(Collectors.toList());
}
}

拓展 - 聊聊EasyExcel的转化器

写过程

1
2
3
EasyExcel.write(file, TheDate.class)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.sheet("theDate").doWrite(listOriginal);
  • EasyExcel.write(file, TheDate.class): 会创建一个 ExcelWriterBuilder,目前也仅仅是设置了文件输出路径和表头格式。
  • registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL)): 为 ExcelWriterBuilder.writeWorkbook添加自定义转化器。
  • sheet("theDate"): 创建ExcelWriterSheetBuilder,并配置ExcelWriter的上下文,也就是转化器等信息。
  • .doWrite(listOriginal): ExcelWriter将列表生成excel文件。

转化器的配置就发生在sheet("theDate")中。按照:

1
2
3
4
ExcelWriterSheetBuilder.sheet() -> ExcelWriterSheetBuilder.build()
-> new ExcelWriter(writeWorkbook) -> new ExcelBuilderImpl(writeWorkbook)
-> new WriteContextImpl(writeWorkbook) -> WirteContextImpl.initCurrentSheetHolder(writeSheet)
-> new WriteSheetHolder(writeSheet, writeWorkbookHolder) -> new AbstractWriteHolder()

到这里就可以找到配置Converter的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 配置默认Converter
if (parentAbstractWriteHolder == null) {
setConverterMap(DefaultConverterLoader.loadDefaultWriteConverter());
} else {
setConverterMap(new HashMap<String, Converter>(parentAbstractWriteHolder.getConverterMap()));
}
// 配置自定义Conveter
if (writeBasicParameter.getCustomConverterList() != null
&& !writeBasicParameter.getCustomConverterList().isEmpty()) {
for (Converter converter : writeBasicParameter.getCustomConverterList()) {
getConverterMap().put(ConverterKeyBuild.buildKey(converter.supportJavaTypeKey()), converter);
}
}

com.alibaba.excel.converters包下有EasyExcel提供的默认的Converter。在配置默认Converter的流程中,DefaultConverterLoader.loadDefaultWriteConverter()将默认的转化器进行加载。返回一个以converter.supportJavaTypeKey()构成的key,converter作为value的Map,加载完成之后会有如下的列表(映射关系中会将基本类型转化为封装类型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BigDecimal.class:   BigDecimalNumberConverter
Boolean.class: BooleanBooleanConverter
Byte.class: ByteNumberConverter
Date.class: DateStringConverter
Double.class: DoubleNumberConverter
Float.class: FloatNumberConverter
Integer.class: IntegerNumberConverter
Long.class: LongNumberConverter
Short.class: ShortNumberConverter
String.class: StringStringConverter
File.class: FileImageConverter
InpurtStream.class: InputStreamImageConverter
byte[].class: ByteArrayImageConverter
Byte[].class: BoxingByteArrayImageConverter
URL.class: UrlImageConverter

如果有自定义的Converter,则会使用自动定义的Conveter,则会根据supportJavaTypeKey替换原来的默认的Converter。

在写入的时候,由AbstractExcelWriteExecutor根据数据的类型,获取正确的转化器将JavaObject转化为正确的CellData

读过程

1
2
3
List<TheDate> listUsCentralWriteUsCentralRead = EasyExcel.read(file)
.registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL))
.head(TheDate.class).sheet().doReadSync();
  • EasyExcel.read(file): 创建ExcelReaderBuilder对象,配置输入文件位置,默认表头和默认监听器。
  • registerConverter(new DateTimeZoneStringConverter(TIME_ZONE_ID_US_CENTRAL)): 为ExcelReaderBuilder.readWorkbook添加自定义转化器。
  • head(TheDate.class): 设置表头。
  • sheet(): 创建ExcelReaderSheetBuilder,并配置ExcelReader的上下文,也就是转化器等信息。
  • doReadSync(): 同步读,将数据从文件中读取到对象列表中。

和写过程类似,转化器的配置就发生在sheet()中,且过程基本是一样的。按照:

1
2
3
4
ExcelReaderSheetBuilder.sheet() -> ExcelReaderSheetBuilder.build()
-> new ExcelReader(readWorkbook) -> new ExcelAnalyserImpl(readWorkbook)
-> new AnalysisContextImpl(readWorkbook) -> new ReadWorkbookHolder(readWorkbook)
-> new AbstractReadHolder()

到这里就可以找到配置Converter的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (parentAbstractReadHolder == null) {
setConverterMap(DefaultConverterLoader.loadDefaultReadConverter());
} else {
setConverterMap(new HashMap<String, Converter>(parentAbstractReadHolder.getConverterMap()));
}
if (readBasicParameter.getCustomConverterList() != null
&& !readBasicParameter.getCustomConverterList().isEmpty()) {
for (Converter converter : readBasicParameter.getCustomConverterList()) {
getConverterMap().put(
ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()),
converter);
}
}

和写过程不同,读过程通过DefaultConverterLoader.loadDefaultReadConverter()加载映射关系,加载之后可以得到由converter.supportJavaTypeKey()converter.supportExcelTypeKey()构成的key,以converter为value的map,有如下的映射列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
BigDecimal.class <- CellDataTypeEnum.BOOLEAN:   BigDecimalBooleanConverter
BigDecimal.class <- CellDataTypeEnum.NUMBER: BigDecimalNumberConverter
BigDecimal.class <- CellDataTypeEnum.STRING: BigDecimalStringConverter

Boolean.class <- CellDataTypeEnum.BOOLEAN: BooleanBooleanConverter
Boolean.class <- CellDataTypeEnum.NUMBER: BooleanNumberConverter
Boolean.class <- CellDataTypeEnum.STRING: BooleanStringConverter

Byte.class <- CellDataTypeEnum.BOOLEAN: ByteBooleanConverter
Byte.class <- CellDataTypeEnum.NUMBER: ByteNumberConverter
Byte.class <- CellDataTypeEnum.STRING: ByteStringConverter

Date.class <- CellDataTypeEnum.NUMBER: DateNumberConverter
Date.class <- CellDataTypeEnum.STRING: DateStringConverter

Double.class <- CellDataTypeEnum.BOOLEAN: DoubleBooleanConverter
Double.class <- CellDataTypeEnum.NUMBER: DoubleNumberConverter
Double.class <- CellDataTypeEnum.STRING: DoubleStringConverter

Float.class <- CellDataTypeEnum.BOOLEAN: FloatBooleanConverter
Float.class <- CellDataTypeEnum.NUMBER: FloatNumberConverter
Float.class <- CellDataTypeEnum.STRING: FloatStringConverter

Integer.class <- CellDataTypeEnum.BOOLEAN: IntegerBooleanConverter
Integer.class <- CellDataTypeEnum.NUMBER: IntegerNumberConverter
Integer.class <- CellDataTypeEnum.STRING: IntegerStringConverter

Long.class <- CellDataTypeEnum.BOOLEAN: LongBooleanConverter
Long.class <- CellDataTypeEnum.NUMBER: LongNumberConverter
Long.class <- CellDataTypeEnum.STRING: LongStringConverter

Long.class <- CellDataTypeEnum.BOOLEAN: LongBooleanConverter
Long.class <- CellDataTypeEnum.NUMBER: LongNumberConverter
Long.class <- CellDataTypeEnum.STRING: LongStringConverter

Short.class <- CellDataTypeEnum.BOOLEAN: ShortBooleanConverter
Short.class <- CellDataTypeEnum.NUMBER: ShortNumberConverter
Short.class <- CellDataTypeEnum.STRING: ShortStringConverter

String.class <- CellDataTypeEnum.BOOLEAN: StringBooleanConverter
String.class <- CellDataTypeEnum.NUMBER: StringNumberConverter
String.class <- CellDataTypeEnum.STRING: StringStringConverter

String.class <- CellDataTypeEnum.ERROR: StringErrorConverter

和写入不同,读具有更多的组合方式,excel文件中的字段类型可以有多种,对应的javaObject的属性也可以有多种。通过这样的映射关系可以确定输入数据的类型要转化为目标数据类型所需要使用到的转化器。

如果有自定义的Converter,则会使用自动定义的Conveter,则会根据supportJavaTypeKeysupportExcelTypeKey替换原来的默认的Converter。

类型转化的使用就得看ReadListener的子类的使用了。

参考和其他

源码见:tutorial/tutorial-excel
该功能已经提出issue,可以关注:希望为DateTimeFormat增加时区参数 #841

前端生成Excel的技术可以了解:

CATALOG
  1. 1. 写在前面
  2. 2. 实现方案
  3. 3. 定义 @DateTimeZone 注解
  4. 4. 实现时区转化器:Date String
  5. 5. 实现时区转化器:Date Number
  6. 6. 测试
  7. 7. 拓展 - 聊聊EasyExcel的转化器
    1. 7.1. 写过程
    2. 7.2. 读过程
  8. 8. 参考和其他