メインコンテンツまでスキップ

サーバーサイド編(追加)

POSTへの対応

HTTP の POST に対応します。POST で送信されたデータを標準出力に表示します。

ソースコードの修正

src/main/java/com/example/HelloServlet.javaを修正します。

HelloServlet.java
package com.example;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

/**
* 共通処理: GET/POSTのパラメータを同様に取得して応答を生成します。
*/
private void process(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 文字コードをUTF-8に統一
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json; charset=UTF-8");

Map<String, String> resMap = new HashMap<>();
try {
Map<String, String[]> params = req.getParameterMap();
if (params.isEmpty()) {
throw new IllegalArgumentException("No parameters");
}
// 受信パラメータをログ出力
for (String key : params.keySet()) {
for (String value : params.get(key)) {
System.out.println(key + " = " + value);
}
}
resp.setStatus(HttpServletResponse.SC_OK);
resMap.put("RET", "0000");
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
System.out.println("Raw query string: " + req.getQueryString());
resMap.put("RET", "9999");
resMap.put("ERROR", e.getMessage());
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}

// JSONで応答
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(resMap);
try (PrintWriter out = resp.getWriter()) {
out.println(json);
out.flush();
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
process(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
process(req, resp);
}
}

ソースコードを修正したらデプロイします。

動作確認

PowerShell のcurlコマンドはInvoke-WebRequestコマンドのエイリアス(別名)です。Linux や MacOS で使うcurlコマンドとパラメータや動作が異なります。API テストや Web 開発は本物のcurl.exeを使う方が便利です。

winget コマンドでインストールします。

winget install cURL.cURL

次のコマンドで HTTP GET の動作を確認します。

curl -G http://localhost:8080/servlet-demo/hello 
--data-urlencode "name=鈴木太郎" --data-urlencode "age=37"

次のコマンドで、HTTP POST の動作を確認します。

curl -X POST http://localhost:8080/servlet-demo/hello 
`-H "Content-Type: application/x-www-form-urlencoded"` -d "name=鈴木太郎&age=37"

ロガーの導入

アプリケーションにログの機能を追加します。ログの機能を実現する方には様々な方法があります。今回は、SLF4J と Logback を使います。SLF4J (Simple Logging Facade for Java) は、ロギングのインタフェースを提供します。実際のログ出力は別の実装に委譲します。Logback は SLF4J の推奨実装です。設定は logback.xml で行います。

依存関係の追加

pom.xmlに Logback の依存関係を追加します。

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.x</version>
</dependency>

Logback の設定ファイル

ログを標準出力(コンソール)とファイル/var/log/tomcat10/app.logに出力します。 ファイルsrc/main/resource/logback.xmlを作成します。

logback.xml
<!-- src/main/resources/logback.xml -->
<configuration>
<!-- コンソール出力 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- JST表示。%d{ISO_OFFSET_DATE_TIME,Asia/Tokyo} 等も可 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS,Asia/Tokyo} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- 日毎ローテーションのファイル出力 -->
<appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS,Asia/Tokyo} %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>

<!-- ルートロガー -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="APP_FILE"/>
</root>

<!-- パッケージ単位で詳細化(例:自作コードはDEBUG) -->
<logger name="com.example" level="DEBUG"/>
</configuration>

アプリケーションの修正

SLF4J を使うようにsrc/main/java/com/example/HelloServlet.javaを修正します。

HelloServlet.java
package com.example;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger; // ロギング用に追加
import org.slf4j.LoggerFactory; // ロギング用に追加
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

// ロガーの生成
private static final Logger LOG = LoggerFactory.getLogger(HelloServlet.class);
private static final ObjectMapper MAPPER = new ObjectMapper();

