我们的应用程序一直存在问题,生成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();
}
答案 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数据存储库上使用此功能,请在此处检查。