Apache POI のイベントモデル(XSSF)でセルの値を読み込み、任意のBeanのフィールドへ設定するjavaのコードです。
メモリ使用量が半端じゃないPOIですが、イベントモデルを使用するとストリーミングで読み込めるので、メモリ使用量を抑えることができます。
その場合、xmlファイルをテキストとして読み込んで処理をすることになるので、ユーザーモデルに比べると分かりにくく日本語のドキュメントもあまり無かったので、勉強がてら実際に動くものを作ってみました。
取りあえず動くことを目標にしていたのでExceptionは全てthrowsで処理してしまっていたり、いろいろと手を抜いているところがあります。
イベントモデルではxmlファイルからの値は文字列型で取得することになるので、このサンプルでは取得した値の型を設定先のbeanのフィールドの型に変換します。ですのでbeanのフィールドがDateなのにセルの値が"abc"などの文字列だと値を取得できません。また1904年始まりのExcelファイルには対応押していません。
使用ライブラリはApachePOI3.9とApacheXerces2.11.0(+それらが使っているライブラリ群)です。
十分なデバッグはしていないので自己責任でご利用ください。
SheetToBeanUtils.java
package poi.example.eventmodel.mapping;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class SheetToBeanUtils {
private static Log log = LogFactory.getLog(SheetToBeanUtils.class);
// ワークブックへのパスとbeanのインスタンスを渡すと、指定のシートの値をbeanへ設定して返します。
// ワークブックをイベントモデルで読み込むのでメモリの消費量が少なく済みます。
public static <B> B mapSheetToBean(String workbookPath, B bean)
throws Exception {
// excelから値を取得
SheetToBeanMapper mapper = new SheetToBeanMapper(workbookPath, bean);
mapper.execute();
return bean;
}
}
SheetToBeanMapper.java
package poi.example.eventmodel.mapping;
import java.io.InputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.xerces.parsers.SAXParser;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import poi.example.eventmodel.SheetHandler;
import poi.example.eventmodel.WorkbookHandler;
import poi.example.eventmodel.mapping.annotation.Sheet;
public class SheetToBeanMapper {
private static Log log = LogFactory.getLog(SheetToBeanMapper.class);
// 値を設定する対象のbean
private Object bean = null;
// ワークブックのパス
private String filePath = null;
// 値を取得するシート
private String sheetName = null;
public SheetToBeanMapper(String filePath, Object bean) {
this.filePath = filePath;
this.bean = bean;
Sheet sheet = bean.getClass().getAnnotation(Sheet.class);
if(sheet == null){
throw new IllegalArgumentException("クラスに@Sheetアノテーションが設定されていません。");
}
sheetName = sheet.name();
}
// マッピング処理開始
public void execute() throws Exception {
OPCPackage pkg = OPCPackage.open(filePath);
XSSFReader reader = new XSSFReader(pkg);
// ワークブックの情報から対象シートのrIdを取得する
XMLReader wbParser = new SAXParser();
WorkbookHandler wbHandler = new WorkbookHandler(sheetName);
wbParser.setContentHandler(wbHandler);
// ワークブックの情報を取得
InputSource wbSource = new InputSource(reader.getWorkbookData());
wbParser.parse(wbSource);
// 指定のシート名に対応するrIdを取得
String rId= wbHandler.getrId();
if(rId == null){
throw new Exception("対象のシートがありません。シート名=" + sheetName);
}
// ワークシートから値を取得する
SharedStringsTable sst = reader.getSharedStringsTable();
// パーサを取得
XMLReader parser = new SAXParser();
SheetHandler handler = new SheetHandler(sst, new FieldValueArranger(bean));
parser.setContentHandler(handler);
// シートを指定して取得
InputStream targetSheet = reader.getSheet(rId);
InputSource sheetSource = new InputSource(targetSheet);
// シートの内容をパースしながらbeanへマッピング
// この中でSheetHandlerが処理を行ってます。
parser.parse(sheetSource);
targetSheet.close();
}
}
WorkbookHandler.java
package poi.example.eventmodel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
// ワークブックの情報を取得するハンドラ
// シート名から特定のシートを取得する際に必要
public class WorkbookHandler extends DefaultHandler {
private static Log log = LogFactory.getLog(WorkbookHandler.class);
// 行のタグ
private static final String TAG_SHT = "sheet";
// シート名の属性名
private static final String NM_SNM = "name";
// r:idの属性名
private static final String NM_RID = "r:id";
// 対象のシート
private String sheetName = null;
// 対象のシートのr:id
private String rId = null;
public String getrId() {
return rId;
}
public WorkbookHandler(String sheetName) {
this.sheetName = sheetName;
}
// 開始タグを取得した際に動作するメソッド
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
// シート開始タグの場合
if (name.equals(TAG_SHT)) {
String sheetNameTemp = attributes.getValue(NM_SNM);
String rIdTemp = attributes.getValue(NM_RID);
if(sheetName.equals(sheetNameTemp)){
rId = rIdTemp;
}
}
}
}
SheetHandler.java
package poi.example.eventmodel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import poi.example.eventmodel.mapping.FieldValueArranger;
import poi.example.util.ExcelUtils;
// シートを処理するhandler
// ここでシートの値を取得し、FieldValueArrangerに渡します。
public class SheetHandler extends DefaultHandler {
private static Log log = LogFactory.getLog(SheetHandler.class);
// 行のタグ
private static final String TAG_ROW = "row";
// 列のタグ
private static final String TAG_COL = "c";
// 値のタグ
private static final String TAG_VAL = "v";
// 行番号の属性名
private static final String NM_RNO = "r";
// レンジの属性名
private static final String NM_RNG = "r";
// タイプの属性名
private static final String NM_TYP = "t";
// 文字列タイプ
private static final String TYP_STR = "s";
// 共有している文字列テーブル
private final SharedStringsTable sst;
// 取得した値を扱うクラス
private FieldValueArranger arranger = null;
// 直前のコンテンツの値
private String lastContents;
// 次の値が文字列かどうか
private boolean nextIsString;
// 現在の行
private int currentRow = 0;
// 現在の列
private int currentCol = 0;
public SheetHandler(SharedStringsTable sst, FieldValueArranger arranger) {
this.sst = sst;
this.arranger = arranger;
}
// 開始タグを取得した際に動作するメソッド
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
// 行開始タグの場合
if (name.equals(TAG_ROW)) {
// 行タグの場合は行番号を取得し、列番号をクリア
currentRow = Integer.parseInt(attributes.getValue(NM_RNO));
currentCol = 0;
}
// 列開始タグの場合
if (name.equals(TAG_COL)) {
// レンジを取得
String range = attributes.getValue(NM_RNG);
String rangeCol = ExcelUtils.getFirstColString(range);
// 現在の列番号を取得
currentCol = ExcelUtils.toColIndex(rangeCol);
// セルタイプを取得する。
String cellType = attributes.getValue(NM_TYP);
// セルタイプが"s"の場合は文字列であることをフラグに設定
if (cellType != null && cellType.equals(TYP_STR)) {
nextIsString = true;
} else {
nextIsString = false;
}
}
// コンテンツをクリア
lastContents = "";
}
// 終了タグを取得した際に動作するメソッド
public void endElement(String uri, String localName, String name)
throws SAXException {
// 文字列の場合は、今持っているlastContentsはSharedStringsTableのインデックスなので、文字列に置き換える。
if (nextIsString) {
// 直前の値を数値(参照先のインデックス)に変換
int idx = Integer.parseInt(lastContents);
// 文字列の場合はSharedStringsTableで別管理になっているので、そちらから取得
lastContents = new XSSFRichTextString(sst.getEntryAt(idx))
.toString();
// フラグをクリア
nextIsString = false;
}
// 値終了タグの場合
if (name.equals(TAG_VAL)) {
// lastContentsにセルの値が入っているので、ここでbeanにマッピングする
try {
arranger.arrange(currentRow, currentCol, lastContents);
} catch (Exception e) {
log.warn("beanに値を設定できませんでした。row=" + currentRow + ",col="
+ currentCol + ",value=" + lastContents, e);
}
}
}
// 要素の値を保持するためのメソッド
public void characters(char[] ch, int start, int length)
throws SAXException {
// 現在の値を取得する
lastContents += new String(ch, start, length);
}
}
FieldValueArranger.java
package poi.example.eventmodel.mapping;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import poi.example.eventmodel.mapping.annotation.ImportField;
import poi.example.eventmodel.mapping.annotation.ImportListField;
import poi.example.util.ExcelUtils;
import poi.example.util.ReflectionUtils;
// row,col,valueを受け取り、beanへマッピングするクラス。
public class FieldValueArranger {
private static Log log = LogFactory.getLog(FieldValueArranger.class);
// 値を設定する対象のbean
private Object bean = null;
// 設定先のフィールドの情報
private List<ImportFieldInfo> fieldInfoList = new ArrayList<ImportFieldInfo>();
public FieldValueArranger(Object bean) {
this.bean = bean;
init(bean);
}
// マッピング対象のフィールド情報を収集します。
private void init(Object bean) {
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
if (!initImportField(field)) {
initImportBeanList(field);
}
}
}
// マッピング対象の単一フィールド情報を収集します。
private boolean initImportField(Field field) {
ImportField ifAn = field.getAnnotation(ImportField.class);
if (ifAn != null) {
fieldInfoList.add(new ImportFieldInfo(field, ifAn.row(), ExcelUtils
.toColIndex(ifAn.col())));
return true;
}
return false;
}
// マッピング対象のリストフィールド情報を収集します。
private void initImportBeanList(Field field) {
ImportListField ilfAn = field.getAnnotation(ImportListField.class);
if (ilfAn != null) {
Class<?> childClazz = ReflectionUtils.getFirstChildClazz(field);
Field[] childFields = childClazz.getDeclaredFields();
for (Field childField : childFields) {
ImportField ifAn = childField.getAnnotation(ImportField.class);
if (ifAn != null) {
int childCol = ExcelUtils.toColIndex(ifAn.col());
// 子供のフィールドを追加
fieldInfoList.add(new ImportFieldInfo(field, childField,
ilfAn.startRow(), ilfAn.endRow(), childCol));
}
}
}
}
// row,colを元にフィールドを特定し値を設定します
public boolean arrange(int row, int col, String value) throws Exception {
ImportFieldInfo info = getImportField(row, col);
if (info != null) {
if (info.isChildField()) {
// リスト形式の項目へ設定
info.parentField.setAccessible(true);
@SuppressWarnings("unchecked")
List<Object> children = (List<Object>) info.parentField
.get(bean);
info.parentField.setAccessible(false);
int rowIndex = info.getListIndex(row);
Object childBean = null;
if (children.size() - 1 < rowIndex) {
// 子クラスを取得する
Class<?> childClazz = info.childClazz;
// 子クラスのインスタンスを作る
childBean = childClazz.newInstance();
while (children.size() < rowIndex) {
children.add(null);
}
children.add(childBean);
} else {
childBean = children.get(rowIndex);
}
// 子クラスのフィールドへ設定
return setFieldValue(childBean, info.field, value);
} else {
// 単一項目へ設定
return setFieldValue(bean, info.field, value);
}
}
return false;
}
// row,colに対応するフィールド情報を取得する。
private ImportFieldInfo getImportField(int row, int col) {
return getImportField(null, row, col);
}
// row,colに対応するフィールド情報を取得する。
private ImportFieldInfo getImportField(Field pareintField, int row, int col) {
for (ImportFieldInfo range : fieldInfoList) {
if (range.isIncleded(pareintField, row, col)) {
return range;
}
}
return null;
}
// fieldのclassに合わせた形式に変換し値を設定
private boolean setFieldValue(Object bean, Field field, String value)
throws Exception {
field.setAccessible(true);
if (field.getType().equals(String.class)) {
// 文字列の場合
field.set(bean, value);
} else if (field.getType().equals(Integer.class)) {
// 数値の場合
field.set(bean, ExcelUtils.getIntegerValue(value));
} else if (field.getType().equals(Long.class)) {
// 数値の場合
field.set(bean, ExcelUtils.getLongValue(value));
} else if (field.getType().equals(Short.class)) {
// 数値の場合
field.set(bean, ExcelUtils.getShortValue(value));
} else if (field.getType().equals(Double.class)) {
// 数値の場合
field.set(bean, ExcelUtils.getDoubleValue(value));
} else if (field.getType().equals(Date.class)) {
// 日付の場合
field.set(bean, ExcelUtils.getDateValue(value));
} else if (field.getType().equals(Boolean.class)) {
// 真偽値の場合
field.set(bean, ExcelUtils.getBooleanValue(value));
} else {
// その他
field.setAccessible(false);
return false;
}
field.setAccessible(false);
return true;
}
}
ImportFieldInfo.java
package poi.example.eventmodel.mapping;
import java.lang.reflect.Field;
import poi.example.util.ReflectionUtils;
// 値の設定先のフィールドの情報を管理するクラス
class ImportFieldInfo {
// Listの子クラス
Class<?> childClazz = null;
// 親のフィールド
Field parentField = null;
// 対象のフィールド
Field field = null;
// 値を取得する対象となる範囲
int startRow = 0;
int endRow = 0;
int col = 0;
// beanのfield用のコンストラクタ
ImportFieldInfo(Field field, int row, int col) {
this(null, field, row, row, col);
}
// Listに保持されたbeanのfield用のコンストラクタ
ImportFieldInfo(Field parentField, Field field, int startRow, int endRow,
int col) {
this.field = field;
this.startRow = startRow;
this.endRow = endRow;
this.col = col;
this.parentField = parentField;
if (parentField != null) {
this.childClazz = ReflectionUtils.getFirstChildClazz(parentField);
}
}
// 指定のrow,colはfieldが値を取得する対象か
boolean isIncleded(int row, int col) {
return isIncleded(null, row, col);
}
// 指定のrow,colはfieldが値を取得する対象か(Listに保持されたbeanのfield用)
boolean isIncleded(Field parentField, int row, int col) {
if (parentField == null || parentField == this.parentField) {
if (startRow <= row && row <= endRow && this.col == col) {
return true;
}
}
return false;
}
// rowに対応するListのindexを取得
int getListIndex(int row) {
return row - startRow;
}
// List配下のbeanのfieldか
boolean isChildField() {
return parentField != null;
}
@Override
public String toString() {
return "ImportFieldInfo [childClazz=" + childClazz + ", parentField="
+ parentField + ", field=" + field + ", startRow=" + startRow
+ ", endRow=" + endRow + ", col=" + col + "]";
}
}
ExcelUtils.java
package poi.example.util;
import java.util.Date;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.util.CellReference;
// Excel関連の便利なものいろいろ
public class ExcelUtils {
// TRUE扱いの値
public static final String STRING_TRUE = "1";
private ExcelUtils() {
}
// 日付形式で値を取得
public static Date getDateValue(String value) {
if (value == null) {
return null;
}
Double dblVal = null;
try {
dblVal = getDoubleValue(value);
} catch (Exception e) {
throw new IllegalStateException("日付に変換できません。value=" + value, e);
}
// 1900年始まりのみ対応
return DateUtil.getJavaDate(dblVal.doubleValue());
}
// 数値形式で値を取得
public static Integer getIntegerValue(String value) {
if (value == null) {
return null;
}
Double dblVal = getDoubleValue(value);
return Integer.valueOf(dblVal.intValue());
}
// 数値形式で値を取得
public static Long getLongValue(String value) {
if (value == null) {
return null;
}
Double dblVal = getDoubleValue(value);
return Long.valueOf(dblVal.longValue());
}
// 数値形式で値を取得
public static Short getShortValue(String value) {
if (value == null) {
return null;
}
Double dblVal = getDoubleValue(value);
return Short.valueOf(dblVal.shortValue());
}
// 数値形式で値を取得
public static Double getDoubleValue(String value) {
if (value == null) {
return null;
}
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
throw new IllegalStateException("数値に変換できません。value=" + value, e);
}
}
// 真偽値形式で値を取得
public static Boolean getBooleanValue(String value) {
return Boolean.valueOf(STRING_TRUE.equals(value));
}
// A1:B1などの表記から先頭のカラム文字列を取り出す
public static String getFirstColString(String range) {
char[] cs = range.toCharArray();
String col = "";
for (char c : cs) {
if ('0' <= c && c <= '9') {
return col;
} else {
col += c;
}
}
return col;
}
// A1表記からR1C1表記のカラムへ変換
public static int toColIndex(String colString) {
return CellReference.convertColStringToIndex(colString) + 1;
}
}
ReflectionUtils.java
package poi.example.util;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
// リフレクション用のユーティリティクラス
public class ReflectionUtils {
private ReflectionUtils() {
}
// Listの子要素のクラスを取得するメソッド
public static Class<?> getFirstChildClazz(Field field) {
// Genericも含めたTypeを取得
Type type = field.getGenericType();
// GenericのTypeを取得
ParameterizedType pType = (ParameterizedType) type;
return (Class<?>) pType.getActualTypeArguments()[0];
}
}
Sheet.java
package poi.example.eventmodel.mapping.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 値の取得元になるシートを指定する為のアノテーション
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sheet {
// シート名
String name();
}
ImportField.java
package poi.example.eventmodel.mapping.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 単独の値のマッピングを行うためのアノテーション
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ImportField {
// 行のデフォルト値
int DFAULT_ROW = 0;
// 行
int row() default DFAULT_ROW;
// 列
String col();
}
ImportListField.java
package poi.example.eventmodel.mapping.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 繰り返し項目に設定するアノテーション
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ImportListField {
// 終了行のデフォルト値
int DFAULT_END_ROW = Integer.MAX_VALUE;
// 開始行
int startRow();
// 終了行
int endRow() default DFAULT_END_ROW;
}
ParentBean.java
package poi.example.eventmodel.mapping.bean;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import poi.example.eventmodel.mapping.annotation.ImportField;
import poi.example.eventmodel.mapping.annotation.ImportListField;
import poi.example.eventmodel.mapping.annotation.Sheet;
@Sheet(name = "Sheet1")
public class ParentBean {
// セルB1の値をStringで取得
@ImportField(row = 1, col = "B")
private String field1 = null;
// セルB2の値をDateで取得
@ImportField(row = 2, col = "B")
private Date field2 = null;
// セルH1の値をBooleanで取得
@ImportField(row = 1, col = "H")
private Boolean field3 = null;
// 6行目以降をChildBeanのリストとして取得
@ImportListField(startRow = 6)
private List<ChildBean> listField = new ArrayList<ChildBean>();
@Override
public String toString() {
return "ParentBean [field1=" + field1 + ", field2=" + field2
+ ", field3=" + field3 + ", listField=" + listField + "]";
}
}
ChildBean.java
package poi.example.eventmodel.mapping.bean;
import java.util.Date;
import poi.example.eventmodel.mapping.annotation.ImportField;
public class ChildBean {
// ParentBeanのlistFieldで指定された行以降の、列Aの値をLongで取得
@ImportField(col = "A")
private Long field1 = null;
// ParentBeanのlistFieldで指定された行以降の、列Bの値をStringで取得
@ImportField(col = "B")
private String field2 = null;
// ParentBeanのlistFieldで指定された行以降の、列Cの値をDateで取得
@ImportField(col = "C")
private Date field3 = null;
@Override
public String toString() {
return "ChildBean [field1=" + field1 + ", field2=" + field2
+ ", field3=" + field3 + "]";
}
}