OpenCSV:如何使用自定义列标题和自定义列位置从 POJO 创建 CSV 文件?

Posted

技术标签:

【中文标题】OpenCSV:如何使用自定义列标题和自定义列位置从 POJO 创建 CSV 文件?【英文标题】:OpenCSV: How to create CSV file from POJO with custom column headers and custom column positions? 【发布时间】:2017-12-25 12:15:30 【问题描述】:

我创建了一个 MappingsBean 类,其中指定了 CSV 文件的所有列。接下来我解析 XML 文件并创建一个映射bean 列表。然后我将该数据写入 CSV 文件作为报告。

我正在使用以下注释:

public class MappingsBean 

    @CsvBindByName(column = "TradeID")
    @CsvBindByPosition(position = 0)
    private String tradeId;

    @CsvBindByName(column = "GWML GUID", required = true)
    @CsvBindByPosition(position = 1)
    private String gwmlGUID;

    @CsvBindByName(column = "MXML GUID", required = true)
    @CsvBindByPosition(position = 2)
    private String mxmlGUID;

    @CsvBindByName(column = "GWML File")
    @CsvBindByPosition(position = 3)
    private String gwmlFile;

    @CsvBindByName(column = "MxML File")
    @CsvBindByPosition(position = 4)
    private String mxmlFile;

    @CsvBindByName(column = "MxML Counterparty")
    @CsvBindByPosition(position = 5)
    private String mxmlCounterParty;

    @CsvBindByName(column = "GWML Counterparty")
    @CsvBindByPosition(position = 6)
    private String gwmlCounterParty;

然后我使用 StatefulBeanToCsv 类写入 CSV 文件:

File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
StatefulBeanToCsv<MappingsBean> beanToCsv = new 
                              StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();

这种方法的问题是如果我使用@CsvBindByPosition(position = 0)来控制 位置然后我无法生成列名。如果我使用@CsvBindByName(column = "TradeID"),则无法设置列的位置。

有没有一种方法可以同时使用这两种注释,以便创建带有列标题的 CSV 文件并控制列位置?

问候, 维克拉姆·帕塔尼亚

【问题讨论】:

This 示例应该对您有所帮助。 嗨 Rao,这个链接失效了 正确检查。它正在工作。 它不再工作了...... 【参考方案1】:

我也遇到过类似的问题。 AFAIK 在 OpenCSV 中没有内置功能允许使用自定义列名 顺序将 bean 写入 CSV。

有两个主要的MappingStrategyies 在 OpenCSV 中可用:

HeaderColumnNameMappingStrategy:允许根据自定义名称将 CVS 文件列映射到 bean 字段;将 bean 写入 CSV 时,这允许更改列标题名称,但我们无法控制列顺序 ColumnPositionMappingStrategy:允许根据列顺序将 CSV 文件列映射到 bean 字段;将 bean 写入 CSV 时,我们可以控制列顺序,但我们得到一个空标题(实现返回 new String[0] 作为标题)

我发现实现自定义列名和排序的唯一方法是编写您的自定义MappingStrategy

第一个解决方案:快速简单但硬编码

创建自定义MappingStrategy

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
    private static final String[] HEADER = new String[]"TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty";

    @Override
    public String[] generateHeader() 
        return HEADER;
    

并在StatefulBeanToCsvBuilder中使用它:

final CustomMappingStrategy<MappingsBean> mappingStrategy = new CustomMappingStrategy<>();
mappingStrategy.setType(MappingsBean.class);

final StatefulBeanToCsv<MappingsBean> beanToCsv = new StatefulBeanToCsvBuilder<MappingsBean>(writer)
    .withMappingStrategy(mappingStrategy)
    .build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close()

MappingsBean 类中,我们留下了CsvBindByPosition 注释 - 以控制排序(在此解决方案中,不需要CsvBindByName 注释)。由于自定义映射策略,标题列名称包含在生成的 CSV 文件中。

此解决方案的缺点是,当我们通过 CsvBindByPosition 注释更改列顺序时,我们还必须手动更改自定义映射策略中的 HEADER 常量。

第二种方案:更灵活

第一个解决方案有效,但对我不利。基于MappingStrategy 的内置实现,我想出了另一个实现:

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
    @Override
    public String[] generateHeader() 
        final int numColumns = findMaxFieldIndex();
        if (!isAnnotationDriven() || numColumns == -1) 
            return super.generateHeader();
        

        header = new String[numColumns + 1];

        BeanField beanField;
        for (int i = 0; i <= numColumns; i++) 
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        
        return header;
    

    private String extractHeaderName(final BeanField beanField) 
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) 
            return StringUtils.EMPTY;
        

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    

您可以在StatefulBeanToCsvBuilder 中使用此自定义策略,这与在第一个解决方案中完全相同(记得调用mappingStrategy.setType(MappingsBean.class);,否则此解决方案将不起作用)。

目前我们的MappingsBean 必须同时包含CsvBindByNameCsvBindByPosition 注释。第一个给出标题列名称,第二个在输出 CSV 标题中创建列的顺序。现在,如果我们更改(使用注释)列名或 MappingsBean 类中的排序 - 该更改将反映在输出 CSV 文件中。

