4.11. ファイルダウンロード


4.11.1. Overview

本節では、クライアントにサーバからファイルをダウンロードする機能について説明する。
ファイルのダウンロード処理の概要を、以下に示す。
  1. DispatcherServletは、コントローラへファイルダウンロードのリクエストを送信する。

  2. コントローラは、ファイル表示の情報を取得する。

  3. コントローラは、Viewを選択する。

  4. ファイルレンダリングは、Viewで行われる。

本ガイドラインでは、共通ライブラリから提供しているorg.terasoluna.gfw.web.download.AbstractFileDownloadViewを継承したカスタムViewを実装することを推奨する。

Tip

ファイルダウンロード機能を提供する際には、ディレクトリトラバーサル攻撃への対策が必要な場合がある。

ディレクトリトラバーサル攻撃については、ディレクトリトラバーサル攻撃 を参照すること。


4.11.2. How to use

4.11.2.1. ViewResolverの設定

カスタムViewクラスを用いてファイルをダウンロードするため、jspやthymeleafのような画面名ではなくBean名を指定する必要がある。
blankプロジェクトから生成されたプロジェクトでは、Springのコンテキストで管理されたBean名を用いて実行するViewを選択できるようにorg.springframework.web.servlet.view.BeanNameViewResolverが以下のように設定されている。

SpringMvcConfig.java

@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    // omitted

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.beanName(); // (1)
        registry.jsp("/WEB-INF/views/", ".jsp");
    }

項番

説明

(1)
ViewResolverRegistry#beanNameを呼び出し、BeanNameViewResolverを定義する。
JSP用のViewResolverより先に定義することで、BeanNameViewResolverの優先度を高くする。

Note

Spring FrameworkはさまざまなViewResolverを提供しており、複数のViewResolverをチェーンすることができる。

ただし、読み込み順によっては意図しないViewが選択されてしまうことがある。

この動作は、<mvc:view-resolvers>要素の子要素やViewResolverRegistryに、優先したいViewResolverを上から順に定義する事で防ぐことができる。


4.11.2.2. ファイルのダウンロード

共通ライブラリが提供しているorg.terasoluna.gfw.web.download.AbstractFileDownloadViewを継承したクラスを実装する。
AbstractFileDownloadViewでは、以下を実装する必要がある。
  1. レスポンスボディへの書き込むためのInputStreamを取得する。

  2. HTTPヘッダに情報を設定する。


4.11.2.2.1. カスタムViewの実装

テキストファイルをダウンロードする例を用いて、説明を行う。

AbstractFileDownloadViewを継承したクラスの実装例

@Component  // (1)
public class TextFileDownloadView extends AbstractFileDownloadView {  // (2)

    @Override
    protected InputStream getInputStream(Map<String, Object> model, HttpServletRequest request)
            throws IOException { // (3)
        Resource resource = new ClassPathResource("testdata/サンプル.txt");
        return resource.getInputStream();
    }

    @Override
    protected void addResponseHeader(Map<String, Object> model, HttpServletRequest request,
            HttpServletResponse response) { // (4)
        String encodedFileName = URLEncoder.encode("サンプル.txt", StandardCharsets.UTF_8);
        String contentDisposition =
                String.format("attachment; filename*=UTF-8''%s", encodedFileName);

        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", contentDisposition); // (5)
        response.setContentType("text/plain"); // (6)
    }
}

項番

説明

(1)
本例では、@Componentアノテーションを使用して、component-scanの対象としている。
前述した、org.springframework.web.servlet.view.BeanNameViewResolverの対象とすることができる。
(2)
AbstractFileDownloadViewを継承する。
(3)
getInputStreamメソッドを実装する。
ダウンロード対象の、InputStreamを返却すること。
(4)
addResponseHeaderメソッドを実装する。
ダウンロードするファイルに合わせたContent-DispositionContentTypeを設定する。
(5)
Content-Dispositionを設定する。
上記例では、attachment; filename*=UTF-8''サンプル.txtを指定しているため、サンプル.txtというテキストファイルがダウンロードされる。
設定可能な値はContent Disposition Values and Parametersを参照されたい。
(6)
ContentTypeを設定する。
上記例ではテキストファイルとして扱うため、text/plainを指定している。
設定可能な値はMedia Typesを参照されたい。

4.11.2.2.2. ControllerでのViewの指定

BeanNameViewResolverにより、コントローラでtextFileDownloadViewを返却することで、Springのコンテキストで管理されたBeanIDがtextFileDownloadViewのViewが使用される。

Javaソース

@GetMapping(value = "download")
public String download() {
    return "textFileDownloadView"; // (1)
}

項番

説明

(1)
textFileDownloadViewをメソッドの戻り値として返却することで、Springのコンテキストで管理されたTextFileDownloadViewクラスが実行される。

4.11.3. Appendix

4.11.3.1. 動的に生成したファイルのダウンロード

AbstractFileDownloadViewは、静的なファイルだけではなく動的に生成したファイルをダウンロードさせる場合にも利用できる。
ここでは、参考程度にPDFファイルやExcelファイルを動的に生成しダウンロードさせる例を示す。

