#146 URLの取得と構築方法の比較
はじめに
JavaでURLやURLの各コンポーネントを取得する方法はいくつか用意されていますが、どの方法を使うのが最適なのか、それぞれの処理速度について改めて検証をしてみました。
検証環境
計測には以下の環境を使用しています。
- 5.15.167.4-microsoft-standard-WSL2
- 21.0.6+7-Ubuntu-124.04.1
- Apache Tomcat 10.1.16
- OpenJDK 21.0.6
検証方法
今回は、requestからURLを取得し、それをもとにコンテキストパスまでのURLを構築してトップページにリダイレクトさせるような場合を想定してみました。
java.net.URLについてはURLの取得や構築には現在非推奨なようなので今回の計測には含めていません。今回検証した処理方法は以下の通りです。
- URI.create()を使う方法
- java,net.URI.create()でURLの解析、取得をして組み立てる
- split()を使う方法
- URLを"/"で分割して必要な部分まで取得する
- indexOf()を使う方法
- indexOf()でコンテキストパスを文字列検索してsubstring()で取り出す
- HttpServletRequestのメソッドを使う方法
- HttpServletRequestの各メソッドで個別にURLのコンポーネントを取得して組み立てる
処理時間の差をわかりやすくするために各処理を10,000回ずつ計測したかったので、計測をする10,000回のループの処理の中ではリダイレクトの代わりにログに出力することで構築したURLを使うことにしました。
また、HttpServletRequestを使った処理を計測するのにrequestで送られてきたURLが必要だったことと、比較条件を統一したかったことから、すべての方法でフロント画面側のボタンを押すと処理が実行されるように設定しました。
それぞれの処理を10,000回繰り返してその処理時間を測定し、これを10回ずつ実行して平均値を算出してみました。
計測結果
ログに出力された処理時間は以下のようになりました。
(ログには毎回構築したURLも使用させるために出力していますが、ここでは省略して計測時間の出力部分のみ掲載しています)
URI: 118 ms
URI: 119 ms
URI: 124 ms
URI: 103 ms
URI: 106 ms
URI: 103 ms
URI: 106 ms
URI: 106 ms
URI: 96 ms
URI: 97 ms
Split: 128 ms
Split: 112 ms
Split: 121 ms
Split: 109 ms
Split: 139 ms
Split: 127 ms
Split: 119 ms
Split: 108 ms
Split: 109 ms
Split: 125 ms
indexOf: 91 ms
indexOf: 96 ms
indexOf: 92 ms
indexOf: 96 ms
indexOf: 90 ms
indexOf: 88 ms
indexOf: 78 ms
indexOf: 96 ms
indexOf: 93 ms
indexOf: 95 ms
HttpServletRequest: 85 ms
HttpServletRequest: 75 ms
HttpServletRequest: 82 ms
HttpServletRequest: 93 ms
HttpServletRequest: 83 ms
HttpServletRequest: 80 ms
HttpServletRequest: 73 ms
HttpServletRequest: 80 ms
HttpServletRequest: 77 ms
HttpServletRequest: 78 ms
この結果をもとに、各処理方法で10,000回処理した時の平均処理時間をまとめると以下のようになりました。
| 処理方法 | 10,000回処理するのにかかる時間の平均(ms) |
|---|---|
| URI | 107.8 |
| Split() | 119.7 |
| indexOf() | 91.5 |
| HttpServletRequest | 80.6 |
考察
-
HttpServletRequestを使う方法が最も速く約80.6ms、次いでindexOf()が約91.5msの処理時間でした。
- HttpServletRequestのみを使ったものは、プロトコル・ホスト・ポート・コンテキストパスの取得を個別のメソッドで行うため処理が速いと考えられます。
- indexOf()を使用するものは、文字列検索のみの処理なのでオーバーヘッドが少なく早く処理をすることができたようです。
- 今回のようにrequestに含まれるURLをもとにURLの取得や構築を行う場合には、HttpServletRequestを単体で使ったりindexOf()で文字列検索を使うのが処理速度を優先するなら適切だと考えられます。
-
URLやsplit()を使う方法は処理にやや時間がかかる傾向がみられました。
- URI.create()はURL解析のための追加処理が必要だったことから、split()を使用する方法は文字列を配列に分割する処理が必要だったことから、それぞれ負荷が高くなってしまったと考えられます。
使用したコード
今回の計測に使用したコードはこちらです。
ボタンを押すと各処理へ遷移し、それぞれ10,000回のループが実行されるようにしています。遷移先の処理の流れはループ内部以外共通なので、サーブレットの2つ目以降のコードは計測部分(ループ部分)のみ抜粋して掲載しています。
<html>
<head>
<meta charset="UTF-8">
<title>処理時間計測</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header></header>
<main>
<div class="wrapper">
<h1 class="ttl">処理時間計測のためのサンプル</h1>
<button>
<a class="btn__link" href="testUri">
URIを使って計測する
</a>
</button>
<button>
<a class="btn__link" href="testSplit">
split()を使って計測する
</a>
</button>
<button>
<a class="btn__link" href="testIndexOf">
indexOf()を使って計測する
</a>
</button>
<button>
<a class="btn__link" href="testRequest">
HttpServletRequestを使って計測する
</a>
</button>
</div>
</main>
<footer></footer>
</body>
</html>
↓java.net.URIを使って解析をする場合
package com.example
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
@WebServlet("/testUri")
public class TestUri extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ここから計測開始↓↓
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
// URIインスタンスを作成
URI uri = URI.create(request.getRequestURL().toString());
// URLを構築
String url = uri.getScheme() + "://" + uri.getAuthority() + request.getContextPath();
System.out.println(url);
}
long endTime = System.currentTimeMillis();
// ここまで計測↑↑
// 計測結果を計算してログに出力
long processingTime = endTime - startTime;
System.out.println("URI: " + processingTime + " ms");
}
}
↓split()で分割して処理する場合
(@WebServlet("/testSplit")内のループ部分のみ抜粋)
// ここから計測開始↓↓
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
// フルURLを取得して"/"で分割
String[] parts = request.getRequestURL().toString().split("/");
StringBuffer sb = new StringBuilder();
// コンテキストパスまで組み立てたらやめる
for (int j = 0; j < parts.length; j++) {
sb.append(parts[j]).append("/");
if (parts[j].equals(request.getContextPath().replace("/",""))) {
break;
}
}
//URLを構築
String url = sb.toString();
System.out.println(url);
}
long endTime = System.currentTimeMillis();
// ここまで計測↑↑
↓indexOf()で文字列検索する場合
(@WebServlet("/testIndexOf")内のループ部分のみ抜粋)
// ここから計測開始↓↓
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
// フルURLを取得し、コンテキストパスの位置を探す
String fullUrl = request.getRequestURL().toString();
int index = fullUrl.indexOf(request.getContextPath());
// URLを構築
String url = fullUrl.substring(0, index + request.getContextPath().length());
System.out.println(url);
}
long endTime = System.currentTimeMillis();
// ここまで計測↑↑
↓HttpServletRequestで部品を取得する場合
(@WebServlet("/testRequest")内のループ部分のみ抜粋)
// ここから計測開始↓↓
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
// HttpServletRequestから各コンポーネント取得
String scheme = request.getScheme();
String host = request.getServerName();
int port = request.getServerPort();
String contextPath = request.getContextPath();
// URLを構築
String url = scheme + "://" + host + ":" + port + contextPath;
System.out.println(url);
}
long endTime = System.currentTimeMillis();
// ここまで計測↑↑
おわりに
今回の検証では、requestからURLを取得する場合で比較して、HttpSrevletRequestのみやindexOf()を利用するのが処理速度を優先するなら効率が良いことがわかりました。ただし、HttpServletRequestはポートの扱いがやや面倒だったり、メンテナンス性や再利用性についてはjava.net.URIのほうが得意だったりすることもあるようなので、取得したい場面に合わせて使い分けていただければ嬉しいです。
最後までお読みいただきありがとうございました。
参考
Discussion