使用Apache poi来编写导出excel的工具类
在JavaWeb开发的需求中,我们会经常看到导出excel的功能需求,然后java并没有提供操作office文档的功能,这个时候我们就需要使用额外的组件来帮助我们完成这项功能了。
很高兴Apache基金会为我们提供了poi组件,让我们能够在它的帮助下操作office文档,下面就让我们来使用它来制作导出excel的工具类。
一.前期说明
1.我们要理解一个excel文件的组成。首先要有一个workbook(工作簿,即excel文件本身),然后workbook中有几个sheet(工作表),sheet中是由row(行)和column(列)构成的,行和列确定一个唯一的cell(单元格)。理清了这几个组成部分,我们再来理解工具类的就比较容易了。
2.office文件分为97-2003版本和2007版本,这两个是截然不同的文件格式,97-2003的excel的文件后缀为.xsl,而2007的excel的文件后缀为.xslx。我们在这里暂时只讨论97-2003版本的操作,至于2007版本的操作原理相同,只是所使用的类不同。
3.操作excel 97-2003的文件,只需要引入poi-version-yyyymmdd.jar(例如poi-3.13-20150929.jar)即可,其余jar包的用途,详情请百度“poi各jar包的作用”。
二.代码分析
工具类代码
package com.billionsfinance.bqap.util;
import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFRichTextString; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import com.billionsfinance.common.Exception.BizException; public class ExportExcel<T> { /** * 利用了JAVA的反射,将javabean的集合以一定的格式输出到指定IO设备上 * @param sheetName * 工作表标题 * @param headerMap * 表格列属性及属性名 * @param dataset * 需要显示的数据集合 * @param out * 与输出设备相关的流对象,可以将Excel文档导出到本地或者网络中 * @throws IOException */ public void exportExcel(String sheetName, Map<String, String> headerMap, Collection<T> dataset, OutputStream out) throws IOException{ //声明一个工作簿 HSSFWorkbook workbook = new HSSFWorkbook(); //生成一个工作表 HSSFSheet sheet = workbook.createSheet(sheetName); //设置表格默认列宽
//有些资料在设置宽度的时候用的是short(20),但是在实际使用的过程中,short(20)已不被推荐使用,所以直接使用int(20) sheet.setDefaultColumnWidth(20); //获取属性Map的keySet Set<String> keySet = headerMap.keySet(); //获取属性的数组 String[] fields = keySet.toArray(new String[0]); //对keySet进行遍历,获得属性对应的属性名称来构成标题行 Iterator<String> key = keySet.iterator(); //第一行 HSSFRow row = sheet.createRow(0); for(int i = 0; key.hasNext(); i++){ //产生单元格 HSSFCell cell = row.createCell(i); //产生标题行 HSSFRichTextString title = new HSSFRichTextString(headerMap.get(key.next()));
//为单元格赋值 cell.setCellValue(title); } /* * 遍历数据集合,产生数据行 * 利用反射,通过属性数组来拼接getXXX的方法去获得属性的值 */ //属性数组的长度 int size = fields.length; //属性的get方法的数组 String[] methodNames = new String[size]; for(int i = 0; i < size; i++){
//拼凑属性的getter方法 methodNames[i] = "get" + fields[i].substring(0, 1).toUpperCase() + fields[i].substring(1); } Iterator<T> ite = dataset.iterator(); for(int index = 1; ite.hasNext(); index++){ row = sheet.createRow(index); T t = (T)ite.next(); for(int i = 0; i < size; i++){ HSSFCell cell = row.createCell(i); try{ Class<?> cla = t.getClass(); Method getMethod = cla.getMethod(methodNames[i], new Class[]{}); Object value = getMethod.invoke(t, new Object[]{}); String textValue = null; //如果属性的类型是String,则直接输出字符串 if(value instanceof String){ textValue = value.toString(); } //如果属性的类型是Date,则需要格式转换 if(value instanceof Date){ Date date = (Date)value; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss"); textValue = sdf.format(date); } //如果是字符串,则利用正则表达式判断textValue是否全部由数字组成 if(textValue != null){ Pattern p = Pattern.compile("^//d+(//.//d+)?$"); Matcher matcher = p.matcher(textValue); if(matcher.matches()){ //是数字则当做double处理 cell.setCellValue(Double.parseDouble(textValue)); }else{ HSSFRichTextString richString = new HSSFRichTextString(textValue); cell.setCellValue(richString); } } }catch(BizException ex){ LogUtil.ERROR.error(ex, ex); }catch(Exception e){ e.printStackTrace(); } } }
//将workbook输出到输出设备中 workbook.write(out);
//关闭资源 out.flush(); workbook.close(); out.close(); } }
代码说明:
1.sheetName,即工作表的名称,这里只有一张工作表,所以一次导出的数据不能多于6万条。注意,一次导出太多的数据会消耗服务器内存,有可能导致服务器宕机,所以导出的时候最好对一次性导出数据量做限定。
2.headerMap,此处应传入一个linkedHashMap<String,String>对象。linkedHashMap既指定了属性和属性名称(name:名称)的对应关系,又保证了属性与属性之间的顺序,这样我们就能很好的控制在excel表格中属性的顺序。
3.dataset,这里使用的泛型,不过我们在实际开发中一般都是使用List集合来传递数据。
4.out,输出设备,这里既可以是屏幕设备,也可以是文件系统,当然也可以是response响应了。
代码分析:
1.在代码中,我尽量避免使用java的反射,因为反射是比较消耗性能的操作,只在获得getter方法的时候使用了反射。属性的获取是通过headerMap遍历,这么做一是节省了性能的消耗,二是可以指定需要导出的属性。例如我们在使用实体类的时候经常需要序列化,那么实体类中肯定有serialVersionUID属性,然而这个属性我们是不需要导出的。我们只需要在headerMap中添加需要导出的属性即可做到剔除不需要的属性。
三.使用
1.bean
package com.billionsfinance.bqap.bean; public class SmsFailHistoryBean implements java.io.Serializable{ private static final long serialVersionUID = 5449014577923870013L; private String id; //流水号 private String sender; //发送人 private String appTypeName; //发送类型名称 private String phone; //手机号码 private String message; //短信内容 private String supplier; //供应商 private String status; //状态 private String submitTime; //提交时间 private String createTime; //创建时间 private String appType; //发送类型
/* getter and setter */ }
这个bean就是我们在项目开发中经常使用的实体类,看,其中就有serialVersionUID属性,可是我们并不需要导出它。
2.controller
package com.billionsfinance.bqap.controller; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.billionsfinance.bqap.bean.SmsFailHistoryBean; import com.billionsfinance.bqap.bean.SmsSendHistoryBean; import com.billionsfinance.bqap.entity.Config; import com.billionsfinance.bqap.server.IConfigServer; import com.billionsfinance.bqap.server.ISmsSendHistoryServer; import com.billionsfinance.bqap.util.Constant; import com.billionsfinance.bqap.util.ExportExcel; import com.billionsfinance.bqap.util.GroupNoManager; import com.billionsfinance.bqap.util.StringUtils; import com.billionsfinance.common.framework.RemoteCall; /** * 导出excel表格,暂时只能用来导出发送历史和失败历史 * @author XIAYU * */ @Controller public class ExportExcelController { /* * 导出发送历史或发送失败历史 */ @RequestMapping("/exportExcel") public void exportExcel(HttpServletRequest request, HttpServletResponse response) throws Exception{ ISmsSendHistoryServer smsSendHistoryServer = RemoteCall.getRemoteObject(ISmsSendHistoryServer.class, GroupNoManager.getGroupNo(request)); //获取查询条件 Map<String, String> params = this.setParmas(request); //文件名称 String filename = null; response.reset();
//设置响应头 response.setContentType("application/msexcel"); OutputStream out = response.getOutputStream(); Map<String, String> headerMap = new LinkedHashMap<String, String>(); String page = request.getParameter("page"); //如果是smsSendHistoryList.jsp页面发过来的则导出发送历史 if("smsSendHistory".equals(page)){ filename = "发送历史总记录.xls"; response.setHeader("Content-disposition", "attachment;filename=" + encodeChineseDownloadFileName(request, filename)); ExportExcel<SmsSendHistoryBean> ex = new ExportExcel<SmsSendHistoryBean>(); headerMap.clear(); headerMap.put("id", "流水号"); headerMap.put("sender", "发送人"); headerMap.put("appTypeName", "发送类型"); headerMap.put("message", "短信内容"); headerMap.put("supplier", "供应商"); headerMap.put("success", "成功条数"); headerMap.put("successPhone", "成功手机号码"); headerMap.put("failure", "失败条数"); headerMap.put("failurePhone", "失败手机号码"); headerMap.put("count", "总条数"); headerMap.put("sendTime", "定时发送时间"); headerMap.put("createTime", "创建时间");
//获取需要导出的数据集合 List<SmsSendHistoryBean> dataset = smsSendHistoryServer.listSmsSendHistory(params); ex.exportExcel("发送历史", headerMap, dataset, out); } //如果是smsFailHistoryList.jsp页面发过来的则导出失败历史 if("smsFailHistory".equals(page)){ filename = "短信发送失败记录.xls"; response.setHeader("Content-disposition", "attachment;filename=" + encodeChineseDownloadFileName(request, filename)); ExportExcel<SmsFailHistoryBean> ex = new ExportExcel<SmsFailHistoryBean>(); headerMap.clear(); headerMap.put("id", "流水号"); headerMap.put("sender", "发送人"); headerMap.put("appTypeName", "发送类型"); headerMap.put("phone", "手机号码"); headerMap.put("message", "短信内容"); headerMap.put("supplier", "供应商"); headerMap.put("status", "状态"); headerMap.put("submitTime", "提交时间"); headerMap.put("createTime", "创建时间");
//获取需要导出的数据集合 List<SmsFailHistoryBean> dataset = smsSendHistoryServer.listSmsFailHistory(params); ex.exportExcel("失败历史", headerMap, dataset, out); } if(out != null){ out.flush(); out.close(); } } /** * 根据配置代码获取获取枚举参数信息 * @param request * @param configCode * @return */ private List<Config> getConfigList(HttpServletRequest request,String configCode){ IConfigServer configserver =RemoteCall.getRemoteObject(IConfigServer.class, GroupNoManager.getGroupNo(request)); Config config = new Config(); config.setConfigCode(configCode); List<Config> list = configserver.listConfig(config); return list; } /** * 存储参数,保证页面刷新后参数还在查询栏中 * 返回参数,用于查询条件 * @param request * @return */ private Map<String,String> setParmas(HttpServletRequest request){ List<Config> appList = this.getConfigList(request, Constant.APP_CODE); request.setAttribute("appList", appList); List<Config> supplierList = this.getConfigList(request, Constant.SUPPLIER_CODE); request.setAttribute("supplierList", supplierList); Map<String,String> params = new HashMap<String,String>(); String appCode = request.getParameter("appCode"); String sendType = request.getParameter("sendType"); String supplier = request.getParameter("supplier"); String startTime = request.getParameter("startTime"); String endTime = request.getParameter("endTime"); params.put("appCode", appCode); params.put("sendType", sendType); params.put("supplier", supplier); params.put("startTime", startTime); params.put("endTime", endTime); request.setAttribute("appCode", appCode); request.setAttribute("sendType", sendType); request.setAttribute("supplier", supplier); request.setAttribute("startTime", startTime); request.setAttribute("endTime", endTime); return params; }
/**
* 解决浏览器对文件中文名的编码问题
* 不同的浏览器对文件中文名的处理方法不同
* @param request
* @param pFileName
* @return
*/ private String encodeChineseDownloadFileName(HttpServletRequest request, String pFileName) throws UnsupportedEncodingException{ String filename = null; //获取请求头中的浏览器标识 String agent = request.getHeader("USER-AGENT"); if(agent != null){ if(agent.indexOf("Firefox") != -1){ //Firefox filename = "=?UTF-8?B?" + (new String(Base64.encodeBase64(pFileName.getBytes("UTF-8")))) + "?="; }else if(agent.indexOf("Chrome") != -1){ //Chrome filename = new String(pFileName.getBytes(), "ISO8859-1"); }else{ //IE7+ filename = URLEncoder.encode(pFileName, "UTF-8"); //替换空格 filename = StringUtils.replace(filename, "+", "%20"); } }else{ filename = pFileName; } return filename; } }
这个Controller是springMVC下的Controller,使用注解。hessian调用不必太在意,主要是注意调用工具类时应传入的参数。
3.html
<a href="${ctx}/exportExcel.action?page=smsFailHistory¶ms=...">导出</a>
只需要一个a标签或者一个button就能触发导出事件呢,这里要注意一个问题,在导出操作的时候不要使用ajax方式,否则返回的文件数据会直接打印到浏览器的后台中,而不是输出到文件系统。所以就按照平常的方式去提交请求就好。
四.总结
这只是poi操作excel文件的一个简单示例,它还有其他很多的功能,比如改变excel数据的样式,编写excel公式等等,,,这些功能需要熟读api,待以后有时间,再来写博。
无他,唯手熟尔~