Dynamic CSV Export for Large Datasets Using LinkedHashMap in Java
CSV Export Utility
The DynamicCsvExporter class handles writing CSV content directly from query result maps without requiring predefined DTOs. It relies on LinkedHashMap to preserve column ordering and retain null entries. The MyBatis mapper returns java.util.LinkedHashMap (resultType).
package com.example.util;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class DynamicCsvExporter {
private static final String DELIMITER = ",";
private static final String LINE_END = "\r\n";
/**
* Writes CSV content to the output stream.
*
* @param records data rows as LinkedHashMap list
* @param headers comma-separated header names
* @param fields comma-separated map keys
* @param out target output stream
*/
public static void writeCsv(List<LinkedHashMap<String, String>> records,
String headers, String fields, OutputStream out) throws Exception {
StringBuilder content = new StringBuilder();
String[] headerArr = headers.split(",");
String[] fieldArr = fields.split(",");
// Append header row
for (String h : headerArr) {
content.append(h).append(DELIMITER);
}
content.append(LINE_END);
// Append data rows
if (!CollectionUtils.isEmpty(records)) {
for (LinkedHashMap<String, String> row : records) {
for (String field : fieldArr) {
content.append(escapeCell(row.get(field))).append(DELIMITER);
}
content.append(LINE_END);
}
}
out.write(content.toString().getBytes("UTF-8"));
out.flush();
}
/**
* Configures HTTP response for CSV download.
*
* @param baseFileName file name without extension
* @param response HttpServletResponse
*/
public static void prepareResponse(String baseFileName, HttpServletResponse response)
throws UnsupportedEncodingException {
String timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
String fullName = baseFileName + timestamp + ".csv";
String charset = "UTF-8";
response.setContentType("application/ms-txt.numberformat:@");
response.setCharacterEncoding(charset);
response.setHeader("Pragma", "public");
response.setHeader("Cache-Control", "max-age=30");
response.setHeader("Content-Disposition", "attachment; filename="
+ URLEncoder.encode(fullName, charset));
}
private static String escapeCell(Object value) {
if (value == null) {
return "";
}
String cell = value.toString();
if (cell.contains("\"")) {
cell = cell.replace("\"", "\"\"");
}
if (cell.contains(",")) {
return "\"" + cell + "\"";
}
// prefix with tab to prevent date-format auto-conversion in some spreadsheet apps
return "\t" + cell;
}
}
MyBatis Configuration
Enable null-value mapping in MyBatis (or MyBatis-Plus) to ensure columns with null values appear in the result map:
# mybatis-plus example
mybatis-plus:
configuration:
call-setters-on-nulls: true
# plain mybatis example
mybatis:
configuration:
call-setters-on-nulls: true
Mapper XML should define resultType="java.util.LinkedHashMap" for the query that fetches export data.
Controller Endpoint
A Spring MVC controller method accepts filter criteria, queries the data base, dynamically derives column keys from the first row, and streams the CSV response.
@PostMapping(value = "/export-csv", produces = "text/plain;charset=UTF-8")
public void exportCsv(HttpServletResponse response, @RequestBody Map<String, Object> params) throws Exception {
List<LinkedHashMap<String, String>> rows = reportMapper.fetchExportData(params);
ServletOutputStream out = response.getOutputStream();
// Build comma-separated key list from first row
String keys = buildKeys(rows.get(0));
// Headers are identical to keys in this example (or can be separate mapping)
// Set response headers
DynamicCsvExporter.prepareResponse("report", response);
DynamicCsvExporter.writeCsv(rows, keys, keys, out);
}
private String buildKeys(LinkedHashMap<String, String> sample) {
StringBuilder sb = new StringBuilder();
for (String key : sample.keySet()) {
sb.append(key).append(",");
}
// Remove trailing comma
return sb.substring(0, sb.length() - 1);
}
Client-Side Dowlnoad
The frontend performs an HTTP POST, converts the response into a Blob, and triggers a file download.
this.$http
.post("/api/report/export-csv", {
startDate: this.filters.startDate,
endDate: this.filters.endDate
})
.then((res) => {
loading.close();
const blob = new Blob([res.data]);
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = 'export-' + Date.now() + '.csv';
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
window.URL.revokeObjectURL(url);
})
.catch((e) => {
loading.close();
this.$message.error(e.message);
});