2018-05-27 Spring拦截器和Skywalking冲突
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2018-05-27 Spring拦截器和Skywalking冲突相关的知识,希望对你有一定的参考价值。
参考技术A skywalking-5.0.0-alpha
已经修复的版本
skywalking-5.0.0-beta
在观察项目中的日志时,发现
有一个拦截全部Controller方法的日志切面WebRequestInterceptor类
该方法会拦截skywalking的某个方法。
导致每次Web请求,Controller虽然被执行一次,但是doAround会被执行两次。
如果在doAround中做了某些身份认证,Token校验之类的,则产生了不必要的重复请求。
在doAround中打印:
会发现调用了getSkyWalkingDynamicField。
这个方法由skywalking-agent的InstMethodsInter 生成的:
SkyWalkingAgent通过
找到一个具体实现类
然后调用define
然后生成了InstMethodsInter
这个类intercept的方法中:
interceptor.afterMethod对应
GetBeanInterceptor 的 afterMethod, 这里会调用getSkyWalkingDynamicField ,但是 这里会被WebRequestInterceptor拦截!!! 。
getSkyWalkingDynamicField出自EnhancedInstance
在InstMethodsInter.afterMethod调用getSkyWalkingDynamicField的时候,不小心会被Spring的Aspect拦截。
导致重复调用了一次WebRequestInterceptor的doAround。
改写JoinPoint,并排除getSkyWalkingDynamicField方法 :
果然一个月之前官方已经修复了这个BUG
修复的原理就是把Spring的org.springframework.aop.support.MethodMatchers 的matches 静态方法给增强了,那就是让Spring忽略EnhancedInstance接口的所有方法!
这方法既粗暴,又优雅,非常值得学习。
相关链接
issue #1114
pull #1118
Spring Boot拦截器使用和常用功能统一封装
文章目录
1. 拦截器
1.1 拦截器的使用
Spring 中提供了拦截器 HandlerInteceptor,它的具体使用分为以下两个步骤:
- 创建自定义拦截器,实现 HandlerInteceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
- 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中。
1.2 拦截器的原理
通过使用拦截器,业务的执行流程一般如下:
其中所有 controller 的执行都会通过一个调度器 DispatcherServlet 来实现,而所有的方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 的源码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try
try
ModelAndView mv = null;
Object dispatchException = null;
try
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null)
this.noHandlerFound(processedRequest, response);
return;
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method))
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet)
return;
// 调用预处理
if (!mappedHandler.applyPreHandle(processedRequest, response))
return;
// 执行 controller 中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted())
return;
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
catch (Exception var20)
dispatchException = var20;
catch (Throwable var21)
dispatchException = new NestedServletException("Handler dispatch failed", var21);
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
catch (Exception var22)
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
catch (Throwable var23)
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
finally
if (asyncManager.isConcurrentHandlingStarted())
if (mappedHandler != null)
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
else if (multipartRequestParsed)
this.cleanupMultipart(processedRequest);
在上述源码中标注了预处理和执行 controller 中业务的位置,而预处理即就是我们要探索的拦截器源码的位置,其中使用了 applyPreHandle 方法,applyPreHandle 方法的源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception
HandlerInterceptor[] interceptors = this.getInterceptors();
if (!ObjectUtils.isEmpty(interceptors))
for(int i = 0; i < interceptors.length; this.interceptorIndex = i++)
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler))
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
return true;
我们可以发现预处理中首先获取到了所有的拦截器,并进行遍历,如果当前拦截器的返回值为 true 则不进行拦截,如果返回值为 false 则提前结束,而在预处理中则会直接 return。
以上就是对拦截器源码的解析,在 applyRreHandle 中会获取所有拦截器 HandlerInterceptor,并执行拦截器的 preHandle 方法,这样就会与个人自定义的拦截器对应上。
而本质上 Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的,大体的调用流程如下:
2. 用户登录权限校验
-
定义用户登录权限校验的拦截器。
public class LoginInterceptor implements HandlerInterceptor @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception HttpSession session = request.getSession(false); if (session != null && session.getAttribute("user") != null) return true; response.setStatus(401); return false;
-
将自定义拦截器加入到系统配置。
@Configuration public class InterceptorConfig implements WebMvcConfigurer /** * 添加拦截器和指定拦截规则 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/user/**"); // 放行 user 的请求
3. 统一异常处理
统一异常处理使用的是 @ControllerAdvice 和 @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法,具体实现步骤如下:
-
创建一个类,并在此类上添加
@ControllerAdvice
和@ResponseBody
注解。@ControllerAdvice // 表示控制器通知类 @ResponseBody public class ExceptionAdvice
-
编写异常返回数据的方法,在该方法上加上
@ExceptionHandler
注解。@ControllerAdvice // 表示控制器通知类 @ResponseBody public class ExceptionAdvice @ExceptionHandler(Exception.class) public Object Exeception(Exception e) HashMap<String, Object> map = new HashMap<>(); map.put("code", 0); // 状态码(设置0为异常,1为正常) map.put("data", null); // 返回数据 map.put("msg", e.getMessage()); // 异常信息 return map;
-
当有多个异常通知时,匹配顺序为存在的当前类及其子类向上依次匹配。如统一异常代码如下:
@ControllerAdvice // 表示控制器通知类 @ResponseBody public class ExceptionAdvice @ExceptionHandler(Exception.class) public Object Exeception(Exception e) HashMap<String, Object> map = new HashMap<>(); map.put("code", 0); // 状态码(设置0为异常,1为正常) map.put("data", null); // 返回数据 map.put("msg", "总的异常信息:" + e.getMessage()); // 异常信息 return map; @ExceptionHandler(ArithmeticException.class) public Object ArithmeticException(ArithmeticException e) HashMap<String, Object> map = new HashMap<>(); map.put("code", 0); map.put("data", null); map.put("msg", "算数异常信息:" + e.getMessage()); return map;
当出现算数异常时,返回的结果应该是 ArithmeticException 方法通知的异常。
4. 统一数据返回格式
对于统一数据的格式可以手动进行封装,代码如下:
@Data
public class Response implements Serializable
private int code; // 状态码(-1-失败 0-成功 1-空)
private Object data; // 数据
private String msg; // 异常信息
public static String success(Object data)
Response response = new Response();
response.code = 0;
response.data = data;
response.msg = null;
return JSON.toJSONString(response);
public static String success()
Response response = new Response();
response.code = 0;
response.data = null;
response.msg = null;
return JSON.toJSONString(response);
public static String fail(String msg)
Response response = new Response();
response.code = -1;
response.data = null;
response.msg = msg;
return JSON.toJSONString(response);
public static String fail()
Response response = new Response();
response.code = -1;
response.data = null;
response.msg = null;
return JSON.toJSONString(response);
public static String Empty()
Response response = new Response();
response.code = 1;
response.data = null;
response.msg = "没有找到数据!";
return JSON.toJSONString(response);
也可以通过 @ControllerAdvice 注解和实现 ResponseBodyAdvice 接口来实现自动的封装。实现方式如下:
-
创建一个类,给类上添加 @ControllerAdvice 注解,并实现 ResponseBodyAdvice 接口。
@ControllerAdvice public class MyResponseBodyAdvice implements ResponseBodyAdvice
-
重写 ResponseBodyAdvice 接口的两个方法。
supports
方法表示是否在返回数据之前重写数据,返回值为 true 表示重写。beforeBodyWrite
方法用来重写方法返回的数据,将数据封装后返回给前端。
@ControllerAdvice public class MyResponseBodyAdvice implements ResponseBodyAdvice /** * 将 supports 的返回值设置为 true 来表示在返回数据前重写数据 */ @Override public boolean supports(MethodParameter methodParameter, Class aClass) return true; /** * 封装要重写的数据 */ @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) HashMap<String, Object> map = new HashMap<>(); map.put("code", 1); map.put("data", o); map.put("msg", null); return map;
以上是关于2018-05-27 Spring拦截器和Skywalking冲突的主要内容,如果未能解决你的问题,请参考以下文章
考研目标(5.27-5.30)(2018-05-27 00:08)