4.11.3.1.1. PDFファイルの動的な生成

PDFのレンダリングにOpenPDFを利用する例を紹介する。


4.11.3.1.1.1. OpenPDFを使用するための設定

pom.xmlに OpenPDFの定義を追加する。

<dependencies>
    <!-- omitted -->
    <dependency>
        <groupId>com.github.librepdf</groupId>
        <artifactId>openpdf</artifactId>
        <version>${OPENPDF_VERSION}</version>
    </dependency>
</dependencies>

4.11.3.1.1.2. カスタムViewの実装
@Component
public class SamplePdfView extends AbstractFileDownloadView { // (1)

    @Inject
    private PdfHelper pdfHelper; // (2)

    @Override
    protected InputStream getInputStream(Map<String, Object> model, HttpServletRequest request)
            throws IOException {
        return pdfHelper.createPdf(model); // (2)
    }

    @Override
    protected void addResponseHeader(Map<String, Object> model, HttpServletRequest request,
            HttpServletResponse response) {
      String encodedFileName = URLEncoder.encode("サンプル.pdf", StandardCharsets.UTF_8);
      String contentDisposition =
              String.format("attachment; filename*=UTF-8''%s", encodedFileName);

      response.setCharacterEncoding("UTF-8");
      response.setHeader("Content-Disposition", contentDisposition); // (3)
      response.setContentType("application/pdf"); // (4)
    }
}

項番

説明

(1)
AbstractFileDownloadViewを継承したカスタムViewクラスを実装する。
(2)
PDFファイルを生成するためのヘルパークラスを利用する。
ヘルパークラスの実装例は後述する。
(3)
Content-Dispositionを設定する。
上記例では、attachment; filename*=UTF-8''サンプル.pdfを指定しているため、サンプル.pdfというPDFファイルがダウンロードされる。
(4)
ContentTypeを設定する。
PDFファイルとして扱うため、application/pdfを指定している。

4.11.3.1.1.3. ヘルパークラスの実装
Viewに処理を書くのではなく、ヘルパークラスで処理を実装する。
ヘルパークラスを作成することで再利用性を高めることができる。
下記サンプルでは、modelに設定されたserverTimeをPDFに出力する単純な例となる。
本サンプルで紹介していない機能を使用したい場合は、OpenPDFのサンプルやチュートリアルを参考に実装されたい。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfWriter;

@Component
public class PdfHelper {

    public InputStream createServerTimePdf(Map<String, Object> model) throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            try (Document document = new Document(PageSize.A4)) { // (1)

                PdfWriter.getInstance(document, outputStream); // (2)
                document.open();

                BaseFont bf = BaseFont.createFont("HeiseiKakuGo-W5", "UniJIS-UCS2-H", false);
                Font titleFont = new Font(bf, 18);

                Paragraph paragraph = new Paragraph(new Phrase("サンプル PDF", titleFont));
                paragraph.setAlignment(Element.ALIGN_CENTER);
                document.add(paragraph); // (3)

                String serverTime = model.get("serverTime") != null ? model.get("serverTime").toString()
                        : "Server Time not available";
                document.add(new Paragraph("Server Time: " + serverTime)); // (3)
            } catch (DocumentException e) {
                throw new IOException("Failed to create PDF document", e);
            }

            return new ByteArrayInputStream(outputStream.toByteArray());
        } catch (Exception e) {
            throw new IOException("Failed to create PDF document", e);
        }
    }
}

項番

説明

(1)
Documentを生成する。
上記例ではA4サイズのPDFを生成している。
(2)
PdfWriterのインスタンスを生成し、DocumentとOutputStreamを関連付ける。
(3)
PDFドキュメントにParagraphでテキストを追加する。

4.11.3.1.1.4. ControllerでのViewの指定
BeanNameViewResolverにより、コントローラでsamplePdfViewを返却することで、Springのコンテキストで管理されたBeanIDがsamplePdfViewのViewが使用される。

Javaソース

@GetMapping(value = "sample", params= "pdf")
public String samplePdf(Model model) {
    model.addAttribute("serverTime", LocalDateTime.now());
    return "samplePdfView"; // (1)
}

項番

説明

(1)
samplePdfViewをメソッドの戻り値として返却することで、Springのコンテキストで管理されたSamplePdfViewクラスが実行される。

上記の手順を実行した後、以下に示すようなPDFが生成されダウンロードすることができる。

FILEDOWNLOAD PDF

Picture - FileDownload PDF


4.11.3.1.2. Excelファイルの動的な生成

ExcelのレンダリングにApache POIを利用する例を紹介する。


4.11.3.1.2.1. Apache POIを使用するための設定

pom.xmlに Apache POIの定義を追加する。

<dependencies>
    <!-- omitted -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>${POI_VERSION}</version>
    </dependency>
</dependencies>

Note

Apache POI 5.1.0 以降のバージョンではApache Log4j v2を依存関係に含んでおり、POIがログ実装としてLog4j 2を直接使用するようになった。

Apache POIで出力されるログをTERASOLUNA Server Framework for Java (5.x)で設定しているSLF4Jでログを出力するためには、log4j-to-slf4jを依存関係に含む必要がある。

