POI导出Excel的定制化/模板化开发

1.背景

前阵子做OA系统,遇到了将表格导出为Excel文件的需求。poi生成Excel的时候,一般是每个excel书写一个模板类,很是麻烦,此篇博客希望提供一种思路,用于快速创建excel模板类,并增加代码的复用性。很多地方可能做的不够好,有不妥的地方,请不吝赐教,大家一起进步!

2.功能说明

  1. 该工具的目的在于支持excel表格的定制化开发,提供了基本默认样式类、以及公共模板类,加速不同样式表格的生成,很适合表格复杂多变的场景;
  2. 数据、样式、excel模板是分离的,样式和模板可分别配置,可任意组合复用;
  3. 样式、表头、字体、宽度等均可配置,并支持根据不同需要对单元格的数据进行特殊处理后再输出;
  4. 测试很多次但问题在所难免,分享不易,请确认是否适合你的需求再使用它;
  5. 应为数据的载体是Map,所以如果数据量过大,需要稍微改造一下噢;

3.目录结构

如下图所示,有四个包inter,test,utils,write;

inter放接口

目前很简单,就一个接口

test测试文件

提供了一个测试案例,要看怎么调用的请看这里。大部分都是在准备数据,模板和样式有了以后,调用也就几行代码的事。

utils工具类

常量、POI基础工具类、默认样式类等。
目前比较杂,可以考虑分开成不同包。

write放写好的excel的模板

除了抽象类模板,已经提供了两个模板示例了

这里写图片描述

4.如何使用

吹牛那么多,不如先来看一下测试案例。