【讨论】:

其实 ColumnPositionMappingStrategy 有 setColumnMapping(String... columnMapping) method() 来解决这个问题。您必须在 setType() 之前调用它。无论如何,这很愚蠢,因为它已经由注释定义。我检查了 ColumnPositionMappingStrategy 类,它扩展了 HeaderColumnNameMappingStrategy!因此,两种类型的注释都必须有效(至少这个想法是编程的)。如果不是,这是一个错误。 @SimonLogic 恐怕你的建议对我不起作用。 :\ 您是否能够实际验证这一点,或者您只是从文档中推断出来的?如果你碰巧有一个 Github 示例,我有兴趣参考它,看看我缺少什么。谢谢! :) @sebast26 我可以确认 OP 的两种解决方案都对我有用。非常感谢您提供的出色细节!我会用你的动态解决方案,谢谢。 解决方案完美运行,对于更新版本,请参阅 Lalji Gajera 的回答 第一个解决方案不适用于 4.5 版本。为了使它工作添加 super.generateHeader(bean);作为覆盖的 generateHeader(T bean) 中的第一行【参考方案2】:

如果您没有 getDeclaredAnnotationsByType 方法,但需要您的原始字段名称的名称:

beanField.getField().getName()

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
@Override
public String[] generateHeader() 
    final int numColumns = findMaxFieldIndex();
    if (!isAnnotationDriven() || numColumns == -1) 
        return super.generateHeader();
    

    header = new String[numColumns + 1];

    BeanField beanField;
    for (int i = 0; i <= numColumns; i++) 
        beanField = findField(i);
        String columnHeaderName = extractHeaderName(beanField);
        header[i] = columnHeaderName;
    
    return header;


private String extractHeaderName(final BeanField beanField) 
    if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotations().length == 0) 
        return StringUtils.EMPTY;
    
    return beanField.getField().getName();

【讨论】:

【参考方案3】:

更正上述答案以匹配新版本。

package csvpojo;

import org.apache.commons.lang3.StringUtils;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 

super.setColumnMapping(new String[ FieldUtils.getAllFields(bean.getClass()).length]);
        final int numColumns = findMaxFieldIndex();
        if (!isAnnotationDriven() || numColumns == -1) 
            return super.generateHeader(bean);
        

        String[] header = new String[numColumns + 1];

        BeanField<T> beanField;
        for (int i = 0; i <= numColumns; i++) 
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        
        return header;
    

    private String extractHeaderName(final BeanField<T> beanField) 
        if (beanField == null || beanField.getField() == null
                || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) 
            return StringUtils.EMPTY;
        

        final CsvBindByName bindByNameAnnotation = beanField.getField()
                .getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    

然后调用它来生成 CSV。我已将访客用作我的 POJO,以便在必要时进行填充和更新。

        CustomMappingStrategy<Visitors> mappingStrategy = new CustomMappingStrategy<>();
        mappingStrategy.setType(Visitors.class);
        // writing sample
        List<Visitors> beans2 = new ArrayList<Visitors>();

        Visitors v = new Visitors();
        v.set_1_firstName(" test1");
        v.set_2_lastName("lastname1");
        v.set_3_visitsToWebsite("876");
        beans2.add(v);

        v = new Visitors();
        v.set_1_firstName(" firstsample2");
        v.set_2_lastName("lastname2");
        v.set_3_visitsToWebsite("777");
        beans2.add(v);

        Writer writer = new FileWriter("G://output.csv");
        StatefulBeanToCsv<Visitors> beanToCsv = new StatefulBeanToCsvBuilder<Visitors>(writer)
                .withMappingStrategy(mappingStrategy).withSeparator(',').withApplyQuotesToAll(false).build();
        beanToCsv.write(beans2);
        writer.close();

我的 bean 注释看起来像这样

 @CsvBindByName (column = "First Name", required = true)
 @CsvBindByPosition(position=1)
 private String firstName;


 @CsvBindByName (column = "Last Name", required = true)
 @CsvBindByPosition(position=0)
 private String lastName;

【讨论】:

Sebast26 答案不适用于 4.3,但确实如此。谢谢! 使用新版本更好地工作(使用 4.5) 这适用于我的 opencsv 版本 4.5,谢谢。 不适用于最新的 5.0 版本。 ColumnPositionMappingStrategy & BeanField 中没有方法 findMaxFieldIndex() 被概括为 BeanField 上述代码在 opencsv-4.5 中工作以获取列名和位置。谢谢【参考方案4】:

感谢这个线程,它对我来说真的很有用......我已经对提供的解决方案进行了一些改进,以便也接受某些字段没有注释的 POJO(不打算读/写):

public class ColumnAndNameMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 

