Skip to content

Commit c09387f

Browse files
authored
refactor(examples): Fesod Examples Module Refactoring (#770)
* feat(examples): refactor example module * refactor(examples): clean up test-scope examples and legacy classes/resources * refactor(examples): replace System.out with logging for file write success messages * refactor(examples): update imports to use common utility package * feat(examples): update file path retrieval to use temporary path utility * feat(ci): include fesod-examples in Maven build process * feat(dependencies): update logging dependencies and add integration test for FesodWebApplication * refactor(examples): enhance Javadoc comments for clarity and detail across multiple classes * fix: rename excel's header name * feat(examples): add CustomConverterData model and update example to use it;avoid reuse fork in example ut because of custom convertor test will overwrite global convert setting
1 parent a88534a commit c09387f

257 files changed

Lines changed: 4089 additions & 10001 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
restore-keys: |
8787
${{ runner.os }}-m2
8888
- name: Test with Maven
89-
run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet
89+
run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet,fesod-examples/fesod-sheet-examples
9090
- name: Publish Unit Test Results
9191
uses: EnricoMi/publish-unit-test-result-action@v2
9292
if: (!cancelled())

fesod-examples/fesod-sheet-examples/pom.xml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,72 @@
3333
<packaging>jar</packaging>
3434
<name>Fesod Sheet Examples</name>
3535

36+
<properties>
37+
<maven.test.skip>false</maven.test.skip>
38+
</properties>
39+
40+
<build>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.apache.maven.plugins</groupId>
44+
<artifactId>maven-surefire-plugin</artifactId>
45+
<configuration>
46+
<includes>
47+
<include>**/*Test.java</include>
48+
<include>**/*ITCase.java</include>
49+
</includes>
50+
<!--
51+
Tests that register global converters (e.g. CustomConverterExampleITCase)
52+
mutate shared static state. Run them in isolated JVM forks to prevent
53+
cross-test contamination. This follows the same pattern used by
54+
Apache Kafka and Apache Flink for tests with static side-effects.
55+
See: https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html
56+
-->
57+
<forkCount>1</forkCount>
58+
<reuseForks>false</reuseForks>
59+
</configuration>
60+
</plugin>
61+
</plugins>
62+
</build>
63+
3664
<dependencies>
65+
3766
<dependency>
3867
<groupId>org.apache.fesod</groupId>
3968
<artifactId>fesod-sheet</artifactId>
4069
<version>${project.version}</version>
4170
</dependency>
71+
72+
<dependency>
73+
<groupId>ch.qos.logback</groupId>
74+
<artifactId>logback-classic</artifactId>
75+
<version>${logback-classic.version}</version>
76+
</dependency>
77+
78+
<dependency>
79+
<groupId>org.apache.logging.log4j</groupId>
80+
<artifactId>log4j-to-slf4j</artifactId>
81+
</dependency>
82+
83+
<dependency>
84+
<groupId>com.alibaba.fastjson2</groupId>
85+
<artifactId>fastjson2</artifactId>
86+
</dependency>
87+
4288
<dependency>
4389
<groupId>org.springframework.boot</groupId>
4490
<artifactId>spring-boot-starter-web</artifactId>
91+
</dependency>
92+
93+
<dependency>
94+
<groupId>org.junit.jupiter</groupId>
95+
<artifactId>junit-jupiter</artifactId>
96+
<scope>test</scope>
97+
</dependency>
98+
99+
<dependency>
100+
<groupId>org.springframework.boot</groupId>
101+
<artifactId>spring-boot-starter-test</artifactId>
45102
<scope>test</scope>
46103
</dependency>
47104
</dependencies>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fesod.sheet.examples.advanced;
21+
22+
import java.util.ArrayList;
23+
import java.util.Date;
24+
import java.util.List;
25+
import lombok.extern.slf4j.Slf4j;
26+
import org.apache.fesod.sheet.FesodSheet;
27+
import org.apache.fesod.sheet.context.AnalysisContext;
28+
import org.apache.fesod.sheet.examples.advanced.converter.CustomStringStringConverter;
29+
import org.apache.fesod.sheet.examples.advanced.data.CustomConverterData;
30+
import org.apache.fesod.sheet.examples.util.ExampleFileUtil;
31+
import org.apache.fesod.sheet.read.listener.ReadListener;
32+
33+
/**
34+
* Demonstrates registering a custom converter at the builder level for read and write.
35+
*
36+
* <h2>Scenario</h2>
37+
* <p>You need the same data transformation applied to ALL fields of a matching type,
38+
* not just a specific annotated field. For example, adding a "Custom:" prefix to every
39+
* string column, or encrypting/decrypting all string values.</p>
40+
*
41+
* <h2>Key Concepts: Per-Field vs. Global Converter Registration</h2>
42+
* <table>
43+
* <tr><th>Approach</th><th>Scope</th><th>How</th></tr>
44+
* <tr>
45+
* <td>Per-field</td>
46+
* <td>Single field only</td>
47+
* <td>{@code @ExcelProperty(converter = MyConverter.class)}</td>
48+
* </tr>
49+
* <tr>
50+
* <td>Global (this example)</td>
51+
* <td>All fields matching Java type + Excel type</td>
52+
* <td>{@code .registerConverter(new MyConverter())} on the builder</td>
53+
* </tr>
54+
* </table>
55+
*
56+
* <h2>How It Works</h2>
57+
* <ol>
58+
* <li><b>Write:</b> The {@link CustomStringStringConverter} is registered on the write builder.
59+
* During write, every {@code String} field is transformed (prefixed with "Custom:").</li>
60+
* <li><b>Read:</b> The same converter is registered on the read builder.
61+
* During read, every string cell is transformed back through the converter.</li>
62+
* </ol>
63+
*
64+
* <h2>Converter Resolution Priority</h2>
65+
* <pre>
66+
* 1. Field-level converter (@ExcelProperty(converter = ...)) ← highest
67+
* 2. Builder-level converter (.registerConverter(...)) ← this example
68+
* 3. Built-in default converter ← lowest
69+
* </pre>
70+
*
71+
* <h2>Related Examples</h2>
72+
* <ul>
73+
* <li>{@link org.apache.fesod.sheet.examples.read.ConverterReadExample} — Per-field converter via annotation.</li>
74+
* </ul>
75+
*
76+
* @see CustomStringStringConverter
77+
* @see org.apache.fesod.sheet.converters.Converter
78+
*/
79+
@Slf4j
80+
public class CustomConverterExample {
81+
82+
public static void main(String[] args) {
83+
String fileName = ExampleFileUtil.getTempPath("customConverter" + System.currentTimeMillis() + ".xlsx");
84+
customConverterWrite(fileName);
85+
customConverterRead(fileName);
86+
}
87+
88+
/**
89+
* Writes data with a globally registered converter that prefixes all string values.
90+
*
91+
* <p>The {@link CustomStringStringConverter} transforms "String0" → "Custom:String0"
92+
* for every string field in the data model.</p>
93+
*/
94+
public static void customConverterWrite(String fileName) {
95+
FesodSheet.write(fileName, CustomConverterData.class)
96+
.registerConverter(new CustomStringStringConverter())
97+
.sheet("CustomConverter")
98+
.doWrite(data());
99+
log.info("Successfully wrote file with custom converter: {}", fileName);
100+
}
101+
102+
/**
103+
* Reads the previously written file with the same converter registered.
104+
*
105+
* <p>The converter's {@code convertToJavaData()} method is applied during read,
106+
* transforming cell values as they are parsed.</p>
107+
*/
108+
public static void customConverterRead(String fileName) {
109+
FesodSheet.read(fileName, CustomConverterData.class, new ReadListener<CustomConverterData>() {
110+
@Override
111+
public void invoke(CustomConverterData data, AnalysisContext context) {
112+
log.info("Read data with custom converter: {}", data);
113+
}
114+
115+
@Override
116+
public void doAfterAllAnalysed(AnalysisContext context) {
117+
log.info("Custom converter read completed");
118+
}
119+
})
120+
.registerConverter(new CustomStringStringConverter())
121+
.sheet()
122+
.doRead();
123+
}
124+
125+
private static List<CustomConverterData> data() {
126+
List<CustomConverterData> list = new ArrayList<>();
127+
for (int i = 0; i < 10; i++) {
128+
CustomConverterData data = new CustomConverterData();
129+
data.setString("String" + i);
130+
data.setDate(new Date());
131+
data.setDoubleData(0.56);
132+
list.add(data);
133+
}
134+
return list;
135+
}
136+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fesod.sheet.examples.advanced;
21+
22+
import java.util.ArrayList;
23+
import java.util.Date;
24+
import java.util.List;
25+
import lombok.extern.slf4j.Slf4j;
26+
import org.apache.fesod.sheet.ExcelWriter;
27+
import org.apache.fesod.sheet.FesodSheet;
28+
import org.apache.fesod.sheet.examples.util.ExampleFileUtil;
29+
import org.apache.fesod.sheet.examples.write.data.DemoData;
30+
import org.apache.fesod.sheet.util.FileUtils;
31+
import org.apache.fesod.sheet.write.handler.WorkbookWriteHandler;
32+
import org.apache.fesod.sheet.write.handler.context.WorkbookWriteHandlerContext;
33+
import org.apache.fesod.sheet.write.metadata.WriteSheet;
34+
import org.apache.poi.ss.usermodel.Workbook;
35+
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
36+
37+
/**
38+
* Demonstrates writing very large Excel files (100,000+ rows) with memory optimization.
39+
*
40+
* <h2>Scenario</h2>
41+
* <p>You need to export a large dataset (e.g., database dump, log analysis) that would
42+
* exhaust memory if all rows were held at once. Fesod uses Apache POI's streaming
43+
* API (SXSSF) internally, but temporary XML files can consume significant disk space.</p>
44+
*
45+
* <h2>Key Optimization: Temporary File Compression</h2>
46+
* <p>When POI writes large XLSX files, it creates temporary XML files on disk
47+
* (one per sheet). These can be several times larger than the final file.
48+
* Enabling compression via {@code setCompressTempFiles(true)} significantly reduces
49+
* disk usage at the cost of slightly more CPU.</p>
50+
*
51+
* <h2>Architecture</h2>
52+
* <pre>
53+
* Data (in memory, batched) Fesod POI/SXSSF
54+
* │ │ │
55+
* ├─ 100 rows batch ───────────▶ write() ──────▶ temp XML (compressed)
56+
* ├─ 100 rows batch ───────────▶ write() ──────▶ temp XML (append)
57+
* │ ... (1000 batches) │ │
58+
* └─ close() ──────────────────▶ finalize ─────▶ final .xlsx
59+
* </pre>
60+
*
61+
* <h2>Performance Tips</h2>
62+
* <ul>
63+
* <li>Use {@code ExcelWriter} (try-with-resources) for batch writing instead of
64+
* loading all data with {@code doWrite()}.</li>
65+
* <li>Enable temp file compression for disk-constrained environments.</li>
66+
* <li>Tune batch size (100 rows here) based on your row width and available memory.</li>
67+
* <li>Monitor temp directory size: {@code FileUtils.getPoiFilesPath()}.</li>
68+
* </ul>
69+
*
70+
* <h2>Expected Result</h2>
71+
* <p>Writes 100,000 rows (1000 batches x 100 rows) to a single sheet without
72+
* OutOfMemoryError, using compressed temp files on disk.</p>
73+
*
74+
* <h2>Related Examples</h2>
75+
* <ul>
76+
* <li>{@link org.apache.fesod.sheet.examples.write.BasicWriteExample} — Simple small-file write.</li>
77+
* </ul>
78+
*
79+
* @see ExcelWriter
80+
* @see org.apache.poi.xssf.streaming.SXSSFWorkbook#setCompressTempFiles(boolean)
81+
*/
82+
@Slf4j
83+
public class LargeFileWriteExample {
84+
85+
public static void main(String[] args) {
86+
compressedTemporaryFile();
87+
}
88+
89+
/**
90+
* Writes 100,000 rows in batches with compressed temporary files.
91+
*
92+
* <p>Uses a {@link WorkbookWriteHandler} to access the underlying POI
93+
* {@link SXSSFWorkbook} and enable temp file compression. Writing is done
94+
* in 1,000 batches of 100 rows each via the {@link ExcelWriter} API.</p>
95+
*/
96+
public static void compressedTemporaryFile() {
97+
log.info("Temporary XML files are stored at: {}", FileUtils.getPoiFilesPath());
98+
String fileName = ExampleFileUtil.getTempPath("largeFile" + System.currentTimeMillis() + ".xlsx");
99+
100+
try (ExcelWriter excelWriter = FesodSheet.write(fileName, DemoData.class)
101+
.registerWriteHandler(new WorkbookWriteHandler() {
102+
@Override
103+
public void afterWorkbookCreate(WorkbookWriteHandlerContext context) {
104+
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
105+
if (workbook instanceof SXSSFWorkbook) {
106+
// Enable temporary file compression.
107+
((SXSSFWorkbook) workbook).setCompressTempFiles(true);
108+
}
109+
}
110+
})
111+
.build()) {
112+
WriteSheet writeSheet = FesodSheet.writerSheet("Template").build();
113+
// Write 100,000 rows in batches.
114+
for (int i = 0; i < 1000; i++) {
115+
excelWriter.write(data(), writeSheet);
116+
}
117+
}
118+
log.info("Successfully wrote large file: {}", fileName);
119+
}
120+
121+
private static List<DemoData> data() {
122+
List<DemoData> list = new ArrayList<>();
123+
for (int i = 0; i < 100; i++) {
124+
DemoData data = new DemoData();
125+
data.setString("String" + i);
126+
data.setDate(new Date());
127+
data.setDoubleData(0.56);
128+
list.add(data);
129+
}
130+
return list;
131+
}
132+
}

0 commit comments

Comments
 (0)