如何优雅的导出csv文件

如何优雅地导出 CSV 文件

在日常开发中,我们经常需要将数据导出为 CSV 文件,以便用户下载或进行进一步的数据分析。本文将介绍如何使用 Java 泛型类优雅地实现 CSV 文件导出,同时解决列名与字段名不一致的问题,并优化大数据量下的性能。

一、需求分析

在实际开发中,我们通常需要一个通用的 CSV 导出方法,能够支持以下功能:

  1. 泛型支持:方法应支持任意类型的对象列表。

  2. 列名与字段名映射:列名可以与对象的字段名不一致,例如使用中文列名。

  3. 性能优化:在大数据量下,方法应具有良好的性能和内存占用。

二、实现通用的泛型类

1. 初始实现

最初,我们实现了一个简单的泛型方法,接受一个 List<T> 和一个包含列名的字符串(用逗号 , 分隔):

import com.opencsv.CSVWriter; // 需要引入 OpenCSV 库
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.util.List;

/*泛型类进行导出csv文件*/
public class CsvUtils {

    public static <T> ResponseEntity<byte[]> exportCsv(List<T> dataList, String headerLine) throws IllegalAccessException {
        // HTTP 响应头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.parseMediaType("text/csv"));
        headers.setContentDispositionFormData("attachment", "data.csv");

        // 创建 CSV 写入器
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        CSVWriter writer = new CSVWriter(new OutputStreamWriter(out));

        // 写入 CSV 头
        String[] headers = headerLine.split(",");
        writer.writeNext(headers);

        // 写入数据
        for (T item : dataList) {
            // 使用反射获取字段值
            String[] row = new String[headers.length];
            for (int i = 0; i < headers.length; i++) {
                try {
                    // 假设字段名与列名一致,使用反射获取字段值
                    java.lang.reflect.Field field = item.getClass().getDeclaredField(headers[i].trim());
                    field.setAccessible(true);
                    Object value = field.get(item);
                    row[i] = value != null ? value.toString() : "";
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    row[i] = "";
                }
            }
            writer.writeNext(row);
        }

        // 关闭写入器
        writer.flush();
        writer.close();

        return new ResponseEntity<>(out.toByteArray(), headers, HttpStatus.OK);
    }
}

        使用泛型 List<T>,这意味着方法需要能够处理任何对象类型的数据,只要这些对象包含可以在 CSV 中表示的字段值。这里的关键是如何从一个泛型对象中提取出字段值。嗯,这个问题让我想到,Java 的反射机制可以用来动态获取对象的字段值,虽然反射的性能不是最优,但在这种场景下应该是可以接受的,因为这只是一个导出功能。

为了使用 opencsv,需要在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>com.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>5.7.1</version>
</dependency>

2. 问题发现

在使用过程中,我们发现了一个问题:如果列名与对象的字段名不一致,反射机制将无法正确获取字段值。例如,列名可能使用中文,而字段名是英文。

3. 增加入参

为了解决这个问题,我们决定增加第三个入参,用于指定字段名与列名的映射关系。新的方法签名如下:

import com.opencsv.CSVWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CsvUtils {

    public static <T> ResponseEntity<byte[]> exportCsv(
            List<T> dataList,
            String[] headers,
            String[] fieldNames) throws IllegalAccessException {
        // 确保 headers 和 fieldNames 的长度一致
        if (headers.length != fieldNames.length) {
            throw new IllegalArgumentException("Headers and field names must have the same length");
        }

        // HTTP 响应头
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.parseMediaType("text/csv"));
        httpHeaders.setContentDispositionFormData("attachment", "data.csv");

        // 创建 CSV 写入器
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        CSVWriter writer = new CSVWriter(new OutputStreamWriter(out));

        // 写入 CSV 头
        writer.writeNext(headers);

        // 写入数据
        for (T item : dataList) {
            String[] row = new String[headers.length];
            for (int i = 0; i < headers.length; i++) {
                try {
                    // 使用反射获取字段值
                    Field field = item.getClass().getDeclaredField(fieldNames[i].trim());
                    field.setAccessible(true);
                    Object value = field.get(item);
                    row[i] = value != null ? value.toString() : "";
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    row[i] = "";
                }
            }
            writer.writeNext(row);
        }

        // 关闭写入器
        writer.flush();
        writer.close();

        return new ResponseEntity<>(out.toByteArray(), httpHeaders, HttpStatus.OK);
    }
}

三、解决列名与字段名不一致的问题

1. 使用反射获取字段值

通过增加 fieldNames 参数,我们可以在方法中使用反射获取对象的字段值。具体实现如下:

Field field = item.getClass().getDeclaredField(fieldNames[i].trim());
field.setAccessible(true);
Object value = field.get(item);

2. 支持中文列名