@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 

    super.setColumnMapping(new String[ getAnnotatedFields(bean)]);
    final int numColumns = getAnnotatedFields(bean);
    final int totalFieldNum = findMaxFieldIndex();
    if (!isAnnotationDriven() || numColumns == -1) 
        return super.generateHeader(bean);
    

    String[] header = new String[numColumns];

    BeanField<T> beanField;
    for (int i = 0; i <= totalFieldNum; i++) 
        beanField = findField(i);
        if (isFieldAnnotated(beanField.getField())) 
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        
    
    return header;


private int getAnnotatedFields(T bean) 
    return (int) Arrays.stream(FieldUtils.getAllFields(bean.getClass()))
            .filter(this::isFieldAnnotated)
            .count();


private boolean isFieldAnnotated(Field f) 
    return f.isAnnotationPresent(CsvBindByName.class) || f.isAnnotationPresent(CsvCustomBindByName.class);


private String extractHeaderName(final BeanField beanField) 
    if (beanField == null || beanField.getField() == null) 
        return StringUtils.EMPTY;
    

    Field field = beanField.getField();

    if (field.getDeclaredAnnotationsByType(CsvBindByName.class).length != 0) 
        final CsvBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    

    if (field.getDeclaredAnnotationsByType(CsvCustomBindByName.class).length != 0) 
        final CsvCustomBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0];
        return bindByNameAnnotation.column();
    

    return StringUtils.EMPTY;

【讨论】:

【参考方案5】:

我想实现双向导入/导出 - 能够将生成的 CSV 导入回 POJO,反之亦然。

我无法为此使用@CsvBindByPosition,因为在这种情况下 - 自动选择了 ColumnPositionMappingStrategy。每个文件:this strategy requires that the file does NOT have a header。

我用来实现目标的方法:

HeaderColumnNameMappingStrategy
mappingStrategy.setColumnOrderOnWrite(Comparator<String> writeOrder)

用于读取/写入 csv 的 CsvUtils

import com.opencsv.CSVWriter;
import com.opencsv.bean.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.List;

public class CsvUtils 
    private CsvUtils() 
    

    public static <T> String convertToCsv(List<T> entitiesList, MappingStrategy<T> mappingStrategy) throws Exception 
        try (Writer writer = new StringWriter()) 
            StatefulBeanToCsv<T> beanToCsv = new StatefulBeanToCsvBuilder<T>(writer)
                    .withMappingStrategy(mappingStrategy)
                    .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                    .build();
            beanToCsv.write(entitiesList);
            return writer.toString();
        
    

    @SuppressWarnings("unchecked")
    public static <T> List<T> convertFromCsv(MultipartFile file, Class clazz) throws IOException 
        try (Reader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) 
            CsvToBean<T> csvToBean = new CsvToBeanBuilder<T>(reader).withType(clazz).build();
            return csvToBean.parse();
        
    

用于导入/导出的 POJO

public class LocalBusinessTrainingPairDTO 
    //this is used for CSV columns ordering on exporting LocalBusinessTrainingPairs
    public static final String[] FIELDS_ORDER = "leftId", "leftName", "rightId", "rightName";

    @CsvBindByName(column = "leftId")
    private int leftId;

    @CsvBindByName(column = "leftName")
    private String leftName;

    @CsvBindByName(column = "rightId")
    private int rightId;

    @CsvBindByName(column = "rightName")
    private String rightName;
    // getters/setters omitted, do not forget to add them

用于预定义字符串排序的自定义比较器:

public class OrderedComparatorIgnoringCase implements Comparator<String> 
    private List<String> predefinedOrder;

    public OrderedComparatorIgnoringCase(String[] predefinedOrder) 
        this.predefinedOrder = new ArrayList<>();
        for (String item : predefinedOrder) 
            this.predefinedOrder.add(item.toLowerCase());
        
    

    @Override
    public int compare(String o1, String o2) 
        return predefinedOrder.indexOf(o1.toLowerCase()) - predefinedOrder.indexOf(o2.toLowerCase());
    

POJO 的有序写作(对初始问题的回答)

public static void main(String[] args) throws Exception 
     List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairsDTO = new ArrayList<>();
     LocalBusinessTrainingPairDTO localBusinessTrainingPairDTO = new LocalBusinessTrainingPairDTO();
     localBusinessTrainingPairDTO.setLeftId(1);
     localBusinessTrainingPairDTO.setLeftName("leftName");
     localBusinessTrainingPairDTO.setRightId(2);
     localBusinessTrainingPairDTO.setRightName("rightName");

     localBusinessTrainingPairsDTO.add(localBusinessTrainingPairDTO);

     //Creating HeaderColumnNameMappingStrategy
     HeaderColumnNameMappingStrategy<LocalBusinessTrainingPairDTO> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
     mappingStrategy.setType(LocalBusinessTrainingPairDTO.class);
     //Setting predefined order using String comparator
     mappingStrategy.setColumnOrderOnWrite(new OrderedComparatorIgnoringCase(LocalBusinessTrainingPairDTO.FIELDS_ORDER));
     String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);
     System.out.println(csv);

将导出的 CSV 读回 POJO(原始答案的补充)

重要提示:CSV 可以是无序的,因为我们仍在使用名称绑定:

