はじめに (対象読者・この記事でわかること)
この記事は、JavaでWebアプリケーションを開発しているが「1つのサーブレットコンテナ(Tomcatなど)で、サブドメインごとに別の処理を実行したい」と考えている方を対象としています。
たとえば、api.example.com へのアクセスはAPI用のServletに、admin.example.com へのアクセスは管理画面用のServletに振り分けたい、といった要件に対応したい方です。
この記事を読むことで、以下のことがわかります。
- サブドメインを判定するための基本的な考え方
HttpServletRequest#getServerName()を使った振り分け処理の実装方法- web.xmlやアノテーションを使ったルーティングの設定例
- 1つのWARファイル内で複数のServletを使い分けるアーキテクチャの実践例
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Javaの基本的な文法とServletのライフサイクル
web.xmlまたは@WebServletアノテーションを使ったServletのマッピング方法- ローカル環境でTomcatを起動できること
サブドメインでServletを切り替えたい背景
近年、マイクロサービス化が進む中で「1つのドメインで複数のサービスを提供したい」という要件が増えています。
たとえば、以下のような構成を考えたことがあるのではないでしょうか。
| サブドメイン | 用途 | 起動するServlet |
|---|---|---|
| api.example.com | REST API | ApiServlet |
| admin.example.com | 管理画面 | AdminServlet |
| www.example.com | フロントエンド | FrontendServlet |
このように、URLパスではなくホスト名(サブドメイン)で振り分けたいケースは珍しくありません。
しかし、Servlet仕様には「サブドメインごとに別のServletを起動する」機能は標準で存在しません。
そこで、リクエストのHostヘッダーを読み取り、自前で振り分ける実装が必要になります。
サブドメイン判定でServletを振り分ける実装方法
ここからは、実際に「サブドメインでServletを切り替える」方法をステップごとに解説します。
なお、今回はTomcat 10.x、Java 17、Servlet 5.0を前提とします。
ステップ1:Hostヘッダーを取得するフィルタを作成する
まず、すべてのリクエストを横取りしてHostヘッダーを判定するフィルタ(Filter)を作成します。
これにより、Servletに処理を渡す前に振り分けが可能になります。
Javapackage com.example.routing; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; @WebFilter("/*") public class SubdomainRoutingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String serverName = req.getServerName(); // 例: api.example.com if (serverName == null) { chain.doFilter(request, response); return; } String target = switch (serverName) { case "api.example.com" -> "/api"; case "admin.example.com"-> "/admin"; default -> "/frontend"; }; request.getRequestDispatcher(target + req.getPathInfo()) .forward(request, response); } }
このフィルタは、/* へマッピングされているため、すべてのリクエストで実行されます。
getServerName() で取得したホスト名に応じて、内部フォワード先を切り替えています。
ステップ2:内部フォワード先のServletを定義する
次に、フィルタからフォワードされる先のServletを定義します。
今回はアノテーションで簡潔にマッピングします。
Javapackage com.example.servlet; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @WebServlet("/api/*") public class ApiServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { resp.setContentType("application/json"); resp.getWriter().println("{\"message\":\"API endpoint\"}"); } } @WebServlet("/admin/*") public class AdminServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { resp.setContentType("text/html"); resp.getWriter().println("<h1>Admin Panel</h1>"); } } @WebServlet("/frontend/*") public class FrontendServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { resp.setContentType("text/html"); resp.getWriter().println("<h1>Welcome to Example.com</h1>"); } }
ステップ3:ローカルでテストする(hostsファイルの活用)
ローカルPCで動作確認するには、DNS設定がないため hosts ファイルを編集します。
Bash# /etc/hosts (macOS/Linux) # または C:\Windows\System32\drivers\etc\hosts (Windows) 127.0.0.1 api.example.com 127.0.0.1 admin.example.com 127.0.0.1 www.example.com
これで、ブラウザから https://api.example.com:8080/hello にアクセスすると、ApiServlet が応答するようになります。
ハマった点:リダイレクトループとコンテキストパス
フィルタで forward ではなく sendRedirect を使うと、ブラウザにURLが返却され、無限リダイレクトが発生することがあります。
また、WARファイルをルートコンテキスト(/)以外にデプロイしている場合、request.getContextPath() を考慮しないと、404が返ることもあります。
解決策:コンテキストパスを考慮したフォワード
JavaString context = req.getContextPath(); // /myapp など String target = switch (serverName) { case "api.example.com" -> context + "/api"; case "admin.example.com"-> context + "/admin"; default -> context + "/frontend"; };
これで、コンテキストパスが変わっても正しく振り分けられます。
まとめ
本記事では、サブドメインごとに異なるServletを起動する方法を解説しました。
- Hostヘッダーを使った判定ロジック
- Filterで一括ルーティング
- 内部フォワードでコンテキストパスを考慮する実装
この手法を使えば、1つのTomcatインスタンスで複数のサービスを同居させながら、見た目は完全に別ドメインとして運用できます。
今後は、Servletを跨いで共通の認証フィルタを適用する方法や、Reverse Proxy(nginx)を使った構成についても記事にする予定です。
参考資料