<dependencies>
    <!-- omitted -->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-to-slf4j</artifactId>
    </dependency>
</dependencies>

なお、Log4j 2を依存関係に含んでいる場合、Apache Commons Logging 1.3のログ実装呼び出し順の関係でSLF4JではなくLog4j 2が優先される可能性があるが、blankプロジェクトから生成されたプロジェクトではcommons-logging.propertiesでSLF4Jを優先的に呼び出すように設定しているため、Log4j 2が優先されることはない。

Warning

SLF4J adapter (log4j-to-slf4j) とSLF4J bridge (log4j-slf4j-impl) を一緒に使用すると、SLF4Jと Log4j 2の間でイベントが際限なくルーティングされてしまうため注意すること。

詳しくは、Log4j 2 to SLF4J Adapterを参照されたい。


4.11.3.1.2.2. カスタムViewの実装
@Component
public class SampleExcelView extends AbstractFileDownloadView { // (1)

    @Autowired
    private ExcelHelper excelHelper; // (2)

    @Override
    protected InputStream getInputStream(Map<String, Object> model, HttpServletRequest request)
            throws IOException {
        return excelHelper.createServerTimeExcel(model); // (2)
    }

    @Override
    protected void addResponseHeader(Map<String, Object> model, HttpServletRequest request,
            HttpServletResponse response) {
      String encodedFileName = URLEncoder.encode("サンプル.xlsx", StandardCharsets.UTF_8);
      String contentDisposition =
              String.format("attachment; filename*=UTF-8''%s", encodedFileName);

      response.setCharacterEncoding("UTF-8");
      response.setHeader("Content-Disposition", contentDisposition); // (3)
      response.setContentType(
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); // (4)
    }
}

項番

説明

(1)
AbstractFileDownloadViewを継承したカスタムViewクラスを実装する。
(2)
EXCELファイルを生成するためのヘルパークラスを利用する。
ヘルパークラスの実装例は後述する。
(3)
Content-Dispositionを設定する。
上記例では、attachment; filename*=UTF-8''サンプル.xlsxを指定しているため、サンプル.xlsxというEXCELファイルがダウンロードされる。
(4)
ContentTypeを設定する。
XLSXフォーマットのEXCELファイルとして扱うため、application/vnd.openxmlformats-officedocument.spreadsheetml.sheetを指定している。

4.11.3.1.2.3. ヘルパークラスの実装
Viewに処理を書くのではなく、ヘルパークラスで処理を実装する。
ヘルパークラスを作成することで再利用性を高めることができる。
下記サンプルでは、modelに設定されたserverTimeをEXCELに出力する単純な例となる。
本サンプルで紹介していない機能を使用したい場合は、Apache POIのHow to useを参考に実装されたい。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

@Component
public class ExcelHelper {

    public InputStream createServerTimeExcel(Map<String, Object> model) throws IOException {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             Workbook workbook = new XSSFWorkbook()) { // (1)

            Sheet sheet = workbook.createSheet("サンプル"); // (2)
            sheet.setDefaultColumnWidth(12);

            Cell titleCell = getCell(sheet, 0, 0);
            titleCell.setCellValue("サンプル Excel"); // (3)

            String serverTime = model.get("serverTime") != null ? model.get("serverTime").toString()
                    : "Server Time not available";
            Cell timeCell = getCell(sheet, 2, 0);
            timeCell.setCellValue("Server Time: " + serverTime); // (4)

            workbook.write(outputStream); // (5)

            return new ByteArrayInputStream(outputStream.toByteArray());
        } catch (Exception e) {
            throw new IOException("Failed to create Excel document", e);
        }
    }

    private Cell getCell(Sheet sheet, int rowNumber, int cellNumber) {
        Row row = sheet.getRow(rowNumber);
        if (row == null) {
            row = sheet.createRow(rowNumber);
        }
        return row.createCell(cellNumber);
    }
}

項番

説明

(1)
Workbookを生成する。
上記例ではXLSXフォーマットのEXCELファイルを生成している。
(2)
サンプルシートの作成。
(3)
A1セルの値としてサンプル Excelを設定。
(4)
A3セルの値としてServer Time: {serverTime}を設定。
(5)
Workbookの内容をOutputStreamに書き込む。

4.11.3.1.2.4. ControllerでのViewの指定
BeanNameViewResolverにより、コントローラでsampleExcelViewを返却することで、Springのコンテキストで管理されたBeanIDがsampleExcelViewのViewが使用される。

Javaソース

@GetMapping(value = "sample", params= "excel")
public String sampleExcel(Model model) {
    model.addAttribute("serverTime", LocalDateTime.now());
    return "sampleExcelView"; // (1)
}

項番

説明

(1)
sampleExcelViewをメソッドの戻り値として返却することで、Springのコンテキストで管理されたSampleExcelViewクラスが実行される。

上記の手順を実行した後、以下に示すようなEXCELが生成されダウンロードすることができる。

FILEDOWNLOAD EXCEL

Picture - FileDownload EXCEL