public static void main(String[] args) throws Exception 
    //omitted code from writing
    String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);

    //Exported CSV should be compatible for further import
    File temp = File.createTempFile("tempTrainingPairs", ".csv");
    temp.deleteOnExit();
    BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
    bw.write(csv);
    bw.close();
    MultipartFile multipartFile = new MockMultipartFile("tempTrainingPairs.csv", new FileInputStream(temp));

    List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairDTOList = convertFromCsv(multipartFile, LocalBusinessTrainingPairDTO.class);

总结:

    无论列顺序如何,我们都可以将 CSV 读取到 POJO - 因为我们是 使用@CsvBindByName 我们可以控制写入时使用的列顺序 自定义比较器

【讨论】:

【参考方案6】:

试试下面的方法:

private static class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 

    String[] header;

    public CustomMappingStrategy(String[] cols) 
        header = cols;
    

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
        return header;
    

然后按如下方式使用:

String[] columns = new String[]"Name", "Age", "Company", "Salary";
        CustomMappingStrategy<Employee> mappingStrategy = new CustomMappingStrategy<Employee>(columns);

其中 columns 是你的 bean 的列,Employee 是你的 bean

【讨论】:

【参考方案7】:

如果您只想根据成员变量在模型类中出现的顺序(本例中为CsvRow 行)对 CSV 列进行排序,那么您可以使用Comparator 实现来解决此问题一种比较简单的方式。这是一个在 Kotlin 中执行此操作的示例:

class ByMemberOrderCsvComparator : Comparator<String> 

    private val memberOrder by lazy 
        FieldUtils.getAllFields(CsvRow::class.java)
                .map  it.getDeclaredAnnotation(CsvBindByName::class.java) 
                .map  it?.column ?: "" 
                .map  it.toUpperCase(Locale.US)  // OpenCSV UpperCases all headers, so we do this to match
    

    override fun compare(field1: String?, field2: String?): Int 
        return memberOrder.indexOf(field1) - memberOrder.indexOf(field2)
    


这个Comparator 执行以下操作:

    获取我们数据类中的每个成员变量字段 (CsvRow) 查找所有带有@CsvBindByName 注释的对象(按照您在CsvRow 模型中指定的顺序) 每个大写都匹配默认的 OpenCsv 实现

接下来,将此Comparator 应用到您的MappingStrategy,这样它将根据指定的顺序进行排序:

val mappingStrategy = HeaderColumnNameMappingStrategy<OrderSummaryCsvRow>()
mappingStrategy.setColumnOrderOnWrite(ByMemberOrderCsvComparator())
mappingStrategy.type = CsvRow::class.java
mappingStrategy.setErrorLocale(Locale.US)

val csvWriter = StatefulBeanToCsvBuilder<OrderSummaryCsvRow>(writer)
                    .withMappingStrategy(mappingStrategy)
                    .build()

作为参考,这里有一个示例 CsvRow 类(您可以根据需要将其替换为您自己的模型):

data class CsvRow(
    @CsvBindByName(column = "Column 1")
    val column1: String,

    @CsvBindByName(column = "Column 2")
    val column2: String,

    @CsvBindByName(column = "Column 3")
    val column3: String,

    // Other columns here ...
)

这会产生如下的 CSV:

"COLUMN 1","COLUMN 2","COLUMN 3",...
"value 1a","value 2a","value 3a",...
"value 1b","value 2b","value 3b",...

这种方法的好处是它无需对任何列名进行硬编码,如果您需要添加/删除列,这将大大简化事情。

【讨论】:

【参考方案8】:

我在使用最新版本的opencsv (4.6) 时删除了对已弃用 API 的所有引用,从而改进了以前的答案。

通用 Kotlin 解决方案

/**
 * Custom OpenCSV [ColumnPositionMappingStrategy] that allows for a header line to be generated from a target CSV
 * bean model class using the following annotations when present:
 * * [CsvBindByName]
 * * [CsvCustomBindByName]
 */
class CustomMappingStrategy<T>(private val beanType: Class<T>) : ColumnPositionMappingStrategy<T>() 
    init 
        setType(beanType)
        setColumnMapping(*getAnnotatedFields().map  it.extractHeaderName() .toTypedArray())
    

    override fun generateHeader(bean: T): Array<String> = columnMapping

    private fun getAnnotatedFields() = beanType.declaredFields.filter  it.isAnnotatedByName() .toList()

    private fun Field.isAnnotatedByName() = isAnnotationPresent(CsvBindByName::class.java) || isAnnotationPresent(CsvCustomBindByName::class.java)

    private fun Field.extractHeaderName() =
        getAnnotation(CsvBindByName::class.java)?.column ?: getAnnotation(CsvCustomBindByName::class.java)?.column ?: EMPTY

然后按如下方式使用:

private fun csvBuilder(writer: Writer) =
    StatefulBeanToCsvBuilder<MappingsBean>(writer)
        .withSeparator(ICSVWriter.DEFAULT_SEPARATOR)
        .withMappingStrategy(CustomMappingStrategy(MappingsBean::class.java))
        .withApplyQuotesToAll(false)
        .build()

