深度解析微服务高并发:适配 SpringMVC 框架适配模块及实现原理
适配主流框架
如果不借助 Sentinel 提供的适配主流框架的模块,则在使用 Sentinel 时需要借助 try-catchfinally 将要保护的资源(方法或代码块)包起来,在目标方法或代码块执行之前,调用 ContextUtil#enter 方法及 SphU#entry 方法;在抛出异常时,如果非 BlockException 异常需要调用 Tracer#trace 方法统计异常指标,则在 finally 中需要调用 SphU#entry 方法返回的 Entry 实例的 exit 方法,以及 ContextUtil#exit 方法。
为了节省这些步骤,Sentinel 提供了对主流框架的适配,如适配 Spring MVC、WebFlux、Dubbo、APIGateway 等框架,在 Sentinel 源码之外,Alibaba 的 spring-cloud-starter-alibabasentinel 也为 Sentinel 提供了与 OpenFeign 框架整合的支持。
本篇内容主要包括以下几个方面。
• 适配 Spring MVC 框架。
• 适配 OpenFeign 框架。
• 适配 Dubbo 框架。
• 注解切面。
适配 Spring MVC 框架
Spring MVC 框架是目前使用最多的一个 Web 框架(用 Java 语言编写),而 Sentinel 提供了与 Spring MVC 框架整合使用的适配模块。
本节将介绍如何使用 Spring MVC 适配模块并分析其实现原理。
使用步骤
要使用 Sentinel 提供的 SpringMVC 适配模块,只需要在项目中添加 sentinel-springwebmvc-adapter 模块的依赖即可,借助 WebMvcConfigurer 将 SentinelWebInterceptor 注册到 Spring MVC 框架中,步骤如下。
第一步,在项目中添加 Spring MVC 适配模块的依赖,代码如下。
第二步,编写 WebMvcConfigurer,在 addInterceptors 方法中注入 SentinelWebInterceptor,代码如下。
在创建 SentinelWebInterceptor 时,可以为 SentinelWebInterceptor 添加配置,使用 SentinelWebMvcConfig 封装这些配置。• setBlockExceptionHandler:配置 BlockException 处理器,如果不想配置 BlockException 处理器,则可以在 Spring MVC 的全局异常处理器中处理 BlockException。
• setOriginParser:注册调用来源(origin)解析器,例如,从请求头中获取 S-user 参数的值作为调用来源名称,在向下游服务发起请求时在请求头写入 S-user 参数。
• setHttpMethodSpecify:是否需要给资源名称加上 HttpMethod 前缀,例如,对于 GET 接口/hello,如果将 httpMethodSpecify 配置为 false,则资源名称为/hello,否则资源名称为 GET:/hello。
适配原理
Sentinel 适配 Spring MVC 框架是借助 Spring MVC 框架提供的方法拦截器实现的,通过方法拦截器在目标方法被调用之前、调用之后及发生异常时,分别调用 ContextUtil#enter 方法、SphU#entry 方法、Tracer#trace 方法、Entry#exit 方法和 ContextUtil#exit 方法。
Spring MVC 框架的方法拦截器(HandlerInterceptor)的定义如下。
HandlerInterceptor 在 DispatcherServlet#doDispatch 方法中被调用,每个方法的调用时机如
下。
• preHandle:在调用接口方法之前被调用。
• postHandle:在接口方法执行完成并返回 ModelAndView 时被调用。
•afterCompletion:在接口方法执行完成时被调用,无论执行成功或发生异常都会被调用。
因此,Sentinel 可以借助 HandlerInterceptor 与 SpringMVC 框架整合,在 HandlerInterceptor#preHandle 方法中调用 ContextUtil#enter 方法及 SphU#entry 方法,在 afterCompletion 方法中根据方法参数 ex 是否为空来处理异常情况,并且完成 Entry#exit 方法及 ContextUtil#exit 方法的调用。
SentinelWebInterceptor 是 AbstractSentinelInterceptor 的子类,而 preHandle 方法与 afterCompletion 方法在父类中实现,其自身只实现父类定义的一个获取资源名称的抽象方法,即 getResourceName 方法。getResourceName 方法的源码如下。
资源名称的生成过程如下。
① 从 HttpServletRequest 参数的属性中获取 HandlerMapping 匹配的 URL。
② 如果 UrlCleaner 不为空,则调用 UrlCleaner 的 clean 方法,该方法可将多个接口合并为一个接口。
③ 根据 SentinelWebMvcConfig 配置对象判断是否需要添加 HttpMethod 前缀,如果需要,则给资源名称拼接前缀。
提示:有些接口的名称形如“/hello/{name}”,如果直接从 HttpServletRequest 参数中获取请求路径,则每个请求获取的 URL 可能会不同,所以需要从 HandlerMapping 属性中获取。
UrlCleaner 的 clean 方法可将多个接口合并为一个接口,例如,若借助 UrlCleaner 将接口/user/create、/user/del 和/user/update 都改为/user/**,则可以实现 3 个接口使用同一个限流规则。一般来说,不建议添加 HttpMethod 前缀,因为如果接口使用 @RequestMapping 注解声明,那么想对该接口限流就需要配置多个限流规则。旧项目大多使用 @RequestMapping 注解声明接口方法。例如,对于接口/user/create,用户可能需要为此接口配置 GET:/user/create、POST:/user/create 等多个资源的限流规则。
由于 AbstractSentinelInterceptor 的源码较多,因此分步骤分析。
AbstractSentinelInterceptor#preHandle 方法的源码如下。
① 获取资源名称。
② 调用 OriginParser#parseOrigin 方法解析调用来源,如从请求头中获取 S-user 参数的值。
③ 调用 ContextUtil#enter 方法,调用链入口名称为 sentinel_spring_web_context。
④ 调用 SphU#entry 方法,资源类型为 COMMON_WEB,流量类型为 IN。
⑤ 将 SphU#entry 方法返回的 Entry 实例放入 HttpServletRequest 参数的属性表中,方便在 AbstractSentinelInterceptor#afterCompletion 方法中取出。
⑥ 如果抛出 BlockException,则说明当前请求被拒绝,需要调用 handleBlockException 方
法处理 BlockException。
AbstractSentinelInterceptor#handleBlockException 方法的源码如下。
AbstractSentinelInterceptor#handleBlockException 方法实现的功能:若 SentinelWebMvcConfig 配置了 BlockExceptionHandler,则调用 BlockExceptionHandler#handle 方法处理 BlockException,否则将抛出 BlockException,并由全局处理器处理。
AbstractSentinelInterceptor#afterCompletion 方法的源码如下。
① 从 HttpServletRequest 参数的属性表中获取 preHandle 方法中的 Entry 实例。
② 调用 AbstractSentinelInterceptor#traceExceptionAndExit 方法。
③ 调用 ContextUtil#exit 方法。
AbstractSentinelInterceptor#traceExceptionAndExit 方法的源码如下。
AbstractSentinelInterceptor#traceExceptionAndExit 方法实现的功能:当方法执行抛出异常时,调用 Tracer#traceEntry 方法统计异常指标数据。
评论