Spring Boot控制器返回嵌套JSON转CSV文件实现方案咨询
嵌套JSON转CSV的可行方案(Spring Boot环境)
常规JSON转CSV工具失效的核心原因是存在嵌套对象(如示例中的team),这类工具无法自动解析嵌套字段。解决思路是先扁平化嵌套结构,将嵌套对象的字段拆分为CSV的独立列,再完成转换。以下是三种实用方案:
方案一:手动扁平化+Apache Commons CSV(灵活可控)
适合结构固定的场景,直接提取嵌套字段到顶层,手动构建CSV行。
- 定义实体类(也可直接用Map处理,实体类更易维护):
// 球员实体 public class Player { private Integer id; private String firstName; private String lastName; private String position; private Integer heightFeet; private Integer heightInches; private Integer weightPounds; private Team team; // getter、setter、构造器 } // 球队实体 public class Team { private Integer id; private String abbreviation; private String city; private String conference; private String division; private String fullName; private String name; // getter、setter、构造器 }
- 将JSON转为实体对象:
ObjectMapper objectMapper = new ObjectMapper(); Player player = objectMapper.readValue(apiJsonResponse, Player.class);
- 用Apache Commons CSV生成文件:
先引入依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.10.0</version> </dependency>
转换代码:
// 定义CSV表头,包含扁平化后的字段 String[] HEADERS = { "id", "first_name", "last_name", "position", "height_feet", "height_inches", "weight_pounds", "team_id", "team_abbreviation", "team_city", "team_conference", "team_division", "team_full_name", "team_name" }; // 写入到响应流(可直接输出给前端下载) try (CSVPrinter printer = new CSVPrinter(new OutputStreamWriter(response.getOutputStream()), CSVFormat.DEFAULT.builder() .setHeader(HEADERS) .setSkipHeaderRecord(false) .build())) { // 手动提取嵌套字段,写入一行数据 printer.printRecord( player.getId(), player.getFirstName(), player.getLastName(), player.getPosition(), player.getHeightFeet(), player.getHeightInches(), player.getWeightPounds(), player.getTeam().getId(), player.getTeam().getAbbreviation(), player.getTeam().getCity(), player.getTeam().getConference(), player.getTeam().getDivision(), player.getTeam().getFullName(), player.getTeam().getName() ); } catch (IOException e) { // 业务异常处理 e.printStackTrace(); }
方案二:Jackson自定义CSV序列化(自动扁平化)
适合需要通用处理的场景,通过Jackson的CsvSchema配置实现嵌套字段的自动映射。
- 引入Jackson CSV依赖:
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-csv</artifactId> <version>2.15.2</version> </dependency>
- 序列化代码:
ObjectMapper objectMapper = new ObjectMapper(new CsvMapper()); // 构建CSV Schema,指定嵌套字段的列名 CsvSchema schema = CsvSchema.builder() .addColumn("id") .addColumn("first_name") .addColumn("last_name") .addColumn("position") .addColumn("height_feet") .addColumn("height_inches") .addColumn("weight_pounds") .addColumn("team_id", CsvSchema.ColumnType.NUMBER) .addColumn("team_abbreviation") .addColumn("team_city") .addColumn("team_conference") .addColumn("team_division") .addColumn("team_full_name") .addColumn("team_name") .setUseHeader(true) .build(); // 配置序列化规则 objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // 转换为CSV字符串并输出 String csvContent = objectMapper.writer(schema).writeValueAsString(player); response.setContentType("text/csv"); response.setHeader("Content-Disposition", "attachment; filename=player.csv"); response.getWriter().write(csvContent);
方案三:OpenCSV配合注解映射
通过OpenCSV的注解直接在实体类上绑定CSV列名,自动完成转换。
- 引入OpenCSV依赖:
<dependency> <groupId>com.opencsv</groupId> <artifactId>opencsv</artifactId> <version>5.6</version> </dependency>
- 修改实体类添加注解:
public class Player { @CsvBindByName(column = "id") private Integer id; @CsvBindByName(column = "first_name") private String firstName; @CsvBindByName(column = "last_name") private String lastName; @CsvBindByName(column = "position") private String position; @CsvBindByName(column = "height_feet") private Integer heightFeet; @CsvBindByName(column = "height_inches") private Integer heightInches; @CsvBindByName(column = "weight_pounds") private Integer weightPounds; private Team team; // 为嵌套字段添加映射Getter,供OpenCSV读取 @CsvBindByName(column = "team_id") public Integer getTeamId() { return team != null ? team.getId() : null; } @CsvBindByName(column = "team_abbreviation") public String getTeamAbbreviation() { return team != null ? team.getAbbreviation() : null; } // 其他team字段的映射Getter同理... // 原有的getter、setter }
- 转换代码:
try (Writer writer = new OutputStreamWriter(response.getOutputStream())) { StatefulBeanToCsv<Player> beanToCsv = new StatefulBeanToCsvBuilder<Player>(writer) .withSeparator(CSVWriter.DEFAULT_SEPARATOR) .withQuotechar(CSVWriter.DEFAULT_QUOTE_CHARACTER) .withOrderedResults(true) .build(); beanToCsv.write(player); } catch (CsvDataTypeMismatchException | CsvRequiredFieldEmptyException | IOException e) { e.printStackTrace(); }
关键注意点
- 无论哪种方案,核心都是扁平化嵌套结构,必须将嵌套对象的字段拆分为CSV的独立列。
- 如果处理批量数据,只需循环写入每个对象即可。
- 注意空值处理,避免嵌套对象为null时出现空指针异常。
内容的提问来源于stack exchange,提问作者kidulf
相关产品推荐
相关产品推荐