// Kotlin try-with-resources construct
PrintWriter(File("$reportOutputDir/$REPORT_FILENAME")).use  writer ->
    csvBuilder(writer).write(makeFinalMappingBeanList())

为了完整起见,这里将 CSV bean 作为 Kotlin 数据类:

data class MappingsBean(
    @field:CsvBindByName(column = "TradeID")
    @field:CsvBindByPosition(position = 0, required = true)
    private val tradeId: String,

    @field:CsvBindByName(column = "GWML GUID", required = true)
    @field:CsvBindByPosition(position = 1)
    private val gwmlGUID: String,

    @field:CsvBindByName(column = "MXML GUID", required = true)
    @field:CsvBindByPosition(position = 2)
    private val mxmlGUID: String,

    @field:CsvBindByName(column = "GWML File")
    @field:CsvBindByPosition(position = 3)
    private val gwmlFile: String? = null,

    @field:CsvBindByName(column = "MxML File")
    @field:CsvBindByPosition(position = 4)
    private val mxmlFile: String? = null,

    @field:CsvBindByName(column = "MxML Counterparty")
    @field:CsvBindByPosition(position = 5)
    private val mxmlCounterParty: String? = null,

    @field:CsvBindByName(column = "GWML Counterparty")
    @field:CsvBindByPosition(position = 6)
    private val gwmlCounterParty: String? = null
)

【讨论】:

【参考方案9】:

通用类的CustomMappingStrategy。

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
   @Override
   public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 

       super.setColumnMapping(new String[ FieldUtils.getAllFields(bean.getClass()).length]);
       final int numColumns = findMaxFieldIndex();
       if (!isAnnotationDriven() || numColumns == -1) 
           return super.generateHeader(bean);
       

       String[] header = new String[numColumns + 1];

       BeanField<T> beanField;
       for (int i = 0; i <= numColumns; i++) 
           beanField = findField(i);
           String columnHeaderName = extractHeaderName(beanField);
           header[i] = columnHeaderName;
       
       return header;
   

   private String extractHeaderName(final BeanField<T> beanField) 
       if (beanField == null || beanField.getField() == null
               || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) 
           return StringUtils.EMPTY;
       

       final CsvBindByName bindByNameAnnotation = beanField.getField()
               .getDeclaredAnnotationsByType(CsvBindByName.class)[0];
       return bindByNameAnnotation.column();
   

POJO 类

 public class Customer

     @CsvBindByPosition(position=1)
     @CsvBindByName(column="CUSTOMER", required = true)
     private String customer;

客户端类

 List<T> data = getEmployeeRecord();
CustomMappingStrategy custom = new CustomMappingStrategy();
custom.setType(Employee.class);
StatefulBeanToCsv<T> writer = new StatefulBeanToCsvBuilder<T>(response.getWriter())
                .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                .withSeparator('|')
                .withOrderedResults(false)
                .withMappingStrategy(custom)
                .build();
        writer.write(reportData);

【讨论】:

【参考方案10】:

4.3以上版本的解决方案:

public class MappingBean 
     @CsvBindByName(column = "column_a")
     private String columnA;
     @CsvBindByName(column = "column_b")
     private String columnB;
     @CsvBindByName(column = "column_c")
     private String columnC;

     // getters and setters

并以此为例:

import org.apache.commons.collections4.comparators.FixedOrderComparator;

...

var mappingStrategy = new HeaderColumnNameMappingStrategy<MappingBean>();
mappingStrategy.setType(MappingBean.class);        
mappingStrategy.setColumnOrderOnWrite(new FixedOrderComparator<>("COLUMN_C", "COLUMN_B", "COLUMN_A"));

var sbc = new StatefulBeanToCsvBuilder<MappingBean>(writer)
      .withMappingStrategy(mappingStrategy)
      .build();

结果:

column_c |列_b | column_a

【讨论】:

【参考方案11】:

在最新版本中@Sebast26 的解决方案不再起作用。不过基本还是很不错的。这是 v5.0 的有效解决方案

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.lang3.StringUtils;

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
        final int numColumns = getFieldMap().values().size();
        super.generateHeader(bean);

        String[] header = new String[numColumns];

        BeanField beanField;
        for (int i = 0; i < numColumns; i++) 
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        
        return header;
    

    private String extractHeaderName(final BeanField beanField) 
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(
                CsvBindByName.class).length == 0) 
            return StringUtils.EMPTY;
        

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    

模型看起来像这样:

@CsvBindByName(column = "id")
@CsvBindByPosition(position = 0)
private Long id;
@CsvBindByName(column = "name")
@CsvBindByPosition(position = 1)
private String name;

我的生成助手看起来像这样:

public static <T extends AbstractCsv> String createCsv(List<T> data, Class<T> beanClazz) 
    CustomMappingStrategy<T> mappingStrategy = new CustomMappingStrategy<T>();
    mappingStrategy.setType(beanClazz);

    StringWriter writer = new StringWriter();
    String csv = "";
    try 
        StatefulBeanToCsv sbc = new StatefulBeanToCsvBuilder(writer)
                .withSeparator(';')
                .withMappingStrategy(mappingStrategy)
                .build();
        sbc.write(data);
        csv = writer.toString();
     catch (CsvRequiredFieldEmptyException e) 
        // TODO add some logging...
     catch (CsvDataTypeMismatchException e) 
        // TODO add some logging...
     finally 
        try 
            writer.close();
         catch (IOException e) 
        
    
    return csv;

【讨论】:

【参考方案12】:

我认为处理标题列顺序的预期和最灵活的方法是通过 HeaderColumnNameMappinStrategy.setColumnOrderOnWrite() 注入一个比较器。

对我来说,最直观的方法是按照在 CsvBean 中指定的顺序编写 CSV 列,但您也可以调整比较器以在指定顺序的位置使用您自己的注释。然后不要忘记重命名 Comparator 类;)

整合:

HeaderColumnNameMappingStrategy<MyCsvBean> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
    mappingStrategy.setType(MyCsvBean.class);
    mappingStrategy.setColumnOrderOnWrite(new ClassFieldOrderComparator(MyCsvBean.class));

比较器:

private class ClassFieldOrderComparator implements Comparator<String> 

    List<String> fieldNamesInOrderWithinClass;

    public ClassFieldOrderComparator(Class<?> clazz) 
        fieldNamesInOrderWithinClass = Arrays.stream(clazz.getDeclaredFields())
                .filter(field -> field.getAnnotation(CsvBindByName.class) != null)
              // Handle order by your custom annotation here
              //.sorted((field1, field2) -> 
              //   int field1Order = field1.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
              //   int field2Order = field2.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
              //   return Integer.compare(field1Order, field2Order);
              //)
                .map(field -> field.getName().toUpperCase())
                .collect(Collectors.toList());
    

    @Override
    public int compare(String o1, String o2) 
        int fieldIndexo1 = fieldNamesInOrderWithinClass.indexOf(o1);
        int fieldIndexo2 = fieldNamesInOrderWithinClass.indexOf(o2);
        return Integer.compare(fieldIndexo1, fieldIndexo2);
    

【讨论】:

【参考方案13】:

很棒的线程,我的 pojo 中没有任何注释,这就是我根据之前的所有答案所做的。希望对其他人有所帮助。

OpenCsv 版本:5.0 列出 readVendors = getFromMethod(); String[] 字段= "id","re​​cordNumber","finVendorIdTb","finVenTechIdTb","finShortNameTb","finVenName1Tb","finVenName2Tb";

            String[] csvHeader= "Id#","Shiv Record Number","Shiv Vendor Id","Shiva Tech Id#","finShortNameTb","finVenName1Tb","finVenName2Tb";

            CustomMappingStrategy<FinVendor> mappingStrategy = new CustomMappingStrategy(csvHeader);//csvHeader as per custom header irrespective of pojo field name
            mappingStrategy.setType(FinVendor.class);
            mappingStrategy.setColumnMapping(fields);//pojo mapping fields


            StatefulBeanToCsv<FinVendor> beanToCsv = new StatefulBeanToCsvBuilder<FinVendor>(writer).withQuotechar(CSVWriter.NO_QUOTE_CHARACTER).withMappingStrategy(mappingStrategy).build();
            beanToCsv.write(readVendors);

//许多用户在线程中提到的自定义映射类 私有静态类 CustomMappingStrategy 扩展 ColumnPositionMappingStrategy

                    String[] header;

                    public CustomMappingStrategy(String[] cols) 

                            header = cols;
                    

                    @Override
                    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
                        super.generateHeader(bean);
                            return header;
                    
                    

输出:

Id#     Shiv Record Number      Shiv Vendor Id   Fin Tech Id#      finShortNameTb  finVenName1Tb   finVenName2Tb   finVenDefaultLocTb
1       VEN00053                678             33316025986        THE ssOHIO S_2  THE UNIVERSITY     CHK         Test
2       VEN02277                1217            3044374205         Fe3 MECHA_1     FR3INC             EFT-1
3       VEN03118                1310            30234484121        PE333PECTUS_1   PER332CTUS AR      EFT-1       Test

【讨论】:

【参考方案14】:

以下解决方案适用于 opencsv 5.0。

首先,您需要继承 ColumnPositionMappingStrategy 类并重写 generateHeader 方法来创建自定义标头,以便同时使用 CsvBindByName 和 CsvBindByPosition 注释,如下所示。

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

/**
 * @param <T>
 */
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
    /*
     * (non-Javadoc)
     * 
     * @see com.opencsv.bean.ColumnPositionMappingStrategy#generateHeader(java.lang.
     * Object)
     */
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
        final int numColumns = getFieldMap().values().size();
        if (numColumns == -1) 
            return super.generateHeader(bean);
        

        String[] header = new String[numColumns];
        super.setColumnMapping(header);

        BeanField<T, Integer> beanField;
        for (int i = 0; i < numColumns; i++) 
            beanField = findField(i);
            String columnHeaderName = beanField.getField().getDeclaredAnnotation(CsvBindByName.class).column();
            header[i] = columnHeaderName;
        
        return header;
    