public class Test{
	...
	//调用
	public static void main(String[] args) {
		
		List<Map> maps = getDatas();//getDatas()的生成暂省略了,总之就是普通map
		
		//sheet名
		String sheetName = "Test ExcelExport";
		//表名
		String headName = "测试表";
		//表格宽度
		int[] fieldsWidths = new int[]{10,10,10,20};
		//列名
		String[] titleNames  = new String[]{"姓名","学号","户籍","销售业绩"};
		//列名对应的Map中的字段名
		String[] tableFileds  = new String[]{"name","num","place","sum"};
		
		//构造载体
		WriteExcelData datas = new WriteExcelData(sheetName, headName, fieldsWidths, 
				titleNames, tableFileds, maps);
		
		SimpleExcel excel = new SimpleExcel(datas);
		
		try {
			//写excel
			OutputStream os = new FileOutputStream("C:/develop/test.xls");
			excel.write(os);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
}

5. Constant——常量类

下图是本例导出的样图。要说明的是,将Excel分成了三个部分来构造,分别是head(表头),title(列名),content(内容);三个部分单独构造,互不影响。
Constant类预先定义好了一些基本的常量。如字体大小,对齐方式,字体样式单元格高度等。
poi中,很多方法中都需要传入了short类型的参数。所以,为了方便对传入的short值的控制,我在Constant中定义了一个short类型的常量SHORT_NULL,当传入这个常量的时候,不进行任何操作。

这里写图片描述

public class Constant {
//传入此变量,表示不进行操作
	public static final short SHORT_NULL = -200;
	public static final short alignment_cell = CellStyle.ALIGN_CENTER;
	public static final short verticalAlignment_cell = CellStyle.VERTICAL_CENTER;
	public static final short forgroundColor_cell = SHORT_NULL;
	public static final short fontHeightInpoint_font_head = (short)12;
	public static final short fontHeightInpoint_font_title = (short)11;
	public static final short fontHeightInpoint_font_content = (short)11;
	public static final short fillPattern = HSSFCellStyle.SOLID_FOREGROUND;
	public static final short boldWeight_head = Font.BOLDWEIGHT_BOLD;
	public static final short boldWeight_title = Font.BOLDWEIGHT_NORMAL;
	public static final short boldWeight_content = Font.BOLDWEIGHT_NORMAL;
	public static final byte charset = Font.DEFAULT_CHARSET;
	public static final short fontColor_font = Font.COLOR_NORMAL;
	public static final short border_style = CellStyle.BORDER_THIN;
	public static final String fontName_head = "宋体";
	public static final String fontName_title = "宋体";
	public static final String fontName_content = "宋体";
	public static final float rowHeight = 23;
	......
}

6. ExcelUtils——工具类

我封装的方法不是很多,这里只贴出来一部分。

public class ExcelUtils {
	public static void setCellStyleBorder(CellStyle style,short left,
								short top,short right,short bottom){
			if(left != Constant.SHORT_NULL){
				style.setBorderLeft(left);
			}
			if(top != Constant.SHORT_NULL){
				style.setBorderTop(left);
			}
			if(right != Constant.SHORT_NULL){
				style.setBorderRight(right);
			}
			if(bottom != Constant.SHORT_NULL){
				style.setBorderBottom(bottom);
			}	
		}	
		/**
		 * 自动列宽
		 * @param size
		 * @param sheet
		 */
		public static void fitColumn(int size,Sheet sheet){
			for (int i = 0; i < size; i++) {
				sheet.autoSizeColumn(i, true);
			}		
		} ......
	}

7. DefaultStyle——默认样式类

目的在于,在创建模板的时候,尽量让通用的样式可复用。

  • 本类封装了一些常用的样式,比如。表头/内容/列名的字体,单元格的样式,行高等;
  • 如果每个单元格都单独new一个font,很占内存;
  • poi中对Font字体的个数的支持是有限制的;
  • DefaultStyle中的content部分的字体样式中,常用的是货币,日期,百分比,普通文本样式;本类默认提供了这四种样式;

贴出部分代码:

	public class DefaultStyle{
		private HSSFWorkbook book;
		//表头
		private Font default_head_font;
		private HSSFCellStyle default_head_style;
		//标题
		private Font default_title_font;
		private HSSFCellStyle default_title_style;
		//内容
		private Font default_content_font;
		private HSSFCellStyle default_content_style_text;
		private HSSFCellStyle default_content_style_money;
		private HSSFCellStyle default_content_style_date;
		private HSSFCellStyle default_content_style_percent;
		//行高
		private float default_rowHeight = Constant.rowHeight;
		//边框样式
		private short default_border_style = Constant.border_style;
		public DefaultStyle(HSSFWorkbook book) {
			this.book = book;
		}
		public HSSFWorkbook getBook() {
			return book;
		}
		public void setBook(HSSFWorkbook book) {
			this.book = book;
		}
		public Font getDefault_head_font() {
			if(this.default_head_font == null){
					this.default_head_font = ExcelUtils.createFont(book, 
						Constant.fontName_head,Constant.SHORT_NULL,
						Constant.fontHeightInpoint_font_head,
						Constant.fontColor_font);
			}
			return default_head_font;
		}
		public void setDefault_head_font(Font default_head_font) {
			this.default_head_font = default_head_font;
		}
		public HSSFCellStyle getDefault_head_style() {
			if(this.default_head_style == null){
					this.default_head_style = ExcelUtils.createCellStyle(
						book,getDefault_head_font(),
						Constant.alignment_cell,
						Constant.verticalAlignment_cell,
						Constant.SHORT_NULL,Constant.SHORT_NULL);
			}
			return default_head_style;
		}
		public void setDefault_head_style(HSSFCellStyle default_head_style) {
			this.default_head_style = default_head_style;
		}
		public Font getDefault_title_font() {
			if(this.default_title_font == null){
					this.default_title_font = ExcelUtils.createFont(book,
						Constant.fontName_title, Constant.SHORT_NULL,
						Constant.fontHeightInpoint_font_title,
						Constant.boldWeight_title,
						Constant.fontColor_font);
			}
			return default_title_font;
		}
		public void setDefault_title_font(Font default_title_font) {
			this.default_title_font = default_title_font;
		}
		public HSSFCellStyle getDefault_title_style() {
			if(this.default_title_style == null){
				this.default_title_style = ExcelUtils.createCellStyle(book,
						Constant.verticalAlignment_cell,
						Constant.SHORT_NULL,Constant.SHORT_NULL);
						
				ExcelUtils.setCellStyleBorder(default_title_style,
						Constant.border_style,
						Constant.border_style,
						Constant.border_style);
			}
			return default_title_style;
		}
	}

8. IWriteExcel ——接口类

正如之前所说,excel分为三个部分构建(head,title,content),IWriteExcel 自然需要包含这三个方法。但是大家发现多了一个方法(onInitRowProperties)。为啥呢?这是因为像行高、列宽呀这些行级别的属性都是通过Sheet来控制的,而行高,列宽,对于一张excel表来说,基本上一开始就确定好了 。所以,在导出excel之前先初始化了这些属性。
除了onInitRowProperties方法外的其他三个方法,它们的返回值都是int类型。因为接口的方法我在后边是按顺序执行的.执行顺序为:

onInitRowProperties–>onCreateHeadRow–>onCreateTitleRow–>onCreateContent

  • 首先,对于一张excel工作表,从第0行开始调用onCreateHeadRow,表头占用多少行不用管,自己去实现即可。
  • 接下来,你要告诉onCreateTitleRow方法,它可以从哪一行开始写入数据;
  • 同理,onCreateContent也需要知道自己能够从哪一行开始写入数据;
  • 所以,每执行完一个方法,返回下一个可以写入的行的index即可;

不同的模板类的实现可能不一样。假如我们实现了一个模板类A。如果另一个模板类B样式与A差不多,如果只有列名部分不一样,那我们可以直接继承自模版A,然后重写onCreateTitleRow()方法即可,这就省去了
重新构建excel其他部分的功夫。

public interface IWriteExcel {
	/**
	 * 初始化行级别的属性(行高。列宽)
	 * @param sheet
	 * @param excelData
	 * @param defaultStyle
	 */
	public void onInitRowProperties(HSSFWorkbook book,HSSFSheet sheet,
									WriteExcelData excelData,
									DefaultStyle defaultStyle);
	
	/**
	 * 创建列名行
	 * @param sheet
	 * @param excelData
	 * @param defaultStyle
	 * @param allColumnSize
	 * @param currentIndex
	 * @return
	 */
	public int onCreateTitleRow(HSSFWorkbook book,HSSFSheet sheet,
								WriteExcelData excelData,DefaultStyle defaultStyle,
								int allColumnSize,int currentIndex); 
	
	/**
	 * 创建表头行
	 * @param sheet
	 * @param excelData
	 * @param defaultStyle
	 * @param allColumnSize
	 * @param currentIndex
	 * @return
	 */
	public int onCreateHeadRow(HSSFWorkbook book,HSSFSheet sheet,
								WriteExcelData excelData,DefaultStyle defaultStyle, 
								int allColumnSize,int currentIndex);
	
	/**
	 * 创建表格内容部分
	 * @param sheet
	 * @param excelData
	 * @param defaultStyle
	 * @param allColumnSize
	 * @param currentIndex
	 * @return
	 */
	public int onCreateContent(HSSFWorkbook book,HSSFSheet sheet,
								WriteExcelData excelData,DefaultStyle defaultStyle,
								int allColumnSize,int currentIndex);
}

###9. WriteExcelData——封装数据类
这是一个封装的数据资源类,我们就是从这里面取出数据,经过一些列操作后,写入到excel中去。

  • sheetName,headName:工作表名、表头名;
  • fieldsWidths:列宽数组,预先定义excel好每一列的宽度。
  • titleNames:列名数组,每一列的列名。titleNames的数据顺序要与fieldsWidths的顺序一致(分别对应着每个列的列名和列宽)。
  • maps:数据源,是一个Map集合,因为公司前端接收list数据;我懒得为导出excel单独写一个查询方法;所以,我直接用的之前的查询方法;但是这个改成list当然也可以。只是在后面的时候的时候取数据的方法不一样而已;
  • tableFields:要输入的数据,也就是map中的key。这里的key应当要与titleNames和fieldsWidths保持对应关系;也许你会问?为什么要单独传入一个tableFields呢?其实我之前也没想传这个变量。因为后来我们的需求发生了变化:公司需要我们实现excel的个性化导出,具体要导出哪些列,让用户自己去选择;而且,不能简单地将map中的所有key对应的value导出,因为可能有些用户没有查看所有列的权限。所以,tableFields就起到了这样的作用。

简单来说就是,maps就是最终你要导出到excel的实际数据,用list来表示,一个Map表示一行数据。而titleNames、fieldsWidths则是用来表示哪一列应该显示什么数据(是什么标题),这一列的宽度应该是多少。而最终的显示结果是通过tableFields与titleNames来共同控制的。

比如,

titleNames[0]的值为"姓名",说明第一列要显示的是姓名,
tableFields[0]的值为"name",说明应该map中对应的key为"name",
此时,
Map map = maps[0];
String showName = map .getValue(“name”);
所以,第一行中的第一列要显示的数据就是showName了;

下面给出数据类的定义:

public class WriteExcelData {
	private String sheetName;//工作簿名
	private String headName;//表头名
	private int[] fieldsWidths;//列宽
	private String[] titleNames;//列名
	private String[] tableFileds;//要显输出maps中的哪些数据(哪些key-value)

	public WriteExcelData() {
	
	}
	
	public WriteExcelData(String sheetName, String headName,
						int[] fieldsWidths, String[] titleNames,
						String[] tableFileds, List<Map> maps) {
		this.sheetName = sheetName;
		this.headName = headName;
		this.fieldsWidths = fieldsWidths;
		this.titleNames = titleNames;
		this.tableFileds = tableFileds;
		this.maps = maps;
	}
	public String getSheetName() {
		return sheetName;
	}
	public void setSheetName(String sheetName) {
		this.sheetName = sheetName;
	}
	public String getHeadName() {
		return headName;
	}
	public void setHeadName(String headName) {
		this.headName = headName;
	}
	public int[] getFieldsWidths() {
		return fieldsWidths;
	}
	public void setFieldsWidths(int[] fieldsWidths) {
		this.fieldsWidths = fieldsWidths;
	}
	public String[] getTitleNames() {
		return titleNames;
	}
	public void setTitleNames(String[] titleNames) {
		this.titleNames = titleNames;
	}
	public String[] getTableFileds() {
		return tableFileds;
	}
	public void setTableFileds(String[] tableFileds) {
		this.tableFileds = tableFileds;
	}
	public List<Map> getMaps() {
		return maps;
	}
	public void setMaps(List<Map> maps) {
		this.maps = maps;
	}
}

10. AbstractExcel ——导出Excel的模板抽象类

理论上所有的Excel模板类都应该继承自这个类。这个类的作用主要是进行一些初始化操作,并且引导正确的方法调用顺序。
成员变量包括了上述的DefaultStyle默认样式类和WriteExcelData数据类。还包含了IWriteExcel类,而IWriteExcel类需要AbstractExcel自己去创建。
导出excel的时候,我们只需要在构造方法中传入一个WriteExcelData,并且手动传入一个IWriteExcel实例即可,然手直接调用write()方法即可。

public abstract class AbstractExcel {
public DefaultStyle config;
private WriteExcelData datas;

	private HSSFWorkbook book;
	
	private HSSFSheet sheet;
	
	protected int cellSize;//列的长度
	
	private int dataSize;//内容部分的数据长度
	
	private int currentIndex;//当前写入到的行
	
	private IWriteExcel writeExcel;

	public AbstractExcel(WriteExcelData datas) {
		this.datas = datas;
		if(datas.getTableFileds() != null){
			this.cellSize = datas.getTableFileds().length;
		}
		if(datas.getMaps() != null){
			this.dataSize = datas.getMaps().size();
		}
	}
	
	private void init(){
		book = new HSSFWorkbook();
		String sheetName = datas.getSheetName();
		if(StringUtils.isEmpty(sheetName)){
			sheet = book.createSheet();
		}else{
			sheet = book.createSheet(sheetName);
		}
		config = new DefaultStyle(this.book);
		
	}
	
	public void destroy(){
		sheet = null;
		book = null;
		if(datas!=null){
			List<Map> maps = datas.getMaps();
			if(maps != null){
				maps.clear();
			}
		}
		datas = null;
	}
	
	public abstract IWriteExcel createWriteExcel();
	
	public void write(OutputStream os) throws IOException{
		
		init();
		writeExcel = createWriteExcel();
		int rowLength = datas.getTitleNames().length;
		
		writeExcel.onInitRowProperties(book,sheet, datas, config);
		
		currentIndex = writeExcel.onCreateHeadRow(book,sheet, datas, config, rowLength, currentIndex);
		
		currentIndex = writeExcel.onCreateTitleRow(book,sheet, datas, config, rowLength, currentIndex);
		
		currentIndex = writeExcel.onCreateContent(book,sheet, datas, config, rowLength, currentIndex);
		
		book.write(os);
}}

11. SimpleExcel ——最简单的AbstractExcel实现类

SimpleExcel 继承自AbstractExcel ,并且实现了IWriteExcel接口。此外,还定义了一个IcellValueTransformer接口——单元格内容转化器,可将单元格的值进行处理以后再输出,也可以重新设置单元格的样式;

public class SimpleExcel extends AbstractExcel implements IWriteExcel{
	/**
	 * 单元格内容转化器,可以单元格的值进行处理,或者重新设置单元格的样式
	 * @author sonnyching
	 *
	 */
	public interface IcellValueTransformer{
		/**
		 * 将单元格的内容转化
		 * @param defaultStyle 默认样式
		 * @param cell 要转化的单元格
		 * @param mapKey map中的key
		 * @param value 单元格中最初的数据
		 */
		public <T> void transformCellValue(DefaultStyle defaultStyle,HSSFCell cell,String mapKey,T value);
	}
	
	private IcellValueTransformer transformer;
	
	public SimpleExcel(WriteExcelData datas) {
		super(datas);
	}
	
	@Override
	public IWriteExcel createWriteExcel() {
		return this;
	}
	
	@Override
	public void onInitRowProperties(HSSFWorkbook book, HSSFSheet sheet,
			WriteExcelData excelData, DefaultStyle defaultStyle) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public int onCreateTitleRow(HSSFWorkbook book, HSSFSheet sheet,
			WriteExcelData excelData, DefaultStyle defaultStyle,
			int allColumnSize, int currentIndex) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int onCreateHeadRow(HSSFWorkbook book, HSSFSheet sheet,
			WriteExcelData excelData, DefaultStyle defaultStyle,
			int allColumnSize, int currentIndex) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int onCreateContent(HSSFWorkbook book, HSSFSheet sheet,
			WriteExcelData excelData, DefaultStyle defaultStyle,
			int allColumnSize, int currentIndex) {
		// TODO Auto-generated method stub
		return 0;
	}

	public void setTransformer(IcellValueTransformer transformer) {
		this.transformer = transformer;
	}
}
  1. onCreateRowProperties()的实现
    新建一个私有方法来调用。若没有传入列宽数组,则自动调整列宽。

private void autoInitRowProperties(HSSFWorkbook book,HSSFSheet sheet,
WriteExcelData excelData,DefaultStyle defaultStyle){

int[] columnWidths = excelData.getFieldsWidths();
sheet.setDefaultRowHeightInPoints(defaultStyle.getDefault_rowHeight());//默认行高
try {
	if(columnWidths == null){
		ExcelUtils.fitColumn(super.cellSize, sheet);
	}else{
		for (int i = 0; i < super.cellSize; i++) {
			sheet.setColumnWidth(i, columnWidths[i]*256);
		}
	}		
} catch (Exception e) {
	e.printStackTrace();
	//throw new IllegalAccessException("列宽数组与列的数量不一致");
}	

}
2. onCreateHeadRow()的实现
同样新建一个私有方法。也很简单。默认将表头部分在水平方向进行了合并,合并的列数正好是所有列
的个数。如果不需要表头,直接返回0就可以了。mergeCells(Sheet sheet,int startRow,int endRow,int starColumn,int endColumn)我自己封装的方法,传
入要合并的区域值即可。

private int autoCreateHeadRow(HSSFWorkbook book, HSSFSheet sheet,
WriteExcelData excelData, DefaultStyle defaultStyle,
int allColumnSize, int currentIndex){

String headName = excelData.getHeadName();
if(StringUtils.isEmpty(headName) ){
	return currentIndex;
}
try {
	HSSFRow headRow = sheet.createRow(currentIndex);
	//合并
	HSSFCell cell = headRow.createCell(0);
	cell.setCellStyle(super.config.getDefault_head_style());
	cell.setCellValue(headName);
	ExcelUtils.mergeCells(sheet, 0, 0, 0, super.cellSize - 1);	
	return ++currentIndex;
} catch (Exception e) {
	e.printStackTrace();
}	
return currentIndex;

}
3)onCreateTitleRow()的实现
也就是将列名数组中的数据挨个取粗来用而已。默认一个列名只占一行,且没有单元格合并。
tips:随着表格复杂度的增加,列名可能会出现好几个单元格合并,列名不仅仅占一行的情况。这时我们只
需要重写此方法即可。原理一样。

private int autoCreateTitleRow(HSSFWorkbook book, HSSFSheet sheet,
WriteExcelData excelData, DefaultStyle defaultStyle,
int allColumnSize, int currentIndex){
HSSFRow titleRow = sheet.createRow(currentIndex);
String[] rowNames = excelData.getTitleNames();
for (int i = 0; i < super.cellSize; i++) {
String value = rowNames[i];
HSSFCell cell = titleRow.createCell(i);
cell.setCellStyle(super.config.getDefault_title_style());
cell.setCellValue(value==null?"":value);
}
return ++currentIndex;
}
4)onCreateContent()的实现
内容部分的导出。其实也就是遍历list数据源的数据而已。当然,无用的列是不需要导出的,用tableFileds数组来确定。这里还要判断是否传入了IcellValueTransformer单元格转化器,如果,传入了,就要使用转化器转化后再输出。

private int autoCreateContent(HSSFWorkbook book, HSSFSheet sheet,
WriteExcelData excelData, DefaultStyle defaultStyle,
int allColumnSize, int currentIndex){

String[] fileds = excelData.getTableFileds();
List<Map> maps = excelData.getMaps();
for (int i = 0; i < maps.size(); i++) {
	//创建行
	HSSFRow row = sheet.createRow(currentIndex);
	//创建列
	Map map = maps.get(i);
	for (int j = 0; j < super.cellSize; j++) {
		Object obj = map.get(fileds[j]);
		HSSFCell cell = row.createCell(j);
		//设置单元格的格式
		if(transformer != null){
			transformer.transformCellValue(defaultStyle, cell, fileds[j], obj);
		}else{
			defaultTransformCellValue(defaultStyle, cell, fileds[j], obj);
		}
	}
	currentIndex++;
}
return currentIndex;	

话不多说。

代码地 戳这里 ——> poi导出excel封装

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值