Webアプリケーション開発において、さまざまなデータ形式のリクエストを処理する必要があり、バックエンドでは対応するパラメータ処理メカニズムが求められます。Spring MVCは、Cookie値、ヘッダー値、JSONデータ、URIパラメータなど、多様なデータソースからの値取得に対応したパラメータリゾルバー群を提供しています。以下で、Spring MVCが提供する主要なパラメータ解析コンポーネントについて詳しく検討します。
Spring MVCでは、パラメータ解析機能の基底インターフェースとしてHandlerMethodArgumentResolverが定義されています。このインターフェースには多数の実装クラスが存在し、それぞれ異なる用途に対応しています。
これらのリゾルバーはHandlerMethodArgumentResolverCompositeというコンテナによって管理され、RequestMappingHandlerAdapter内でパラメータ処理に利用されます。
単純パラメータの解析ケース
以下のリクエストを例に考えてみましょう:
http://localhost:8080/testendpoint?userId=5&displayName=%E3%83%86%E3%82%B9%E3%83%88
対応するコントローラーメソッド:
@GetMapping("testendpoint")
public String processTest(Long userId, String displayName) {
System.out.println(String.format("ユーザーID:%d, 表示名:%s", userId, displayName));
return "/view/main";
}
このパターンで使用されるのはRequestParamMethodArgumentResolverです。supportsParameterメソッドの実装を見てみましょう:
@Override
public boolean supportsParameter(MethodParameter methodParam) {
if (methodParam.hasParameterAnnotation(Param.class)) {
if (Map.class.isAssignableFrom(methodParam.nestedIfOptional().getNestedParameterType())) {
String paramName = methodParam.getParameterAnnotation(Param.class).value();
return StringUtils.hasText(paramName);
} else {
return true;
}
} else {
if (methodParam.hasParameterAnnotation(Part.class)) {
return false;
}
methodParam = methodParam.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(methodParam)) {
return true;
} else if (this.enableDefaultResolution) {
return BeanUtils.isSimpleProperty(methodParam.getNestedParameterType());
} else {
return false;
}
}
}
このロジックでは、@RequestParamアノテーション(ここでは@Paramとして表現)が付与されたパラメータや、Map型以外の単純型パラメータを対象とします。enableDefaultResolutionがtrueの場合、BeanUtils.isSimplePropertyにより単純型かどうかが判定されます。
単純型の判定ロジック
isSimplePropertyメソッドの実装:
public static boolean isSimpleProperty(Class<?> targetClass) {
Assert.notNull(targetClass, "Class must not be null");
return isSimpleValueType(targetClass) ||
(targetClass.isArray() && isSimpleValueType(targetClass.getComponentType()));
}
isSimpleValueTypeメソッドによる詳細な型チェック:
public static boolean isSimpleValueType(Class<?> targetClass) {
return (ClassUtils.isPrimitiveOrWrapper(targetClass) || targetClass.isEnum() ||
CharSequence.class.isAssignableFrom(targetClass) ||
Number.class.isAssignableFrom(targetClass) ||
Date.class.isAssignableFrom(targetClass) ||
URI.class == targetClass || URL.class == targetClass ||
Locale.class == targetClass || Class.class == targetClass);
}
この判定により、列挙型、文字列型、数値型、日付型、URI/URL型、ロケール型、クラス型などの単純型およびそれらの配列型がサポートされます。
パラメータ値の解決処理
resolveArgumentメソッドはパラメータ値の取得と変換を担当します:
@Override
public final Object resolveArgument(MethodParameter methodParam,
ModelAndViewContainer container, NativeWebRequest nativeRequest,
WebDataBinderFactory dataBinderFactory) throws Exception {
NamedValueInfo valueInfo = retrieveNamedValueInfo(methodParam);
MethodParameter nestedParam = methodParam.nestedIfOptional();
Object resolvedKey = resolveStringValue(valueInfo.key);
if (resolvedKey == null) {
throw new IllegalArgumentException(
"Specified key must not resolve to null: [" + valueInfo.key + "]");
}
Object extractedValue = extractValue(resolvedKey.toString(), nestedParam, nativeRequest);
if (extractedValue == null) {
if (valueInfo.defaultValue != null) {
extractedValue = resolveStringValue(valueInfo.defaultValue);
} else if (valueInfo.required && !nestedParam.isOptional()) {
handleMissingParameter(valueInfo.key, nestedParam, nativeRequest);
}
extractedValue = handleNullResult(valueInfo.key, extractedValue,
nestedParam.getNestedParameterType());
} else if ("".equals(extractedValue) && valueInfo.defaultValue != null) {
extractedValue = resolveStringValue(valueInfo.defaultValue);
}
if (dataBinderFactory != null) {
WebDataBinder binder = dataBinderFactory.createBinder(nativeRequest, null, valueInfo.key);
try {
extractedValue = binder.convertIfNecessary(extractedValue,
methodParam.getParameterType(), methodParam);
} catch (ConversionNotSupportedException conversionEx) {
throw new MethodArgumentConversionNotSupportedException(extractedValue,
conversionEx.getRequiredType(), valueInfo.key, methodParam, conversionEx.getCause());
} catch (TypeMismatchException mismatchEx) {
throw new MethodArgumentTypeMismatchException(extractedValue,
mismatchEx.getRequiredType(), valueInfo.key, methodParam, mismatchEx.getCause());
}
}
handleResolvedResult(extractedValue, valueInfo.key, methodParam, container, nativeRequest);
return extractedValue;
}
名前付き値情報の取得
getNamedValueInfoメソッドはキャッシュ機構を使用してパラメータ情報を効率的に取得します:
private NamedValueInfo retrieveNamedValueInfo(MethodParameter methodParam) {
NamedValueInfo cachedInfo = this.namedValueInfoCache.get(methodParam);
if (cachedInfo == null) {
cachedInfo = createNamedValueInfo(methodParam);
cachedInfo = updateNamedValueInfo(methodParam, cachedInfo);
this.namedValueInfoCache.put(methodParam, cachedInfo);
}
return cachedInfo;
}
createNamedValueInfoメソッドではアノテーションの有無に基づいて適切なNamedValueInfoインスタンスを作成します:
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter methodParam) {
Param annotation = methodParam.getParameterAnnotation(Param.class);
return (annotation != null ? new CustomNamedValueInfo(annotation) : new CustomNamedValueInfo());
}
CustomNamedValueInfoクラスの実装:
private static class CustomNamedValueInfo extends NamedValueInfo {
public CustomNamedValueInfo() {
super("", false, ValueConstants.DEFAULT_NONE);
}
public CustomNamedValueInfo(Param annotation) {
super(annotation.value(), annotation.required(), annotation.defaultValue());
}
}
パラメータ名の解決
updateNamedValueInfoメソッドはパラメータ名の確定処理を行います:
private NamedValueInfo updateNamedValueInfo(MethodParameter methodParam, NamedValueInfo baseInfo) {
String paramName = baseInfo.key;
if (baseInfo.key.isEmpty()) {
paramName = methodParam.getParameterName();
if (paramName == null) {
throw new IllegalArgumentException(
"Name for argument type [" + methodParam.getNestedParameterType().getName() +
"] not available, and parameter name information not found in class file either.");
}
}
String actualDefaultValue = (ValueConstants.DEFAULT_NONE.equals(baseInfo.defaultValue) ?
null : baseInfo.defaultValue);
return new NamedValueInfo(paramName, baseInfo.required, actualDefaultValue);
}
パラメータ名の取得にはASMベースのメタデータ解析が使用されるため、コンパイル時のデバッグ情報保持が重要になります。
実際の値抽出処理
resolveNameメソッドはマルチパートリクエストと通常リクエストの両方に対応します:
@Override
protected Object extractValue(String paramName, MethodParameter methodParam,
NativeWebRequest nativeRequest) throws Exception {
HttpServletRequest httpRequest = nativeRequest.getNativeRequest(HttpServletRequest.class);
MultipartHttpServletRequest multiRequest =
WebUtils.getNativeRequest(httpRequest, MultipartHttpServletRequest.class);
Object multipartValue = MultipartResolutionDelegate.resolveMultipartArgument(
paramName, methodParam, httpRequest);
if (multipartValue != MultipartResolutionDelegate.UNRESOLVABLE) {
return multipartValue;
}
Object result = null;
if (multiRequest != null) {
List<MultipartFile> fileList = multiRequest.getFiles(paramName);
if (!fileList.isEmpty()) {
result = (fileList.size() == 1 ? fileList.get(0) : fileList);
}
}
if (result == null) {
String[] values = nativeRequest.getParameterValues(paramName);
if (values != null) {
result = (values.length == 1 ? values[0] : values);
}
}
return result;
}
必須パラメータの欠損処理
handleMissingValueメソッドは必須パラメータが見つからない場合の例外処理を行います:
@Override
protected void handleMissingParameter(String paramName, MethodParameter methodParam,
NativeWebRequest nativeRequest) throws Exception {
HttpServletRequest httpRequest = nativeRequest.getNativeRequest(HttpServletRequest.class);
if (MultipartResolutionDelegate.isMultipartArgument(methodParam)) {
if (!MultipartResolutionDelegate.isMultipartRequest(httpRequest)) {
throw new MultipartException("Current request is not a multipart request");
} else {
throw new MissingServletRequestPartException(paramName);
}
} else {
throw new MissingServletRequestParameterException(paramName,
methodParam.getNestedParameterType().getSimpleName());
}
}
null値の処理
handleNullValueメソッドはnull値に対する特別な処理を実施します:
private Object handleNullResult(String paramName, Object currentValue, Class<?> paramClass) {
if (currentValue == null) {
if (Boolean.TYPE.equals(paramClass)) {
return Boolean.FALSE;
} else if (paramClass.isPrimitive()) {
throw new IllegalStateException("Optional " + paramClass.getSimpleName() +
" parameter '" + paramName + "' is present but cannot be translated into a null value " +
"due to being declared as a primitive type. Consider declaring it as object wrapper " +
"for the corresponding primitive type.");
}
}
return currentValue;
}
基本データ型(int、longなど)に対してnull値が渡された場合、例外が発生します。
アノテーションを使用した明示的なパラメータ名指定:
@GetMapping("annotatedRequest")
public String processAnnotatedRequest(@Param("userId") Long userId) {
System.out.println("ユーザ識別子:" + userId);
return "アノテーション付きリクエストの処理結果";
}
RequestParamアノテーションを使用することで、リクエストパラメータ名の指定、デフォルト値の設定、必須/任意の指定などが可能になります。