下一步是在将 bean 写入 CSV 时使用此映射策略,如下所示。

CustomMappingStrategy<ScanReport> strategy = new CustomMappingStrategy<>();
            strategy.setType(ScanReport.class);

// Write a bean to csv file.
StatefulBeanToCsv<ScanReport> beanToCsv = new StatefulBeanToCsvBuilder<ScanReport>(writer)
                    .withMappingStrategy(strategy).build();
beanToCsv.write(beanList);

【讨论】:

【参考方案15】:

以下内容适用于我将 POJO 映射到具有自定义列定位和自定义列标题的 CSV 文件(使用 opencsv-5.0 测试):

public class CustomBeanToCSVMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 

        String[] headersAsPerFieldName = getFieldMap().generateHeader(bean); // header name based on field name

        String[] header = new String[headersAsPerFieldName.length];

        for (int i = 0; i <= headersAsPerFieldName.length - 1; i++) 

            BeanField beanField = findField(i);

            String columnHeaderName = extractHeaderName(beanField); // header name based on @CsvBindByName annotation

            if (columnHeaderName.isEmpty()) // No @CsvBindByName is present
                columnHeaderName = headersAsPerFieldName[i]; // defaults to header name based on field name

            header[i] = columnHeaderName;
        

        headerIndex.initializeHeaderIndex(header);

        return header;
    

    private String extractHeaderName(final BeanField beanField) 
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) 
            return StringUtils.EMPTY;
        

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    

波乔

生成的 CSV 文件中的列定位:

生成的 CSV 文件中的列定位将根据注释 @CsvBindByPosition

生成的 CSV 文件中的标题名称:

如果该字段有@CsvBindByName,则生成的标题将按照annonation

如果字段没有@CsvBindByName,则生成的标题将根据字段名称

@Getter @Setter @ToString
public class Pojo 

    @CsvBindByName(column="Voucher Series") // header: "Voucher Series"
    @CsvBindByPosition(position=0)
    private String voucherSeries;

    @CsvBindByPosition(position=1) // header: "salePurchaseType"
    private String salePurchaseType;

使用上述自定义映射策略:

CustomBeanToCSVMappingStrategy<Pojo> mappingStrategy = new CustomBeanToCSVMappingStrategy<>();
            mappingStrategy.setType(Pojo.class);

StatefulBeanToCsv<Pojo> beanToCsv = new StatefulBeanToCsvBuilder<Pojo>(writer)
                    .withSeparator(CSVWriter.DEFAULT_SEPARATOR)
                    .withMappingStrategy(mappingStrategy)
                    .build();

beanToCsv.write(pojoList);

【讨论】:

这仍然适用于 OpenCSV 5.2。由于 OpenCSV 的向后兼容性差,大多数提出的其他解决方案甚至无法针对 5.2 进行编译。 我试过这个,但是当我从 csv 读取时,我在 csvbindbyname 和 customcsvbindbyname(带转换器)中设置的元素值没有被应用。 适用于 opencsv 5.4。顺便说一句,有没有办法告诉opencsv:“这是我想要前两列的顺序,但其余的,按照你想要的任何顺序放置,但确保你全部使用它们”。我只关心要先排序的几列,不想记住注释所有其他 bean 字段。 @VahidPazirandeh 在这种自定义情况下,我会使用“opencsv”来解析标题行和数据行。从那时起,我将手动将各个列映射到我的对象字段。我不确定“opencsv”是否提供了这种级别的定制。即使是这样,我相信它会使代码比手动执行复杂得多。【参考方案16】:

sebast26 的第一个解决方案对我有用,但对于 opencsv 5.2 版,它需要对 CustomMappingStrategy 类进行一些更改:

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> 
private static final String[] HEADER = new String[]"TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty";

@Override
public String[] generateHeader() 
    super.generateHeader(bean); // without this the file contains ONLY headers
    return HEADER;

【讨论】:

【参考方案17】:

5.2 版本还有另一个版本,因为我在尝试上述答案时遇到了 @CsvCustomBindByName 注释问题。

我定义了自定义注解:

@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface CsvPosition 

  int position();


和自定义映射策略

public class CustomMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> 

  private final Field[] fields;

  public CustomMappingStrategy(Class<T> clazz) 
    fields = clazz.getDeclaredFields();
    Arrays.sort(fields, (f1, f2) -> 
      CsvPosition position1 = f1.getAnnotation(CsvPosition.class);
      CsvPosition position2 = f2.getAnnotation(CsvPosition.class);
      return Integer.compare(position1.position(), position2.position());
    );
  

  @Override
  public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
    String[] header = new String[fields.length];
    for (Field f : fields) 
      CsvPosition position = f.getAnnotation(CsvPosition.class);
      header[position.position() - 1] = getName(f);
    
    headerIndex.initializeHeaderIndex(header);
    return header;
  

  private String getName(Field f) 
    CsvBindByName csvBindByName = f.getAnnotation(CsvBindByName.class);
    CsvCustomBindByName csvCustomBindByName = f.getAnnotation(CsvCustomBindByName.class);
    return csvCustomBindByName != null
      ? csvCustomBindByName.column() == null || csvCustomBindByName.column().isEmpty() ? f.getName() : csvCustomBindByName.column()
      : csvBindByName.column() == null || csvBindByName.column().isEmpty() ? f.getName() : csvBindByName.column();
  