private void process(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json; charset=UTF-8");

Map<String, Object> resMap = new HashMap<>();
Map<String, Object> echo = new HashMap<>();

// JSTタイムスタンプ(ISO_OFFSET_DATE_TIME)
String ts = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

try {
Map<String, String[]> params = req.getParameterMap();

for (String key : params.keySet()) {
String[] values = params.get(key);
for (String v : values) {
LOG.debug("{} = {}", key, v); // ログ出力
}
echo.put(key, values.length == 1 ? values[0] : Arrays.asList(values));
}

resMap.put("RET", "0000");
resMap.put("params", echo);
resMap.put("timestamp", ts);
resp.setStatus(HttpServletResponse.SC_OK); // 成功時のみOKに設定

} catch (RuntimeException e) {
// 予期せぬ実装バグなど
LOG.error("Unexpected server error", e); // ログ出力
resMap.put("RET", "9999");
resMap.put("ERROR", "Unexpected server error");
resMap.put("params", echo);
resMap.put("timestamp", ts);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500
}

// JSON生成と書き込み
String json = MAPPER.writeValueAsString(resMap);
try (PrintWriter out = resp.getWriter()) {
out.println(json);
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LOG.info("Request from {} {}", req.getMethod(), req.getRequestURI()); // ログ出力
process(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LOG.info("Request from {} {}", req.getMethod(), req.getRequestURI()); // ログ出力
process(req, resp);
}
}

Tomcat にデプロイします。

ロギング機能の動作確認

curl コマンドで HTTP GET を実行します。

curl.exe -G http://localhost:8080/servlet-demo/hello 
--data-urlencode "name=鈴木太郎" --data-urlencode "age=37"

応答が確認できます。

{
"RET": "0000",
"params": {
"name": "鈴木太郎",
"age": "37"
},
"timestamp": "2025-11-09T20:11:10.572529924+09:00"
}

curl コマンドで HTTP POST を実行します。

curl -X POST http://localhost:8080/servlet-demo/hello 
`-H "Content-Type: application/x-www-form-urlencoded"` -d "name=鈴木太郎&age=37"

応答が確認できます。

{
"RET": "0000",
"params": {
"name": "鈴木太郎",
"age": "37"
},
"timestamp": "2025-11-09T21:16:29.768942856+09:00"
}

Linux でログファイルを確認します。

$ tail /var/log/tomcat10/app.log
2025-11-09 21:20:44.559 INFO com.example.HelloServlet - Request from GET /servlet-demo/hello
2025-11-09 21:20:44.560 DEBUG com.example.HelloServlet - name = 鈴木太郎
2025-11-09 21:20:44.560 DEBUG com.example.HelloServlet - age = 37
2025-11-09 21:20:47.667 INFO com.example.HelloServlet - Request from POST /servlet-demo/hello
2025-11-09 21:20:47.668 DEBUG com.example.HelloServlet - name = 鈴木太郎
2025-11-09 21:20:47.669 DEBUG com.example.HelloServlet - age = 37

データをJSONでPOST

これまでのプログラムはクエリパラメータでデータを送信していました。フロントエンド(ブラウザ)でJavaScriptを使えば、フォームに入力されたデータをJSONでPOSTできます。JSONの処理はOSSのFasterXML/jacksonを使います。

HelloServlet.java
package com.example;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
private static final Logger LOG = LoggerFactory.getLogger(HelloServlet.class);
private static final ObjectMapper MAPPER = new ObjectMapper(); // jacksonのORM (Object Relation Mapper)

private void process(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("application/json; charset=UTF-8");

Map<String, Object> resMap = new HashMap<>();
Map<String, Object> echo = new LinkedHashMap<>(); // ★ 受信JSONのエコー用

// JSTタイムスタンプ(ISO_OFFSET_DATE_TIME)
String ts = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

try {
// ★ JSON対応:Content-Type を見て JSON の場合はボディをパース
String contentType = req.getContentType();
boolean isJson = contentType != null && contentType.toLowerCase(Locale.ROOT).contains("application/json");

if (isJson) {
// ボディを Map として受け取る(オブジェクト想定)
try {
// InputStreamとマッピングする型を指定してObjectMapper#TypeReferenceオブジェクトを渡す
echo = MAPPER.readValue(req.getInputStream(), new TypeReference<Map<String, Object>>() {});
// 空ボディ等で readValue が失敗した場合は空オブジェクト扱い
if (echo == null) {
echo = new LinkedHashMap<>();
}
} catch (Exception parseEx) {
// 仕様上は検証エラーにしないので、ログのみ出して空オブジェクトで進行
LOG.debug("Failed to parse JSON body: {}", parseEx.toString());
echo = new LinkedHashMap<>();
}
} else {
// ★ 非JSONのときも仕様上はエラーにしない(空で返す)。
LOG.debug("Non-JSON request received (Content-Type: {}). Echo as empty object.", contentType);
}

// レスポンス作成
resMap.put("RET", "0000"); // ★ 成功
resMap.put("params", echo); // ★ JSONのエコー
resMap.put("timestamp", ts);
resp.setStatus(HttpServletResponse.SC_OK);

} catch (RuntimeException e) {
// 予期せぬ実装バグなど
LOG.error("Unexpected server error", e);
resMap.put("RET", "9999");
resMap.put("ERROR", "Unexpected server error");
resMap.put("params", echo);
resMap.put("timestamp", ts);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500
}

// JSON生成と書き込み
String json = MAPPER.writeValueAsString(resMap);
try (PrintWriter out = resp.getWriter()) {
out.println(json);
}
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LOG.info("Request from {} {}", req.getMethod(), req.getRequestURI());
process(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
LOG.info("Request from {} {}", req.getMethod(), req.getRequestURI());
process(req, resp);
}
}

curlコマンドでJSONをPOSTしてテストします。nameとageを送信します。

curl.exe -X POST http://localhost:8080/servlet-demo/hello 
-H "Content-Type: application/json" -d "{\"name\":\"田中太郎\",\"age\":28}"

次の応答が表示されます。

{
"RET": "0000",
"params": {
"name": "田中太郎",
"age": 28
},
"timestamp": "2025-11-11T11:18:53.864661845+09:00"
}

課題

課題1

HTML のフォームで「氏名」と「年齢」を入力するホームページを作成します。「送信」ボタンをクリックすると、HTTP GET でデータを送信します。 応答の JSON を解析して、RET が0000なら「OK」を表示します。0000以外は「NG」を表示します。

課題2

HTML のフォームで「氏名」と「年齢」を入力するホームページを作成します。「送信」ボタンをクリックすると、HTTP POST でデータを送信します。 応答の JSON を解析して、RET が0000なら「OK」を表示します。0000以外は「NG」を表示します。

課題3

  • ブラウザーで身長[\m]と体重[kg]を入力します。「計算」ボタンをクリックすると、データをサーバーに送信します。
  • サーバは、受信したデータで BMI を計算します。BMI の値(小数点以下2桁)とメッセージをJSONで返信します。
  • ブラウザーは、受信した JSON を解析して、BMI 値とメッセージをホームページに表示します。