前段时间在一个项目里面发现,针对Excel的处理没有一个公用的视图,来个下载的需求就要自己去写一堆POI的东西,终于有一天给我也来了几个,还是按照以前的方式来写,写多了真心想吐,后面想想还是有必要整个解析Excel的视图了。花了一天时间,总结出来共有三种方式可以处理Excel视图。
由于spring已经提供了excel的抽象视图,所以我们直接继承过来就可以了。由于POI对excel2007和2003处理方式有些不同,所以spring4.2以上版本提供了两个excel抽象视图AbstractXlsxView(2007)、AbstractXlsView(2003)。现在又想到曾经公司的报表系统了,下载一年的销售数据,有上百万的数据在一个excel里面,打开直接卡死,后面还得分批次查询去搞,针对这种情况,程序里面其实可以一次性搞定的,分页查询、切割结果集到多个列表、多线程处理等等,所以处理的结果集最后还是放到Map里面的,以防数据量大的时候分成多个列表下载。这里还是只分析3中视图如何处理的,关于性能方面的东西,在实际项目中还是要充分考虑,这里就不分析了。下面的代码都是以excel2003进行分析。
这三种解析方式都要用到具体的Excel实现类,代码如下:
public class ExcelView extends AbstractXlsView {
@Override
protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setHeader("Content-Disposition", "attachment;filename="+ new String((DateFormatUtils.format(new Date(), "yyyyMMddHHmmss") + ".xls").getBytes(), "iso-8859-1")); //分割list for (Entry<String, Object> e : model.entrySet()) { HSSFSheet sheet = (HSSFSheet) workbook.createSheet("测试"); if (e.getValue() instanceof List) { List<?> dataList = (List<?>) e.getValue(); HSSFRow rowHeader = sheet.createRow(0); //添加header rowHeader.createCell(0).setCellValue("id"); rowHeader.createCell(1).setCellValue("名字"); for (int start = 0; start < dataList.size(); start++) { HSSFRow row = sheet.createRow(start + 1); String[] rowsText = dataList.get(start).toString().split(","); for (int col = 0; col < rowsText.length; col++) { HSSFCell cell = row.createCell(col); cell.setCellValue(rowsText[col]); } } } } } }
由于在AbstractXlsView里面,已经做了ContentType以及流的写入,所以我们直接把HSSFSheet里面的数据设置下就可以了。
直接添加excel视图到ModelAndView
这种方式和其他方式差不多,只是把ExcelView作为参数传到ModelAndView,代码如下:
@RequestMapping("/download")
public ModelAndView test() {
ModelAndView mav = new ModelAndView(); ExcelView excelView = new ExcelView(); List<Student> list = new ArrayList<>(); Student student = new Student(); student.setId(1); student.setName("hello"); list.add(student); student = new Student(); student.setId(2); student.setName("world"); list.add(student); mav.addObject("list", list); mav.setView(excelView); return mav; }
不用做其他配置,发送一个请求就可以得到excel文件了,结果如下:
自定义视图解析器
类似于解析jsp,只要给定相应规则把请求指定到配置的解析器就可以了,代码如下:
public class ExcelViewResolver extends AbstractCachingViewResolver implements Ordered { private ApplicationContext context; private int order = Integer.MAX_VALUE; // default: same as non-Ordered public void setOrder(int order) { this.order = order; } @Override public int getOrder() { return this.order; } @Override public boolean isCache() { return false; } /** * 直接在容器中配置视图,单例获取bean,不用每次新建,待优化 TODO */ @Override protected View loadView(String viewName, Locale locale) throws Exception { context = super.getApplicationContext(); if (context != null) { return context.getBean(viewName, View.class); } return null; } }
配置文件:
<bean class="com.myspring.web.view.ExcelViewResolver">
<property name="order" value="0"/> </bean> <bean id="excelView" class="com.myspring.web.view.ExcelView"/>
这种方式需要指定视图的名字为excelView,controller代码如下:
@RequestMapping("/download1")
public ModelAndView excel1() { //针对指定的视图解析 ModelAndView mv = new ModelAndView("excelView"); List<Student> list = new ArrayList<>(); Student student = new Student(); student.setId(1); student.setName("你好"); list.add(student); student = new Student(); student.setId(2); student.setName("世界"); list.add(student); mv.addObject("list", list); return mv; }
运行结果:
自定义注解解析返回值
spring解析json视图的时候只要@ResponseBody注解就可以了,也不用其他配置,非常方便。解析excel一样可以这样做。首先自定义一个注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Excel {
}
实现一个自定义的返回值处理器:
public class ExcelMethodProcessor implements HandlerMethodReturnValueHandler { @Override public boolean supportsReturnType(MethodParameter returnType) { return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), Excel.class) != null || returnType.getMethodAnnotation(Excel.class) != null); } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { mavContainer.setRequestHandled(true); HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); ExcelView view = new ExcelView(); if (returnValue instanceof Map) { view.render( (Map)returnValue, request, response); } else { ModelMap model = new ModelMap(); model.addAttribute("returnValue", returnValue); view.render(model, request, response); } } }
配置文件里面需要把这个bean注入到容器:
<mvc:annotation-driven>
<mvc:return-value-handlers>
<bean class="com.myspring.web.view.ExcelMethodProcessor"/> </mvc:return-value-handlers> </mvc:annotation-driven>
配置完之后,我们就只需在Controller层加个注解就完事儿了,非常简洁,代码如下:
@RequestMapping("/download2")
@Excel
public List<Student> excel2() {
List<Student> list = new ArrayList<>();
Student student = new Student(); student.setId(1); student.setName("Tom"); list.add(student); student = new Student(); student.setId(2); student.setName("Jerry"); list.add(student); return list; }
运行结果:
http://zeng233.github.io/2016/11/02/6.7spring%20MVC%E5%A4%84%E7%90%86Excel%E8%A7%86%E5%9B%BE%E7%9A%84%E4%B8%89%E7%A7%8D%E6%96%B9%E5%BC%8F/
Nowadays, exporting data into different format (Csv, Excel, Pdf ...) is a very general requirement in the most of any project. In this article, we demonstrate how to create an Excel, PDF and CSV views using Spring Boot. When configured properly, a Spring’s view resolver can generate the requested document from model data and send it to the client for downloading. the complete code can be found here.
Spring MVC View Configuration
First thing, We create a WebConfig
class, Annotated with @Configuration
to mark this class as a configuration file. I'm using contentNegotiatingViewResolver
, which tells web controllers to return ModelAndViews or view names and based on various criteria, choose the right data representation strategy.
The highest priority hereby has the file extension which is used if available in the request. Next, the ViewResolver will look for a (definable) request parameter that identifies the view. If that does not help, the ViewResolver uses the Java Activation Framework to determine the Content-Type. If all fails, use the the HTTP Accept header. Of course the steps can be individually disabled. Check out this great article for more details.
In our example, we will be using the URL extension to help determine the media types. Also, we have set the default media type to TEXT_JSON in absence of file extension or when the filetype is unknown.
Also, We need to set theContentNegotiationManager
which will be injected by Spring, and different resolvers for each possible output format our application might produce.
Finally, we have created different view resolvers for PDF, XLS and CSV output which we will discuss next.
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer .defaultContentType(MediaType.APPLICATION_JSON) .favorPathExtension(true); } /* * Configure ContentNegotiatingViewResolver */ @Bean public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) { ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setContentNegotiationManager(manager); // Define all possible view resolvers List<ViewResolver> resolvers = new ArrayList<>(); resolvers.add(csvViewResolver()); resolvers.add(excelViewResolver()); resolvers.add(pdfViewResolver()); resolver.setViewResolvers(resolvers); return resolver; } /* * Configure View resolver to provide XLS output using Apache POI library to * generate XLS output for an object content */ @Bean public ViewResolver excelViewResolver() { return new ExcelViewResolver(); } /* * Configure View resolver to provide Csv output using Super Csv library to * generate Csv output for an object content */ @Bean public ViewResolver csvViewResolver() { return new CsvViewResolver(); } /* * Configure View resolver to provide Pdf output using iText library to * generate pdf output for an object content */ @Bean public ViewResolver pdfViewResolver() { return new PdfViewResolver(); } }
Creating Controller
Nothing much to say here, The Export
controller adds some data to the Model which we’ll display on the views.
@Controller
public class Export {
@Autowired
UserService userService;
/**
* Handle request to download an Excel document */ @RequestMapping(value = "/download", method = RequestMethod.GET) public String download(Model model) { model.addAttribute("users", userService.findAllUsers()); return ""; } }
Excel View
There are 2 file formats in which we can create an Excel document. The .xls
is the old format, the .xlsx
is the new format which is XML based. We are using apache POI to create excel files, when creating .xls
documents make sure the org.apache.poi:poi
dependency is on the classpath. When working with .xlsx
files, you need the org.apache.poi:poi-ooxml
dependency.
ExcelView
which extends from AbstractXlsView
. We create the excel document by overriding the buildExcelDocument, the rest is self explanatory.
public class ExcelView extends AbstractXlsView{
@Override
protected void buildExcelDocument(Map<String, Object> model,
Workbook workbook,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// change the file name
response.setHeader("Content-Disposition", "attachment; filename=\"my-xls-file.xls\"");
@SuppressWarnings("unchecked") List<User> users = (List<User>) model.get("users"); // create excel xls sheet Sheet sheet = workbook.createSheet("User Detail"); sheet.setDefaultColumnWidth(30); // create style for header cells CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontName("Arial"); style.setFillForegroundColor(HSSFColor.BLUE.index); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); font.setBold(true); font.setColor(HSSFColor.WHITE.index); style.setFont(font); // create header row Row header = sheet.createRow(0); header.createCell(0).setCellValue("Firstname"); header.getCell(0).setCellStyle(style); header.createCell(1).setCellValue("LastName"); header.getCell(1).setCellStyle(style); header.createCell(2).setCellValue("Age"); header.getCell(2).setCellStyle(style); header.createCell(3).setCellValue("Job Title"); header.getCell(3).setCellStyle(style); header.createCell(4).setCellValue("Company"); header.getCell(4).setCellStyle(style); header.createCell(5).setCellValue("Address"); header.getCell(5).setCellStyle(style); header.createCell(6).setCellValue("City"); header.getCell(6).setCellStyle(style); header.createCell(7).setCellValue("Country"); header.getCell(7).setCellStyle(style); header.createCell(8).setCellValue("Phone Number"); header.getCell(8).setCellStyle(style); int rowCount = 1; for(User user : users){ Row userRow = sheet.createRow(rowCount++); userRow.createCell(0).setCellValue(user.getFirstName()); userRow.createCell(1).setCellValue(user.getLastName()); userRow.createCell(2).setCellValue(user.getAge()); userRow.createCell(3).setCellValue(user.getJobTitle()); userRow.createCell(4).setCellValue(user.getCompany()); userRow.createCell(5).setCellValue(user.getAddress()); userRow.createCell(6).setCellValue(user.getCity()); userRow.createCell(7).setCellValue(user.getCountry()); userRow.createCell(8).setCellValue(user.getPhoneNumber()); } } }
Spring also provides 2 other abstract classesAbstractXlsxView
and AbstractXlsxStreamingView
to create xlsx files. When working with large excel documents it is profitable to use the streaming xlsx view. The streaming view uses less memory and can improve performance of large excel documents.
PDF view
For that we'll use iText library. Spring provides an AbstractPdfView abstract class which can be subclassed to create a helper class for generating PDF documents. However, it has a big drawback which the AbstractPdfView class only supports old API version of iText i.e. it is using the package com.lowagie.*
(iText version <= 2.1.7) while the recent iText’s package changes to com.itextpdf.*
(iText version >= 5.x)
The old iText version is no longer available nor supported, so subclassing AbstractPdfView
class is discouraged. Instead, I recommend to subclass the AbstractView class to create an iText 5.x-compatible version.
public abstract class AbstractPdfView extends AbstractView { public AbstractPdfView() { setContentType("application/pdf"); } @Override protected boolean generatesDownloadContent() { return true; } @Override protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // IE workaround: write into byte array first. ByteArrayOutputStream baos = createTemporaryOutputStream(); // Apply preferences and build metadata. Document document = new Document(PageSize.A4.rotate(), 36, 36, 54, 36); PdfWriter writer = PdfWriter.getInstance(document, baos); prepareWriter(model, writer, request); buildPdfMetadata(model, document, request); // Build PDF document. document.open(); buildPdfDocument(model, document, writer, request, response); document.close(); // Flush to HTTP response. response.setHeader("Content-Disposition", "attachment"); // make browser to ask for download/display writeToResponse(response, baos); } protected void prepareWriter(Map<String, Object> model, PdfWriter writer, HttpServletRequest request) throws DocumentException { writer.setViewerPreferences(getViewerPreferences()); } protected int getViewerPreferences() { return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage; } protected void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) { } protected abstract void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception; }
After that, all what we need to do next is create PdfView
class and extend from the previously created AbstractPdfView
and override the buildPdfDocument(..)
method to create our PDF document.
CSV View
For that, we will follow the exact same approach used for PDF generation, which involves creating an abstract view class, a concrete view class and view resolver. I'm using Super CSV to generate csv files.
So, below the code for AbstractCsvView
that Subclass the Spring’s AbstractView
class:
public abstract class AbstractCsvView extends AbstractView { private static final String CONTENT_TYPE = "text/csv"; private String url; public AbstractCsvView() { setContentType(CONTENT_TYPE); } public void setUrl(String url) { this.url = url; } @Override protected boolean generatesDownloadContent() { return true; } @Override protected final void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); buildCsvDocument(model, request, response); } protected abstract void buildCsvDocument( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception; }
Then, we write an implementation of the AbstractCsvView
class and make it implements the buildCsvDocument()
method as follows:
public class CsvView extends AbstractCsvView { @Override protected void buildCsvDocument(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setHeader("Content-Disposition", "attachment; filename=\"my-csv-file.csv\""); List<User> users = (List<User>) model.get("users"); String[] header = {"Firstname","LastName","LastName","JobTitle","Company","Address","City","Country", "PhoneNumber"}; ICsvBeanWriter csvWriter = new CsvBeanWriter(response.getWriter(), CsvPreference.STANDARD_PREFERENCE); csvWriter.writeHeader(header); for(User user : users){ csvWriter.write(user, header); } csvWriter.close(); } }
Demo
Run the app, and to download:
- pdf file use: localhost:8080/download.pdf
- XLS file use: localhost:8080/download.xls
- CSV file use: localhost:8080/download.csv
http://xieahui.com/2018/05/30/java/framework/springboot-%E7%94%9F%E6%88%90%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%ACexcel%E5%92%8Cpdf%E4%BB%A5%E5%8F%8A%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/
ContentNegotiatingViewResolver是 ViewResolver 使用所请求的媒体类型的一个实现(基于文件类型扩展,输出格式URL参数指定类型或接受报头)来选择一个合适的视图一个请求。ContentNegotiatingViewResolver本身并不解决视图,只不表示为其他的 ViewResolver,您可以配置来处理特定的视图(XML,JSON,PDF,XLS,HTML,..)。
这里需要使用到以下技术:
- Spring 4.0.6.RELEASE
- jackson-databind 2.4.1.3
- jackson-annotations 2.4.1
- lowagie itext 4.2.1
- Apache POI 3.10-beta2
- Maven 3
- JDK 1.6
- Tomcat 7.0.54
- Eclipse JUNO Service Release 2
我们现在就开始!
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.ctolib.springmvc</groupId> <artifactId>ContentNegotiatingViewResolver</artifactId> <packaging>war</packaging> <version>1.0.0</version> <name>Spring4MVCContentNegotiatingViewResolverExample</name> <properties> <springframework.version>4.0.6.RELEASE</springframework.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${springframework.version}</version> </dependency> <!-- Needed for XML View (with JAXB2) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>${springframework.version}</version> </dependency> <!-- Needed for JSON View --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.4.1.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.4.1</version> </dependency> <!-- Needed for PDF View --> <dependency> <groupId>com.lowagie</groupId> <artifactId>itext</artifactId> <version>4.2.1</version> </dependency> <!-- Needed for XLS View --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.10-beta2</version> </dependency> <!-- Servlet dependencies --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</