サーブレットからフレームワークへ
このレッスンでは、Web サーバーに届く HTTP リクエストが本当はただの文字列であることを確かめ、Spring Boot がその文字列をどこまで面倒みてくれているのかを掴みます。
HTTP リクエストは、ただのテキストです
ブラウザのアドレス欄に URL を打ち込むと、裏側ではサーバーに向けて次のようなテキストが送られています。
プレーンテキスト
GET /users/12 HTTP/1.1
Host: example.com
Accept: application/json
User-Agent: Mozilla/5.0
1 行目を リクエストライン と呼びます。半角スペース区切りで 3 つの要素が並んでいて、順に HTTP メソッド、パス、HTTP のバージョンです。2 行目以降が ヘッダ で、名前 と値を区切ってずらりと並びます。ヘッダの最後には空行が 1 行入り、その後ろに ボディ が続きます。POST であればここに JSON が入ります。
つまりサーバーの側から見ると、届くのはバイト列であり、それを人間が決めた規則にしたがって切り刻んでいくのが仕事の始まりです。
サーブレットの時代は、この解析だけは肩代わりされていました
Java の世界には古くから サーブレット という仕様があります。Tomcat のようなサーブレットコンテナが TCP 接続の待ち受けと HTTP の解析を引き受けてくれて、開発者は解析済みのオブジェクトを受け取るところから書けます。
Java
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws IOException {
String path = req.getPathInfo(); // "/12"
String id = path.substring(1); // "12"
res.setStatus(200);
res.setContentType("application/json");
res.getWriter().write("{\"id\":" + id + "}");
}
}たしかに生のバイト列を切り刻む苦労は消えました。それでもまだ大量の手作業が残っています。パスから ID を取り出すのは自分で substring しています。ID が数字でなかったらどうするかも自分で書きます。返す JSON も文字列の連結で組み立てています。項目が 10 個あるオブジェクトになったら、この連結は一瞬で読めなくなります。しかもこのサーブレットをどの URL に結びつけるかは、web.xml という別のファイルに書く必要がありました。
Spring Boot はここまで肩代わりします
同じ処理を Spring Boot で書くと次のようになります。
Java
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User find(@PathVariable Long id) {
return userService.findById(id);
}
}行数が減ったこと以上に、消えた判断 に注目してください。どの URL のときにこのメソッドを呼ぶかは @GetMapping の 1 行になりました。パスから ID を切り出す処理は @PathVariable が引き受け、しかも String から Long への変換まで済ませてくれます。戻り値の User オブジェクトは Jackson というライブラリが自動で JSON に変換し、Content-Type ヘッダも application/json が自動で付きます。ステータスコードも、正常に返れば 200 が入ります。
肩代わりされているものを整理すると次の通りです。
| 工程 | 生のサーブレット | Spring Boot |
|---|---|---|
| TCP と HTTP の解析 | サーブレットコンテナ | 組み込み Tomcat |
| URL とコードの結び付け | web.xml に手書き | @GetMapping |
| パス変数の取り出しと型変換 | 自分で substring | @PathVariable |
| ボディの JSON をオブジェクトへ | 自分で解析 | @RequestBody |
| 戻り値を JSON へ | 自分で文字列連結 | Jackson が自動変換 |
| ステータスとヘッダ | 自分で設定 | 既定値が入る |
便利ではありますが、この表の左側が「なくなった」わけではありません。誰かがやっています。今回の演習では、その左側の一番入口にあたる処理を自分で書いてみます。
演習でやること
リクエストラインの文字列からメソッドとパスを取り出します。Spring Boot の中では、組み込み Tomcat が毎リクエストこれと同じ切り出しを行い、その結果を使って Spring がどのコントローラのメソッドを呼ぶかを決めています。ここで一度自分で書いておくと、次のレッスンのルーティング表の話がそのまま繋がります。
文字列の分解には String の split を使うのが素直です。"GET /users/12 HTTP/1.1".split(" ") は 3 要素の配列になり、[0] がメソッド、[1] がパスです。ただし現実の入力は必ずしもきれいではないので、要素が 3 つ揃っていない場合の扱いも決めておく必要があります。フレームワークを使わないと、こうした細部が全部自分の責任になる、というのがこのレッスンで一番持ち帰ってほしい感覚です。
要件
- リクエストラインを半角スペースで分解し、1つめをメソッド、2つめをパスとして "GET|/users/12" の形で返すこと
- 分解した結果が3要素でない場合は "bad-request" を返すこと
- 引数が null または空白のみの場合も "bad-request" を返すこと
入出力例
parseRequestLine("GET /users/12 HTTP/1.1") → "GET|/users/12"
parseRequestLine("POST /users HTTP/1.1") → "POST|/users"
parseRequestLine("DELETE /users/9 HTTP/1.1") → "DELETE|/users/9"
parseRequestLine("GET /users") → "bad-request"
parseRequestLine(" ") → "bad-request"
parseRequestLine("PUT /a/b/c HTTP/1.1") → "PUT|/a/b/c"