サンプルコードで身につける: DispatcherServlet と組み込み Tomcat
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1起動ログに出る Tomcat の4行
から起動したときのログのうち、 に関する行です(行頭の時刻などは落としています)。別に用意したサーバへ置いたのではなく、自分のプロセスの中で Tomcat 11.0.24 が立ち上がり、ポート 8080 を開いたことが読み取れます。
o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)o.apache.catalina.core.StandardService : Starting service [Tomcat]o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.24]o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2Servlet は1つ、フィルタは3つ
--logging.level.org.springframework.boot=DEBUG を付けて起動し、最初のリクエストを送ったときのログです。登録された は の1つだけで、対応づけは / でした。初期化の行は main ではなく nio-8080-exec-1 ので出ており、最初のリクエストが来てから行われたことが分かります。
DEBUG [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping filters: characterEncodingFilter urls=[/*] order=-2147483648, formContentFilter urls=[/*] order=-9900, requestContextFilter urls=[/*] order=-105DEBUG [ main] o.s.b.w.s.ServletContextInitializerBeans : Mapping servlets: dispatcherServlet urls=[/]INFO [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'INFO [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'INFO [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 msDEBUG [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/books/42", parameters={}
3ハンドラの中で取ったスタックトレース
の中でを作り、そのを文字列にして返した結果の抜粋です。全体は45行(フレームは44)あり、... の行は省略を表します。下から順に、スレッド、Tomcat の HTTP 解釈、、Servlet API、DispatcherServlet、、自分のと積み上がっています。
java.lang.Exception: stack at handlerat l19.web.BookController.stack(WebApp.java:35)...at java.base/java.lang.reflect.Method.invoke(Method.java:565)at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:252)...at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:853)...at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)...at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:892)...at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710)at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:132)...at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:199)...at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:165)...at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:347)at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:406)...at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:2036)...at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:483)...at java.base/java.lang.Thread.run(Thread.java:1474)
4登録されていた変換係の一覧
が持つ を列挙した結果です。6つあり、application/json を担当するのは最後の JacksonJsonHttpMessageConverter でした。 本体は 3.1.5 で名は tools.jackson ですが、の だけは com.fasterxml.jackson.core の 2.21 が入っていました。
org.springframework.http.converter.ByteArrayHttpMessageConverter [application/octet-stream, */*]org.springframework.http.converter.StringHttpMessageConverter [text/plain, */*]org.springframework.http.converter.ResourceHttpMessageConverter [*/*]org.springframework.http.converter.ResourceRegionHttpMessageConverter [*/*]org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter [application/x-www-form-urlencoded, multipart/form-data, multipart/mixed, multipart/related]org.springframework.http.converter.json.JacksonJsonHttpMessageConverter [application/json, application/*+json]jackson-databind = 3.1.5thread = Thread[#45,http-nio-8080-exec-5,5,main]
5対比: 同じ応答を素の Servlet で書くと
DispatcherServlet が肩代わりしている仕事を見るために、同じ応答を だけで書いた形です。URL から ID を取り出す、Content-Type を決める、JSON の文字列を組み立てる、をすべて自分で書いており、 ではこの3つが DispatcherServlet の内側の部品に移っています。/books/* に対応づけて Tomcat 11.0.26 に置くと同じ本文が返り、Content-Type は application/json;charset=UTF-8 でした。
// 素の Servlet で GET /books/42 に答える形(対比用)
// web.xml などで /books/* に対応づけて登録した場合に動く。完全一致で登録すると getPathInfo() は null になる
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class BookServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. URL から ID を取り出す(Spring MVC では @PathVariable)
String path = req.getPathInfo(); // 例: "/42"
long id = Long.parseLong(path.substring(1));
// 2. Content-Type を決める(Spring MVC では変換係が決める)
resp.setContentType("application/json");
// 3. JSON を組み立てる(Spring MVC では JacksonJsonHttpMessageConverter)
resp.getWriter().print(
"{\"id\":" + id + ",\"title\":\"Spring in Depth\",\"tags\":[\"java\",\"spring\"]}");
}
}
// Spring MVC ではこれが次の形になる
// @GetMapping("/books/{id}")
// Book book(@PathVariable long id) {
// return new Book(id, "Spring in Depth", List.of("java", "spring"));
// }