はじめに
DDD×Apache POI でエクセル出力ドメインを作成していきます
Apache POIとは
Apache POI(以前はJakarta POIと呼ばれていました)はJavaアプリケーションからExcelやWordなどのMicrosoft製品のフォーマットファイルを読み書きするためのAPIです。
環境
Java 11
Spring Boot 2.6.1
Gradle 2.24
Apache POI 4.1.2
設定
poi-ooxmlも追加することで、 xlsx
、dock
といった2007形式のファイルの読み書きが可能となります。
dependencies {
implementation('org.apache.poi:poi:4.1.2')
implementation('org.apache.poi:poi-ooxml:4.1.2')
}
エクセルドメイン
ここからDDDの出番です
まずは、Excel出力
の業務仕様をモデル化し、コードで表記していきます
今回実現したい業務は以下です
・ Excelファイルを作成する
・ Excelファイルをダウンロードする
必要な構成要素は以下です
・拡張子
・ファイル名
・シート名
・タイトル一覧
・タイトル行フォント種別
・タイトル行太字設定
・タイトル行横位置
・タイトル行縦位置
・データ一覧
・データ行フォント種別
Excel出力
クラスをただのデータの入れ物ではなく、モデルを正確に表すため、
属性以外の情報(ファイル作成ロジック等)も作成していきます
出力内容毎に構成要素を追加できるよう抽象クラスにしています
@RequiredArgsConstructor
public abstract class Excel {
/** ファイル拡張子 */
private static final String EXTENSION = ".xlsx";
/** 日時フォーマッタ */
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
/** タイトル行フォント種別 */
private static final FontType titleFontType = FontType.MSP_GOTHIC;
/** タイトル行太文字フラグ */
private static final boolean isBoldTitle = true;
/** タイトル行横位置 */
private static final HorizontalAlignment titleHorizontalAlignment = HorizontalAlignment.CENTER;
/** タイトル行縦位置 */
private static final VerticalAlignment titleVerticalAlignment = VerticalAlignment.CENTER;
/** データ行フォント種別 */
private static final FontType bodyFontType = FontType.MSP_GOTHIC;
/** タイムスタンプ */
private final String TIMESTAMP = LocalDateTime.now().format(FORMATTER);
private final ServletContext context;
private final HttpServletRequest request;
/**
* ファイル名(先頭のみ)を取得します。
* @return ファイル名(先頭のみ)
*/
abstract public String getFileNamePrefix();
/**
* シート名を取得します。
* @return シート名
*/
abstract public String getSheetName();
/**
* タイトル行に出力するタイトル一覧を取得します。
* @return タイトル一覧
*/
abstract public List<String> getTitles();
/**
* データ行に出力する行列の値一覧を取得します。
* @return 行列の値一覧
*/
abstract public List<List<String>> getMatrixValues();
/**
* ファイルを作成します。
* @return ファイル作成成功の場合はtrue
*/
public boolean createFile() {
final FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(getFilePath());
} catch (FileNotFoundException e) {
return false;
}
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet(getSheetName());
int rowNum = 0;
// タイトル行書き込み
Row headerRow = sheet.createRow(rowNum);
XSSFCellStyle titleStyle = createTitleStyle(workbook);
for (int i = 0; i < getTitles().size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(getTitles().get(i));
cell.setCellStyle(titleStyle);
}
// データ行書き込み
for (List<String> columnValues : getMatrixValues()) {
rowNum++;
// フォント設定
XSSFFont font = workbook.createFont();
font.setFontName(bodyFontType.getText());
CellStyle bodyStyle = workbook.createCellStyle();
bodyStyle.setFont(font);
bodyStyle.setLocked(false);
// データ書き込み
XSSFRow row = sheet.createRow(rowNum);
for (int j = 0; j < columnValues.size(); j++) {
row.createCell(j).setCellValue(columnValues.get(j));
}
}
// 列サイズ自動調整
for (int i = 0; i < sheet.getRow(0).getPhysicalNumberOfCells(); i++) {
sheet.autoSizeColumn(i);
}
// ファイル出力
try {
workbook.write(outputStream);
outputStream.flush();
outputStream.close();
workbook.close();
} catch (IOException e) {
return false;
}
return true;
}
/**
* ファイルをダウンロードします。
* @return ファイルデータ
*/
public HttpEntity<byte[]> downloadFile() {
File file = new File(getFilePath());
if (file.exists()) {
try {
FileInputStream inputStream = new FileInputStream(file);
val fileData = IOUtils.toByteArray(inputStream);
val fileTypeMap = new MimetypesFileTypeMap();
val headers = new HttpHeaders();
val mimeType = fileTypeMap.getContentType(getFilePath());
val encodedFilename = URLEncoder.encode(getFileName(), StandardCharsets.UTF_8);
headers.setContentType(MediaType.valueOf(mimeType));
headers.setContentDisposition(ContentDisposition.parse(
"attachment; filename=\"" + getFileName() + "\";filename*=utf-8''" + encodedFilename
));
return new HttpEntity<>(fileData, headers);
} catch (IOException e) {
e.printStackTrace();
} finally {
file.delete();
}
}
return new HttpEntity<>(new byte[]{});
}
/**
* ファイル名を取得します。
* @return ファイル名
*/
private String getFileName() {
return getFileNamePrefix() + "-" + TIMESTAMP + EXTENSION;
}
/**
* ファイルパスを取得します。
* @return ファイルパス
*/
private String getFilePath() {
String contextPath = context.getRealPath(request.getContextPath());
File contextDirectory = new File(contextPath);
if (!contextDirectory.exists()) {
contextDirectory.mkdirs();
}
return contextDirectory + "/" + getFileName();
}
/**
* タイトル行のセルスタイルを設定します。
* @param workbook 対象ブック
* @return セルスタイル
*/
private XSSFCellStyle createTitleStyle(XSSFWorkbook workbook) {
XSSFFont font = workbook.createFont();
font.setBold(isBoldTitle);
font.setFontName(titleFontType.getText());
XSSFCellStyle style = workbook.createCellStyle();
style.setAlignment(titleHorizontalAlignment);
style.setVerticalAlignment(titleVerticalAlignment);
style.setFont(font);
return style;
}
/**
* フォント種別
*/
@AllArgsConstructor
@Getter
public enum FontType {
MSP_GOTHIC("MS Pゴシック (本文)"),
;
private final String text;
}
}
Excelドメインを継承する
Excelドメインを継承し、ファイル名のセットや、出力データのセットをしていきます。
public class SampleExcel extends Excel {
private static final String FILE_NAME = "サンプルファイル";
private static final String SHEET_NAME = "サンプル一覧";
private static final List<String> TITLES = List.of("名称", "登録日時");
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
@Setter
private List<SampleDto> dtoList;
public SampleExcel(ServletContext context, HttpServletRequest request) {
super(context, request);
}
@Override
public String getFileNamePrefix() {
return FILE_NAME;
}
@Override
public String getSheetName() {
return SHEET_NAME;
}
@Override
public List<String> getTitles() {
return TITLES;
}
@Override
public List<List<String>> getMatrixValues() {
val rows = new ArrayList<List<String>>();
for (SampleDto dto : dtoList) {
rows.add(getColumnValues(dto));
}
return rows;
}
/**
* カラムに出力する値一覧を取得します。
* @param dto サンプルDTO
* @return カラムに出力する値一覧
*/
private List<String> getColumnValues(SampleDto dto) {
val columns = new ArrayList<String>();
columns.add(dto.getName());
columns.add(dto.getRegisterTime().format(FORMATTER));
return columns;
}
}
最後に
内容的に至らない部分もあると思います。
間違っている、気になる等ありましたら、優しく諭していただけるととても喜びます。