Spring MVC CSV导出方法中的内存不足错误

时间:2019-03-20 06:29:46

标签: java spring-mvc supercsv

我们的应用程序一直存在问题,生成CSV文件时该应用程序内存不足。特别是在行超过1万行的大型CSV文件中。我们正在使用Spring Boot 2.0.8和SuperCSV 2.4.0。

处理这些情况的正确方法是什么,以使我们的Spring MVC API不会由于OutOfMemoryException而崩溃。

SuperCSV会导致此问题吗?我想不是,只是为了以防万一。

我一直在读@Async,在此方法上使用它来打开单独的线程是个好主意吗?

假设我在控制器中具有以下方法:

@RequestMapping(value = "/export", method = RequestMethod.GET)
public void downloadData(HttpServletRequest request,HttpServletResponse response) throws SQLException, ManualException, IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

    List<?> data = null;
    data = dataFetchService.getData();

    ICsvBeanWriter csvWriter = new CsvBeanWriter(response.getWriter(), CsvPreference.STANDARD_PREFERENCE);

    //these next lines handle the header
    String[] header = getHeaders(data.get(0).getClass());
    String[] headerLocale = new String[header.length];
    for (int i = 0; i < header.length; i++)
        {
            headerLocale[i] = localeService.getLabel(this.language,header[i]);
        }

        //fix for excel not opening CSV files with ID in the first cell
        if(headerLocale[0].equals("ID")) {
            //adding a space before ID as ' ID' also helps
            headerLocale[0] = headerLocale[0].toLowerCase();
        }

    csvWriter.writeHeader(headerLocale);

    //the next lines handle the content
    for (Object line : data) {
        csvWriter.write(line, header);
    }

    csvWriter.close();
    response.getWriter().flush();
    response.getWriter().close();
}

2 个答案:

答案 0 :(得分:1)

代码:

data = dataFetchService.getData();

看起来可能会占用很多内存。此列表可能是数百万条记录。否则,如果许多用户同时导出,将导致内存问题。

由于dataFetchService由Spring数据存储库支持,因此您应该获取返回的记录数量,然后一次获取一个可分页的数据。

示例:如果表中有20,000行,您应该一次获得1000行数据20次,然后慢慢建立CSV。

您还应该以某种顺序请求数据,否则CSV可能最终会以随机顺序出现。

看看在存储库中实现PagingAndSortingRepository

示例应用

Product.java

import javax.persistence.Entity;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {

    @Id
    private long id;
    private String name;
}

ProductRepository.java

import org.springframework.data.repository.PagingAndSortingRepository;

public interface ProductRepository extends PagingAndSortingRepository<Product, Integer> {
}

MyRest.java

import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.supercsv.io.CsvBeanWriter;
import org.supercsv.io.ICsvBeanWriter;
import org.supercsv.prefs.CsvPreference;

@RestController
@RequiredArgsConstructor
public class MyRest {

    @Autowired
    private ProductRepository repo;

    private final int PAGESIZE = 1000;

    @RequestMapping("/")
    public String loadData() {
        for (int record = 0; record < 10_000; record += 1) {
            repo.save(new Product(record, "Product " + record));
        }
        return "Loaded Data";
    }

    @RequestMapping("/csv")
    public void downloadData(HttpServletResponse response) throws IOException {
        response.setContentType("text/csv");
        String[] header = {"id", "name"};
        ICsvBeanWriter csvWriter = new CsvBeanWriter(response.getWriter(), CsvPreference.STANDARD_PREFERENCE);

        csvWriter.writeHeader(header);

        long numberRecords = repo.count();
        for (int fromRecord = 0; fromRecord < numberRecords; fromRecord += PAGESIZE) {
            Pageable sortedByName = PageRequest.of(fromRecord, PAGESIZE, Sort.by("name"));
            Page<Product> pageData = repo.findAll(sortedByName);
            writeToCsv(header, csvWriter, pageData.getContent());
        }
        csvWriter.close();
        response.getWriter().flush();
        response.getWriter().close();
    }

    private void writeToCsv(String[] header, ICsvBeanWriter csvWriter, List<Product> pageData) throws IOException {
        for (Object line : pageData) {
            csvWriter.write(line, header);
        }
    }

}

1)通过调用

加载数据
curl http://localhost:8080

2)下载CSV

curl http://localhost:8080/csv

答案 1 :(得分:0)

您应该尝试使用setFetchSize来分批获取数据,这一次只能使用数据库端的游标来带来有限的行。这增加了网络往返次数,但是由于我正在流式传输下载内容,因此对于用户而言,这无关紧要,因为他们不断获取文件。我还使用Servlet 3.0异步功能来释放容器工作线程并将此任务交给另一个Spring托管线程池。我将其用于Postgresql数据库,它的工作原理就像魅力。 MySQL和Oracle jdbc驱动程序也支持此功能。我正在使用原始JDBCTemplate进行数据访问,并将我的自定义结果集转换为csv转换器以及即时zip转换器。 要在Spring数据存储库上使用此功能,请在此处检查。