アイキャッチ

設計・テスト・レビュー

Spring Framework初心者が知るべき考え方(ハリウッドの原則って何だ)

このページに書いてあること

Spring MVCのControllerに @GetMapping を付けると、誰がそのメソッドを呼ぶのでしょうか。GET /greeting と POST /greeting を送り、Springから同じメソッドが呼ばれる条件を比べます。

Frameworkってなんだっけ問題

Base64変換のライブラリを使うときは、自分のコードが必要な箇所で変換メソッドを呼びます。Spring MVCでHTTPを受け付けるときは、リクエストを受け取った側が条件に合うControllerを選びます。違いが分かると、処理が始まらないときにどこを調べるか決められます。

ありがちなFrameworkの理解

ControllerからService、ServiceからRepositoryへ進む矢印は、自分が実装する呼び出しです。この図だけでは、そもそも誰がControllerの入口を呼ぶかが分かりません。

ControllerからService、Repositoryへ続くアプリ内の呼び出し
Controllerの先は見えますが、手前の入口が描かれていません。

ハリウッドの原則って何だ問題

呼び出し順の一部をフレームワークが持ち、条件がそろった時点でアプリが用意した処理を呼ぶ関係を、ハリウッドの原則と呼びます。Spring MVCでは DispatcherServlet がリクエストを受け、マッピングの条件に合うハンドラへ処理を渡します。@GetMapping("/greeting") は、GETでそのパスに来たときの入口を登録します。DispatcherServletのリクエスト処理

Spring MVCがリクエストを受け付けてControllerに渡す関係図
上段の「リクエスト受付機能 → Controller」が、この章で確認する矢印です。ControllerからServiceへのメソッド呼び出しは、通常はアプリ側のコードが書きます。

実行用サンプル

Spring MVCのMockMvcで、HTTPメソッドだけを変えた2つのケースを検証します。TomcatなどのサーバーやSpring Bootは起動しません。

JDK 17、Maven 3.9系、Spring Framework 6.2.19。ZIPにはソース全文、pom.xml、READMEを収録しています。展開先で mvn test を実行します。

次のControllerは greet() が呼ばれた回数を、テストから確かめるために記録します。

GreetingController.java の全文です。

package example;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
final class GreetingController {
    private int callCount;

    @GetMapping("/greeting")
    public String greet() {
        callCount++;
        return "Hello";
    }

    int callCount() {
        return callCount;
    }
}

MvcCallbackTest.java の全文です。

package example;

import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

class MvcCallbackTest {
    @Test
    void getCallsTheControllerAndUsesItsReturnValue() throws Exception {
        var controller = new GreetingController();
        var mvc = MockMvcBuilders.standaloneSetup(controller).build();

        var response = mvc.perform(get("/greeting")).andReturn().getResponse();

        assertEquals(200, response.getStatus());
        assertEquals("Hello", response.getContentAsString());
        assertEquals(1, controller.callCount());
    }

    @Test
    void postDoesNotCallAControllerMappedToGet() throws Exception {
        var controller = new GreetingController();
        var mvc = MockMvcBuilders.standaloneSetup(controller).build();

        var response = mvc.perform(post("/greeting")).andReturn().getResponse();

        assertEquals(405, response.getStatus());
        assertEquals(0, controller.callCount());
    }
}

GETでは greet() が1回呼ばれ、HTTP 200と本文 Hello が返ります。POSTでは同じパスでもHTTP 405が返り、呼び出し回数は0です。テストはControllerを登録してHTTPリクエストを渡しており、greet() を直接呼んでいません。

Frameworkで問題が起きた時の勘所

Controllerの先頭にブレークポイントを置き、同じリクエストで止まるかを確認します。止まらず405なら、送ったHTTPメソッドと @GetMapping などの条件を突き合わせます。404なら、実際のリクエストパスやControllerの登録を確認します。メソッドに入ってから失敗したら、その時点の引数と下流の例外を見ます。

Beanを誰が組み立てるかと、処理を誰が呼ぶかは別の観点です。Springは通常のシングルトンBeanをコンテキスト初期化時に組み立てて注入し、Controllerの業務コードはそのServiceを呼び出します。フレームワークがすべてのServiceをリクエストのたびに作るわけではありません。

実際に遭遇した具体例

旧Spring MVCのシステムで、Serviceが必要とするSpring Data JPAのRepositoryが登録されず、起動に失敗したことがあります。Serviceの呼び出し側を変更しても解決せず、Repositoryをどこで登録する構成かを調べました。リクエストを受ける前に止まる場合は、処理本体に入る前の登録や初期化を確認します。

当時のXML構成では、通常のコンポーネントスキャンに加え、jpa:repositories の base-package でRepositoryインターフェースの場所を指定しました。現在のJava設定では @EnableJpaRepositories、Spring Bootでは自動設定で探索される範囲を、採用している構成に合わせて確認します。Spring Data JPAのRepository登録

-設計・テスト・レビュー
-,