JSP/ServletでWebアプリを作ってみた

# はじめに
今回も駆け出しのエンジニアが記載する内容となりますが、開発の手順やHTTPやWebサイトの基礎を理解するための学習手段として自作のWebアプリを作成してみました。
個人的に興味のあるお菓子のレシピ投稿アプリになります。

こちらの記事では、以下の目次の通りJSP/Servletの特徴や、制作したアプリケーションの機能などをご紹介していきたいと思います。

### 目次
- [JSP/Servletとは](#jspservletとは)
    - [JSPの特徴](#jspの特徴)
    - [Servletの特徴](#servletの特徴)
- [使用した環境・言語](#使用した環境言語)
- [構造とデータフロー](#構造とデータフロー)
- [機能の詳細](#機能の詳細)
    - [ページング機能](#ページング機能)
    - [お気に入り登録機能](#お気に入り登録機能)
    - [画像アップロード機能](#画像アップロード機能)
- [アプリケーションの実行例](#アプリケーションの実行例)
- [まとめ](#まとめ)

# JSP/Servletとは
Servlet/JSPは、Webサーバ上でJavaを実行し、動的にWebページを生成するための技術です。
 ServletとJSPは一緒に使用することで、データ（モデル）、ビュー（JSPページ）、コントローラー（Servlet）を分離するMVCアーキテクチャを実装することが容易になります。

Servlet/JSPは昔から存在するテクノロジーで、多くの設定を手動で行う必要があります。
そのため、現在ではよりモダンなフレームワークや技術（Spring BootやNode.jsなど）に徐々に置き換えられています。
しかし、JSPとServletの知識はHTTPとWebの基本を深く理解するのに役立つため、今回の開発ではJSP/Servletの技術を使用しました。

それぞれの特徴は以下となります。

### JSPの特徴
* HTMLの一部として、Javaのコードを記述することができる
* タグライブラリを使って、より簡潔で再利用可能なコードを作成することができる
* JSPはJavaで書かれているため、Javaの強力な機能をすべて利用することができる(例外処理、オブジェクト指向プログラミングなどを含む)
* HTMLとJavaの組み合わせにより、プログラマーとウェブデザイナーがそれぞれの専門知識を活かして協力することが可能

### Servletの特徴
* Javaで書かれたServletは、プラットフォームに依存せずに動作する
* 一度ロードされるとメモリ内に常駐し、複数のユーザーリクエストを同時に処理することができる
* Servletの部分を追記するだけで自動的に機能を拡張できる


# 使用した環境・言語
### 環境
Eclipse
Tomcat9
MySQL
Maven(Javaのプロジェクト管理とビルド自動化のツール)
Hibernate(オブジェクト-リレーショナルマッピング (ORM) フレームワーク)

### 言語
Java(バージョン11)
JDBC(Javaとデータベースとの間の接続を提供するAPI)
JavaScript
HTML
CSS


# 構造とデータフロー

アプリケーションのアーキテクチャとしてはMVC（Model-View-Controller）モデルによって成り立っています。

ディレクトリ構造は以下となります。
```
recipepad
│  .gitignore
│  README.md
│  pom.xml
│
├─.settings
├─build
├─src
│  ├─controllers
│  │      ClearErrorServlet.java
│  │      CreateServlet.java
│  │      DestroyServlet.java
│  │      EditServlet.java
│  │      ErrorServlet.java
│  │      FavoriteListServlet.java
│  │      FavoriteServlet.java
│  │      IndexServlet.java
│  │      NewServlet.java
│  │      ShowServlet.java
│  │      UpdateServlet.java
│  │      
│  ├─filters
│  │      EncodingFilter.java
│  │      
│  ├─models
│  │      Favorite.java
│  │      Recipe.java
│  │      
│  └─utils
│          DBUtil.java
│          
├─target
└─WebContent
   ├─css
   ├─META-INF
   └─WEB-INF
       └─views
           ├─layout
           │      app.jsp
           │      
           └─recipes
               edit.jsp
               error.jsp
               favoriteList.jsp
               index.jsp
               new.jsp
               show.jsp

```
※実際のコードはこちらになります。
https://github.com/NanankoMaeda/recipepad

MVCモデルに当てはめたデータフローを簡単に説明します。

##### Model（モデル）
> recipepad\src\models

データとそのデータに対する操作、つまりビジネスロジックを表現します。
JavaのWebアプリケーションでは、この部分は通常、JavaBeansやデータアクセスオブジェクト（DAO）として表現されます。これらはデータベースからデータを取得したり、データをデータベースに保存したりする操作を担当します。


##### View（ビュー）
> recipepad\WebContent\WEB-INF\views

ユーザーに表示される部分であり、Servlet/JSPでは主にJSPファイルによって実現されます。
JSPファイルは動的にHTMLを生成し、コントローラから渡されたモデル（データ）を使ってユーザーインターフェースを生成します。

##### Controller（コントローラ）
> recipepad\src\controllers

ユーザーからの入力をModelやViewに伝える役割を持ちます。
Servlet/JSPでは、Servletがコントローラの役割を果たします。ユーザーの入力に応じてModelからデータを取得し、それをViewに渡してユーザーに表示します。


# 機能の詳細
アプリケーションの機能としましては基本的なCRUDに加え、ユーザーが使いやすくなるような機能をいくつか追加しています。
その中から3つの機能を取り上げて説明していきたいと思います。


## ページング機能
ページング機能は、大量のデータや情報を見やすく表示するための補助機能です。
これにより、ユーザーは一度にすべての情報をスクロールすることなく、必要な情報に簡単にアクセスできます。

具体的な実装としては、一覧ページに表示するレシピの数を10件に制限し、ユーザーが別のページに移動することでさらなるレシピを見ることができます。

[IndexServlet.java]
```java
// ...

public class IndexServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    // 1ページあたりのレシピ数(10件)
    private static final int MAX_RESULTS = 10;

    // ...

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        EntityManager em = DBUtil.createEntityManager();

        //  ページ数の取得
        int page = 1;
        if (request.getParameter("page") != null) {
            page = Integer.parseInt(request.getParameter("page"));
        }

        TypedQuery<Recipe> query = em.createNamedQuery("getAllRecipes", Recipe.class);
        // ページあたりの結果数とオフセットの設定
        query.setFirstResult((page - 1) * MAX_RESULTS);
        query.setMaxResults(MAX_RESULTS);
        List<Recipe> recipes = query.getResultList();

        // レシピの全件数と総ページ数の計算
        long totalRecipes = em.createNamedQuery("countAllRecipes", Long.class).getSingleResult();
        long totalPages = (totalRecipes + MAX_RESULTS - 1) / MAX_RESULTS; 

        em.close();

        request.setAttribute("recipes", recipes);
        // 総ページ数と現在のページ数の情報をリクエストにセット
        request.setAttribute("totalPages", totalPages);
        request.setAttribute("currentPage", page);

        // ...

        RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/recipes/index.jsp");
        rd.forward(request, response);
    }
}

```
[index.jsp]
```jsp
<!-- ページングリンク -->
<c:forEach var="i" begin="1" end="${totalPages}">
    <c:choose>
        <c:when test="${i == currentPage}">
            <span class="pagination-item current-page"><c:out value="${i}" /></span>
        </c:when>
        <c:otherwise>
            <a href="${pageContext.request.contextPath}/index?page=${i}" class="pagination-item"><c:out value="${i}" /></a>
        </c:otherwise>
    </c:choose>
</c:forEach>
```



## お気に入り登録機能
お気に入り登録機能は、ユーザーが特定のレシピを後で簡単に見つけられるようにするための補助機能です。
特に気に入ったレシピを保存し、お気に入りレシピの一覧画面に表示できるようにすることで、後で簡単にアクセスできます。

具体的な実装方法としてはmodelsにFavoriteを作成し、お気に入りID(id)とレシピID(recipe_id)を紐づけるためにfavoritesテーブルを作って情報を管理します。



JSPとしては詳細画面のshow.jspに「お気に入り登録/解除ボタン」を追加し、お気に入りリストを登録/解除するfavoriteList.jspを作成します。

[show.jsp]
```jsp
        // ...

        <%
            //お気に入り状態をリクエスト属性から取得
            boolean isFavorited = (Boolean) request.getAttribute("isFavorited");
        %>

        <% if (isFavorited) { %>
            <!-- お気に入り解除ボタン -->
            <form class="button-container" action="favorite" method="post">
                <input type="hidden" name="id" id="id_recipe" value="${recipe.id}" />
                <button type="submit" name="action" value="remove_favorite" style="width:150px;height:50px">お気に入り解除</button>
            </form>
        <% } else { %>
            <!-- お気に入り登録ボタン -->
            <form class="button-container" action="favorite" method="post">
                <input type="hidden" name="id" id="id_recipe" value="${recipe.id}" />
                <button type="submit" name="action" value="add_favorite" style="width:150px;height:50px">お気に入り登録</button>
            </form>
        <% } %>



```
[favoriteList.jsp]
```jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <h2>お気に入りレシピ</h2>
        <ul class="index">
            <c:forEach var="favorite_recipe" items="${favorite_recipes}">
                <li>
                    <a href="${pageContext.request.contextPath}/show?id=${favorite_recipe.id}">
                        <c:out value="${favorite_recipe.id}" />
                    </a>
                    <c:out value="${favorite_recipe.title}"></c:out>
                </li>
            </c:forEach>
        </ul>

        <p><a href="${pageContext.request.contextPath}/index">一覧に戻る</a></p>
    </c:param>
</c:import>
```



Servletとしては以下の2つを作成しています。

* FavoriteServlet：レシピをお気に入りに追加したり、お気に入りから削除したりするためのServlet
　⇒show.jspへデータを受け渡しする
* FavoriteListServlet：お気に入りに追加したレシピのリストを表示するためのServlet
　⇒favoriteList.jspへデータを受け渡しする

[FavoriteServlet]
```java
package controllers;

import java.io.IOException;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import models.Favorite;
import utils.DBUtil;

/**
 * Servlet implementation class FavoriteServlet
 */
@WebServlet("/favorite")
public class FavoriteServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public FavoriteServlet() {
        super();
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String action = request.getParameter("action");
        if (action.equals("add_favorite")) {
            addFavorite(request, response);
        } else if (action.equals("remove_favorite")) {
            removeFavorite(request, response);
        }
    }

    private void addFavorite(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        EntityManager em = DBUtil.createEntityManager();
        em.getTransaction().begin();

        Favorite f = new Favorite();
        Integer recipeId = Integer.parseInt(request.getParameter("id"));
        f.setRecipe_id(recipeId);

        // 重複したレシピをお気に入りしないようにする
        TypedQuery<Integer> query = em.createQuery("SELECT f.recipe_id FROM Favorite f", Integer.class);
        List<Integer> existingIds = query.getResultList();
        if (existingIds.contains(recipeId)) {
            // エラーを返却する
            request.getSession().setAttribute("errorMessage", "このレシピは既にお気に入り登録されています。");
            response.sendRedirect(request.getContextPath() + "/error");
            return;
        }

        em.persist(f);
        em.getTransaction().commit();
        em.close();

        response.sendRedirect(request.getContextPath() + "/index");
    }

    private void removeFavorite(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        EntityManager em = DBUtil.createEntityManager();
        em.getTransaction().begin();

        // recipe_idからfavoriteを取得する
        Integer recipeId = Integer.parseInt(request.getParameter("id"));
        TypedQuery<Favorite> query = em.createQuery(
                "SELECT f FROM Favorite f WHERE recipe_id = :recipeId",
                Favorite.class)
                .setParameter("recipeId", recipeId);
        Favorite f = query.getSingleResult();

        em.remove(f);
        em.getTransaction().commit();
        em.close();

        response.sendRedirect(request.getContextPath() + "/index");
    }

}
```
[FavoriteListServlet]
```java
package controllers;

import java.io.IOException;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import models.Recipe;
import utils.DBUtil;

/**
 * Servlet implementation class FavoriteListServlet
 */
@WebServlet("/favorite_list")
public class FavoriteListServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public FavoriteListServlet() {
        super();
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        EntityManager em = DBUtil.createEntityManager();

        // データベースからfavoritesテーブルのrecipe_idを取得してListに格納
        TypedQuery<Integer> query = em.createQuery("SELECT f.recipe_id FROM Favorite f", Integer.class);
        List<Integer> recipeIds = query.getResultList();

        // RecipeデータベースからrecipeIdsに一致するRecipeを取得する(IN句を使う)
        TypedQuery<Recipe> recipesQuery = em.createQuery(
                "SELECT r FROM Recipe r WHERE id IN (:recipeIds)",
                Recipe.class)
                .setParameter("recipeIds", recipeIds);
        List<Recipe> favoriteRecipes = recipesQuery.getResultList();

        em.close();

        // お気に入りListをリクエストスコープに保存
        request.setAttribute("favorite_recipes", favoriteRecipes);
        RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/recipes/favoriteList.jsp");
        rd.forward(request, response);
    }

}
```

## 画像アップロード機能
画像アップロード機能は、レシピの出来上がりイメージの画像をアップロードして表示できるようにする補助機能です。

具体的には、フロントエンドから受け取った画像データをサーバー側で適切に処理し、データベースにファイル名を保存します。
表示する機能としてはローカルのファイルのパスを渡して、データを取得できるようになっています。

こちらの機能は以下のサイトを参考にさせていただきました。
追加部分としては新規作成・編集・詳細画面に組み込んでいます。

https://joytas.net/programming/jsp_servlet/fileupload



# アプリケーションの実行例

[一覧画面]
![スクリーンショット 2023-07-17 135424.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834935/888024f9-81f5-21c0-cb92-34f207626b5a.png)

[新規作成画面]
![スクリーンショット 2023-07-17 135613.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834935/8726f1c2-7406-c21f-7aa0-1944e3eb56f7.png)

[詳細画面]
![スクリーンショット 2023-07-17 135410.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834935/e5913e0e-dce0-f8fa-94fc-96c5eee412d9.png)

[編集画面]
![スクリーンショット 2023-07-17 135512.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834935/25856f9d-844c-078e-a167-7e6d31d8a8ed.png)

[更新後画面]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1834935/c437070f-98de-034f-3cb9-b0874f626faa.png)


# まとめ

今回は開発手順やWebサイトの基本的な仕組みを理解することを目的としてJSP/Servletでの開発を実施しましたが、様々なメリットやデメリットがあることがわかりました。
設定の手順が少し多かったですが環境構築ができてしまえば、構造としては理解しやすく作業を進めやすかったと思います。

最後までお読みいただき、ありがとうございました。