headers 参数允许我们使用任何字符作为列名,包括中文。这样,导出的 CSV 文件可以具有更直观的列名。

3. 调用案例

在控制器中调用 exportCsv 方法:

@GetMapping("/export-users")
public ResponseEntity<byte[]> exportUsers() throws Exception {
    List<User> users = Arrays.asList(
        new User("Alice", 25, "alice@example.com"),
        new User("Bob", 30, "bob@example.com")
    );

    String[] headers = { "姓名", "年龄", "电子邮件" };
    String[] fieldNames = { "name", "age", "userEmail" };

    return CsvUtils.exportCsv(users, headers, fieldNames);
}

功能说明

  1. 字段名映射:通过 fieldNames 参数显式指定对象的字段名,避免与列名混淆。

  2. 支持中文列名:列名可以使用任何字符,包括中文。

  3. 错误处理:如果字段不存在或无法访问,会返回空字符串。

四、性能优化

1. 字段缓存

在大数据量的情况下,每次调用 getDeclaredField 方法都会消耗大量性能。为了解决这个问题,我们引入了字段缓存机制:

private static <T> Map<String, Field> getFieldCache(Class<T> clazz, String[] fieldNames) {
    return fieldCache.computeIfAbsent(clazz, k -> Arrays.stream(fieldNames)
            .map(fieldName -> {
                try {
                    return new AbstractMap.SimpleEntry<>(fieldName, clazz.getDeclaredField(fieldName));
                } catch (NoSuchFieldException e) {
                    return new AbstractMap.SimpleEntry<>(fieldName, null);
                }
            })
            .collect(Collectors.toMap(
                    AbstractMap.SimpleEntry::getKey,
                    AbstractMap.SimpleEntry::getValue,
                    (existing, replacement) -> existing,
                    ConcurrentHashMap::new
            )));
}

2. 流处理

为了避免大数据量导致的内存不足问题,我们使用流处理逐行写入数据:

CSVWriter writer = new CSVWriter(new OutputStreamWriter(out));
writer.writeNext(headers);

for (T item : dataList) {
    String[] row = new String[headers.length];
    for (int i = 0; i < headers.length; i++) {
        Field field = fields.get(fieldNames[i]);
        if (field != null) {
            field.setAccessible(true);
            try {
                row[i] = String.valueOf(field.get(item));
            } catch (IllegalAccessException e) {
                row[i] = "";
            }
        } else {
            row[i] = "";
        }
    }
    writer.writeNext(row);
}

五、完整代码示例

以下是优化后的完整代码示例:

import com.opencsv.CSVWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class CsvUtils {

    private static final Map<Class<?>, Map<String, Field>> fieldCache = new ConcurrentHashMap<>();

    public static <T> ResponseEntity<byte[]> exportCsv(
            List<T> dataList,
            String[] headers,
            String[] fieldNames) throws IllegalAccessException {
        if (headers.length != fieldNames.length) {
            throw new IllegalArgumentException("列名和字段名数组必须有相同的长度");
        }

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.parseMediaType("text/csv"));
        httpHeaders.setContentDispositionFormData("attachment", "data.csv");

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        CSVWriter writer = new CSVWriter(new OutputStreamWriter(out));
        writer.writeNext(headers);

        Map<String, Field> fields = getFieldCache(dataList.get(0).getClass(), fieldNames);

        for (T item : dataList) {
            String[] row = new String[headers.length];
            for (int i = 0; i < headers.length; i++) {
                Field field = fields.get(fieldNames[i]);
                if (field != null) {
                    field.setAccessible(true);
                    try {
                        row[i] = String.valueOf(field.get(item));
                    } catch (IllegalAccessException e) {
                        row[i] = "";
                    }
                } else {
                    row[i] = "";
                }
            }
            writer.writeNext(row);
        }

        writer.flush();
        writer.close();

        return new ResponseEntity<>(out.toByteArray(), httpHeaders, HttpStatus.OK);
    }

    private static <T> Map<String, Field> getFieldCache(Class<T> clazz, String[] fieldNames) {
        return fieldCache.computeIfAbsent(clazz, k -> Arrays.stream(fieldNames)
                .map(fieldName -> {
                    Field field = null;
                    try {
                        field = clazz.getDeclaredField(fieldName);
                    } catch (NoSuchFieldException e) {
                        // Handle exception, possibly log or ignore
                    }
                    return new AbstractMap.SimpleEntry<>(fieldName, field);
                })
                .collect(Collectors.toMap(
                        AbstractMap.SimpleEntry::getKey,
                        AbstractMap.SimpleEntry::getValue,
                        (existing, replacement) -> existing,
                        ConcurrentHashMap::new
                )));
    }
}

六、总结

通过以上优化,我们实现了一个通用的 CSV 导出方法,支持列名与字段名的灵活映射,并在大数据量下具有良好的性能。希望本文对你的开发工作有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值