我的 POJO bean 是这样注释的

public class Record 

  @CsvBindByName(required = true)
  @CsvPosition(position = 1)
  Long id;
  @CsvCustomBindByName(required = true, converter = BoolanCSVField.class)
  @CsvPosition(position = 2)
  Boolean deleted;
  ...

作者的最终代码:

CustomMappingStrategy<Record> mappingStrategy = new CustomMappingStrategy<>(Record.class);
mappingStrategy.setType(Record.class);
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer)
.withApplyQuotesToAll(false)
.withOrderedResults(true)
.withMappingStrategy(mappingStrategy)
.build();

希望对大家有所帮助

【讨论】:

太棒了!这应该直接进入源代码。我所做的唯一更改是让您的位置从零开始。【参考方案18】:

这也可以使用HeaderColumnNameMappingStrategy 和自定义Comparator 来完成。 官方文档推荐http://opencsv.sourceforge.net/#mapping_strategies

    File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
    Writer writer = new PrintWriter(reportFile);
    
final List<String> order = List.of("TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty");
    final FixedOrderComparator comparator = new FixedOrderComparator(order);
    HeaderColumnNameMappingStrategy<MappingsBean> strategy = new HeaderColumnNameMappingStrategy<>();
    strategy.setType(MappingsBean.class);
    strategy.setColumnOrderOnWrite(comparator);

    StatefulBeanToCsv<MappingsBean> beanToCsv = new
      StatefulBeanToCsvBuilder(writer)
      .withMappingStrategy(strategy)
      .build();
    beanToCsv.write(makeFinalMappingBeanList());
    writer.close();

【讨论】:

【参考方案19】:

这是添加对基于@CsvBindByPosition 的排序的支持到默认HeaderColumnNameMappingStrategy 的代码。测试最新版本5.2

方法是存储2张地图。第一个 headerPositionMap 用于存储位置元素,因此可以与 setColumnOrderOnWrite 相同,第二个 columnMap 可以从中查找实际列名而不是大写的列名

public class HeaderColumnNameWithPositionMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> 

    protected Map<String, String> columnMap;

    @Override
    public void setType(Class<? extends T> type) throws CsvBadConverterException 
        super.setType(type);
        columnMap = new HashMap<>(this.getFieldMap().values().size());
        Map<String, Integer> headerPositionMap = new HashMap<>(this.getFieldMap().values().size());
        for (Field field : type.getDeclaredFields()) 
            if (field.isAnnotationPresent(CsvBindByPosition.class) && field.isAnnotationPresent(CsvBindByName.class)) 
                int position = field.getAnnotation(CsvBindByPosition.class).position();
                String colName = "".equals(field.getAnnotation(CsvBindByName.class).column()) ? field.getName() : field.getAnnotation(CsvBindByName.class).column();
                headerPositionMap.put(colName.toUpperCase().trim(), position);
                columnMap.put(colName.toUpperCase().trim(), colName);
            
        
        super.setColumnOrderOnWrite((String o1, String o2) -> 
            if (!headerPositionMap.containsKey(o1) || !headerPositionMap.containsKey(o2)) 
                return 0;
            
            return headerPositionMap.get(o1) - headerPositionMap.get(o2);
        );
    

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException 
        String[] headersRaw = super.generateHeader(bean);
        return Arrays.stream(headersRaw).map(h -> columnMap.get(h)).toArray(String[]::new);
    

【讨论】:

【参考方案20】:

如果您需要保留原始 CSV 的列顺序:使用 HeaderColumnNameMappingStrategy 进行读取,然后使用相同的策略进行写入。在这种情况下,“相同”不仅意味着同一个类,而且实际上是同一个对象。

来自StatefulBeanToCsvBuilder.withMappingStrategy的javadoc:

读取 CSV 源代码是完全合法的,请使用映射 读取操作中的策略,并将其传递给此方法以获取 写操作。这节省了一些处理时间,但是,更多 重要的是,保留标题顺序

这样您将获得一个包含标题的 CSV,其中列的顺序与原始 CSV 相同。

使用 OpenCSV 5.4 为我工作。

【讨论】:

以上是关于OpenCSV:如何使用自定义列标题和自定义列位置从 POJO 创建 CSV 文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何对返回 orm 对象和自定义列的元组的查询进行正确排序、分组?

Spotfire 自定义格式和自定义表达式

核心数据和自定义 NSCell

JQuery、CORS 和自定义响应标头

带有附加属性和自定义模板的 ListView GridViewColumnHeader

jqgrid 更改子网格的位置或将子网格图标添加到自定义列而不是其他任何地方?