1. パラメータパーサーとは
これらの注釈 @RequstBody、@RequstParam はおなじみですか?
Controller インターフェースを開発する際に、このようなパラメーター アノテーションをよく使用しますが、これらのアノテーションの機能は何ですか? 私たちは本当に理解していますか?
簡単に言うと、これらのアノテーションは、フロント エンドから渡されたパラメーターをコード ロジックで直接使用できる javaBean に直接解析するのに役立ちます。
フォアグラウンド パスのパラメーター | パラメータ形式 |
---|---|
{ "userId": 1, "userName": "アレックス"} | アプリケーション/json |
通常のコードは次のように記述します。
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
//***
return userInfo.getName();
}
しかし、サービスがパラメーターを受け取る方法を変更すると、次のコードでパラメーターを正常に受け取ることができません。
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
//***
return userName;
}
上記のコードは、アノテーションの使い方を少し変更し、フォアグラウンドでのパラメーターの受け渡し形式を変更すると、正常に解析できます。
フォアグラウンド パスのパラメーター | パラメータ形式 |
---|---|
http://***?userName=Alex&userId=1 | なし |
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
//***
return userName;
}
ここで、これらのアノテーションの背後にある対応するコンテンツ (Spring が提供するパラメーター パーサー) を紹介する必要があります. これらのパラメーター パーサーは、フロント デスクから渡されたパラメーターを解析し、定義したコントローラー入力パラメーターにそれらをバインドし、さまざまな形式でパラメーターを渡すのに役立ちます.さまざまなパラメーター パーサー、場合によってはいくつかの特別なパラメーター フォーマットが必要であり、パラメーター パーサーをカスタマイズする必要さえあります。
SpringBoot でも Spring MVC でも、HTTP リクエストは DispatcherServlet クラス (基本的には HttpServlet から継承されたサーブレット) によって受信されます。Spring は、HttpServlet からのリクエストを取得して解析し、リクエスト uri を Controller クラス メソッドに一致させ、パラメーターを解析してメソッドを実行し、最後に戻り値を処理してビューをレンダリングします。
パラメーター パーサーの機能は、http 要求によって送信されたパラメーターをコントローラー処理ユニットの入力パラメーターに変換することです。元の Servlet がパラメーターを取得する方法は次のとおりであり、必要な情報を HttpServletRequest から手動で取得する必要があります。
@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
/**获取参数开始*/
String resourceId = req.getParameter("resourceId");
String resourceType = req.getHeader("resourceType");
/**获取参数结束*/
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("resourceId " + resourceId + " resourceType " + resourceType);
}
}
開発者が生産性を解放するのを助けるために、Spring は特定の形式 (ヘッダーの content-type に対応する型) を持つパラメーター パーサーを提供します. インターフェイス パラメーターに特定の注釈を追加するだけで済みます (もちろん、ないデフォルトのパーサーがあります)。注釈) )、以下に示すように、HttpServletRequest から元の入力パラメーターを手動で取得することなく、必要なパラメーターを直接取得できます。
@RestController
public class resourceController {
@RequestMapping("/resource")
public String getResource(@RequestParam("resourceId") String resourceId,
@RequestParam("resourceType") String resourceType,
@RequestHeader("token") String token) {
return "resourceId" + resourceId + " token " + token;
}
}
アノテーション クラス パラメータ パーサーの一般的な使用方法と、アノテーションとの対応関係は次のとおりです。
注釈の命名 | 配置 | 使用 |
---|---|---|
@パス変数 | パラメータの前に配置 | URL パスでリクエスト パラメータを許可する |
@RequestParam | パラメータの前に配置 | リクエストのパラメーターを URL アドレスの後に直接接続できるようにします。これは、Spring のデフォルトのパラメーター パーサーでもあります。 |
@RequestHeader | パラメータの前に配置 | リクエスト ヘッダーからパラメータを取得する |
@RequestBody | パラメータの前に配置 | アドレスの直後ではなく、リクエストのパラメータをパラメータ本体に入れることを許可する |
注釈の命名 | 対応パーサー | コンテンツタイプ |
---|---|---|
@パス変数 | PathVariableMethodArgumentResolver | なし |
@RequestParam | RequestParamMethodArgumentResolver | なし (get リクエスト) および multipart/form-data |
@RequestBody | RequestResponseBodyMethodProcessor | アプリケーション/json |
@RequestPart | RequestPartMethodArgumentResolver | マルチパート/フォームデータ |
2. パラメータパーサーの原理
パラメーター パーサーを理解するには、まず最も原始的な Spring MVC の実行プロセスを理解する必要があります。クライアント ユーザーが Http 要求を開始した後、要求はフロント コントローラー (ディスパッチャー サーブレット) に送信され、フロント コントローラーはプロセッサ マッパーを要求し (ステップ 1)、プロセッサ マッパーは実行チェーンを返します (ハンドラー実行ステップ2 )、通常定義するインターセプターはこの段階で実行され、フロントコントローラーはマッパーによって返された実行チェーン内のハンドラー情報をアダプターに送信し (ハンドラーアダプターのステップ 3)、アダプターはそれを見つけて実行します。対応するハンドラー ロジック、つまり、定義したコントローラー コントロール ユニット (手順 4) ハンドラーが実行されると、ビュー パーサーによる解析とビューのレンダリング後にクライアント要求応答情報に返される ModelAndView オブジェクトが返されます。 .
コンテナーが初期化されると、RequestMappingHandlerMapping マッパーは @RequestMapping アノテーションが付けられたメソッドをキャッシュに格納します。キーは RequestMappingInfo で、値は HandlerMethod です。HandlerMethod がどのようにメソッド パラメータの解析とバインドを行うかを理解するためには、リクエスト パラメータ アダプタ **RequestMappingHandlerAdapter、** このアダプタが次のパラメータの解析とバインドのプロセスに対応します。ソースパスは次のとおりです。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
RequestMappingHandlerAdapter の 一般的な解析およびバインディング プロセスを次の図に示します。
RequestMappingHandlerAdapter はインターフェース InitializingBean を実装します。Spring コンテナーが Bean を初期化した後、メソッド afterPropertiesSet ( ) を呼び出して、デフォルトのパラメーター リゾルバーを HandlerMethodArgumentResolverComposite アダプターのパラメーター argumentResolvers にバインドします。ここで、HandlerMethodArgumentResolverComposite は、インターフェース HandlerMethodArgumentResolver の実装クラスです。ソースパスは次のとおりです。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
/** */
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
getDefaultArgumentResolvers ( ) メソッドを通じて、Spring が提供するデフォルトのパラメーター リゾルバーを確認できます。これらのリゾルバーはすべて、 HandlerMethodArgumentResolverインターフェース の実装クラスです。
さまざまなパラメーター タイプに対して、Spring はいくつかの基本的なパラメーター パーサーを提供します, 注釈ベースのパーサー、特定の型ベースのパーサー、そしてもちろんデフォルトのパーサーを含む. 既存のパーサーが解析要件を満たせない場合、Spring はユーザーをサポートする拡張ポイントも提供します.ソースコードは次のとおりです。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
// Annotation-based argument resolution 基于注解
/** @RequestPart 文件注入 */
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
/** @RequestParam 名称解析参数 */
resolvers.add(new RequestParamMapMethodArgumentResolver());
/** @PathVariable url路径参数 */
resolvers.add(new PathVariableMethodArgumentResolver());
/** @PathVariable url路径参数,返回一个map */
resolvers.add(new PathVariableMapMethodArgumentResolver());
/** @MatrixVariable url矩阵变量参数 */
resolvers.add(new MatrixVariableMethodArgumentResolver());
/** @MatrixVariable url矩阵变量参数 返回一个map*/
resolvers.add(new Matrix VariableMapMethodArgumentResolver());
/** 兜底处理@ModelAttribute注解和无注解 */
resolvers.add(new ServletModelAttributeMethodProcessor(false));
/** @RequestBody body体解析参数 */
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
/** @RequestPart 使用类似RequestParam */
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
/** @RequestHeader 解析请求header */
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
/** @RequestHeader 解析请求header,返回map */
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
/** Cookie中取值注入 */
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
/** @Value */
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
/** @SessionAttribute */
resolvers.add(new SessionAttributeMethodArgumentResolver());
/** @RequestAttribute */
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution 基于类型
/** Servlet api 对象 HttpServletRequest 对象绑定值 */
resolvers.add(new ServletRequestMethodArgumentResolver());
/** Servlet api 对象 HttpServletResponse 对象绑定值 */
resolvers.add(new ServletResponseMethodArgumentResolver());
/** http请求中 HttpEntity RequestEntity数据绑定 */
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
/** 请求重定向 */
resolvers.add(new RedirectAttributesMethodArgumentResolver());
/** 返回Model对象 */
resolvers.add(new ModelMethodProcessor());
/** 处理入参,返回一个map */
resolvers.add(new MapMethodProcessor());
/** 处理错误方法参数,返回最后一个对象 */
resolvers.add(new ErrorsMethodArgumentResolver());
/** SessionStatus */
resolvers.add(new SessionStatusMethodArgumentResolver());
/** */
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
// Custom arguments 用户自定义
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all 兜底默认
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
HandlerMethodArgumentResolver インターフェースで定義されているメソッドは、リゾルバ適用可否判定メソッドの supportsParameter() とパラメータ解決メソッドの resolveArgument() の 2 つだけです。異なる目的でのパラメータ リゾルバの使用の違いは、これら 2 つのメソッドに反映されています。具体的には、パラメーターの解析とバインドのプロセスを拡張します。
3. カスタム パラメータ パーサーの設計
Spring の設計は、開始と終了の原則を非常にうまく実装しています. パッケージに多くの非常に強力な機能を統合するだけでなく、ユーザーがカスタマイズおよび拡張する機能も確保しています. パラメータパーサーについても同様です. パラメータパーサーSpringが提供するSpringの提供するパラメータは、一般的に使用されるパラメータ解析機能を基本的に満たすことができますが、多くのシステムのパラメータ送信は標準化されていません.たとえば、JDカラーゲートウェイが送信するビジネスパラメータは、ボディにカプセル化されています.この時点で、Spring が提供するパーサーは役に立たないため、カスタム適応パラメーター パーサーを拡張する必要があります。
Spring は、パラメーター リゾルバーをカスタマイズする 2 つの方法を提供します。1 つは、アダプター インターフェース HandlerMethodArgumentResolverを実装する方法で、もう 1 つは、強化された最適化のためにAbstractNamedValueMethodArgumentResolverなどの既存のパラメーター リゾルバー ( HandlerMethodArgumentResolver インターフェースの既存の実装クラス ) を継承する方法です 。カスタムパラメータパーサのカスタマイズが深い場合は、開発用に独自のインタフェースを実装することをお勧めします.インタフェースアダプタインタフェースのカスタム開発パーサの実装を例に、パラメータパーサのカスタマイズ方法を紹介します.
ソースコードを見てみると、パラメータ解析アダプタインタフェースを拡張する方法として、supportsParameter()とresolveArgument()の2つがあることがわかりました.1つ目の方法は、カスタムパラメータパーサーが適用されるシーン、つまり、パラメーター パーサーをヒットする方法、および 2 つ目は、特定の解析パラメーターの実装です。
public interface HandlerMethodArgumentResolver {
/**
* 识别到哪些参数特征,才使用当前自定义解析器
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 具体参数解析方法
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
ここで、アノテーションベースのカスタム パラメータ パーサーの実装を開始します. これは、コードの実際の使用で使用されるパラメーター パーサーです. カラー ゲートウェイのボディ ビジネス パラメータを取得し、それらを解析して Controller メソッドが直接使用できるようにします.
public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final String DEFAULT_VALUE = "body";
@Override
public boolean supportsParameter(MethodParameter parameter) {
/** 只有指定注解注释的参数才会走当前自定义参数解析器 */
return parameter.hasParameterAnnotation(RequestJsonParam.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
/** 获取参数注解 */
RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
/** 获取参数名 */
String name = attribute.value();
/** 获取指定名字参数的值 */
String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
/** 获取注解设定参数类型 */
Class<?> targetParamType = attribute.recordClass();
/** 获取实际参数类型 */
Class<?> webParamType = parameter.getParameterType()
/** 以自定义参数类型为准 */
Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
if (ObjectUtils.equals(paramType, String.class)
|| ObjectUtils.equals(paramType, Integer.class)
|| ObjectUtils.equals(paramType, Long.class)
|| ObjectUtils.equals(paramType, Boolean.class)) {
JSONObject object = JSON.parseObject(value);
log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
//入参:Integer 目标类型:Long
result = paramType.cast(((Integer) object.get(paramName)).longValue());
}else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
//入参:Integer 目标类型:String
result = String.valueOf(object.get(paramName));
}else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
//入参:Long 目标类型:Integer(精度丢失)
result = paramType.cast(((Long) object.get(paramName)).intValue());
}else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
//入参:Long 目标类型:String
result = String.valueOf(object.get(paramName));
}else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
//入参:String 目标类型:Long
result = Long.valueOf((String) object.get(paramName));
} else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
//入参:String 目标类型:Integer
result = Integer.valueOf((String) object.get(paramName));
} else {
result = paramType.cast(object.get(paramName));
}
}else if (paramType.isArray()) {
/** 入参是数组 */
result = JsonHelper.fromJson(value, paramType);
if (result != null) {
Object[] targets = (Object[]) result;
for (int i = 0; i < targets.length; i++) {
WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
validateIfApplicable(binder, parameter, annotations);
}
}
} else if (Collection.class.isAssignableFrom(paramType)) {
/** 这里要特别注意!!!,集合参数由于范型获取不到集合元素类型,所以指定类型就非常关键了 */
Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
if (result != null) {
Collection<Object> targets = (Collection<Object>) result;
int index = 0;
for (Object targetObj : targets) {
WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
validateIfApplicable(binder, parameter, annotations);
}
}
} else{
result = JSON.parseObject(value, paramType);
}
if (result != null) {
/** 参数绑定 */
WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
result = binder.convertIfNecessary(result, paramType, parameter);
validateIfApplicable(binder, parameter, annotations);
mavContainer.addAttribute(name, result);
}
}
カスタム パラメータ パーサ アノテーションの定義は、次のとおりです. ここでは、特別な属性 recordClass が定義されており、どのような問題が解決されるかは後述します。
/**
* 请求json参数处理注解
* @author wangpengchao01
* @date 2022-11-07 14:18
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
/**
* 绑定的请求参数名
*/
String value() default "body";
/**
* 参数是否必须
*/
boolean required() default false;
/**
* 默认值
*/
String defaultValue() default ValueConstants.DEFAULT_NONE;
/**
* 集合json反序列化后记录的类型
*/
Class recordClass() default null;
}
構成クラスを使用してカスタム パーサーを Spring コンテナーに登録する
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
return new ActMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(actMethodArgumentResolverConfigurer());
}
}
この時点で、完全な注釈ベースのカスタム パラメータ パーサーが完成します。
4. まとめ
Spring のパラメーター パーサーの原理を理解することは、Spring のパラメーター パーサーを正しく使用するのに役立ち、また、独自のシステムに適したパラメーター パーサーを設計することにも役立ちます.いくつかの一般的なパラメーター型の解析では、繰り返しのコードの記述が削減されます、しかし、ここで前提があります私たちのプロジェクトの複合型の入力パラメータは統一されなければならず、フロントエンドによって渡されるパラメータの形式も統一されなければなりません. そうしないと、カスタムパラメータパーサーの設計は災害になります.複雑な互換性作業を行う必要があります。パラメーターパーサーの設計は、できるだけプロジェクト開発の初期段階に置くべきである. 複雑な歴史を持つシステムのインターフェース開発のための統一された仕様がない場合, パラメーターパーサーの設計をカスタマイズすることはお勧めできません. .