「十年饮冰」

在SpingMVC的Interceptor中如何得到被调用方法名

背景

为什么要在interceptor层获得方法名称呢?在分布式链路系统中我们需要在MVC框架层埋点,统计方法调用的耗时、trace信息等,目前公司内部没有统一的MVC框架,但是大多数都是使用的SpringMVC.所以我们在Interceptor这一层埋点就ok。在这里可以统计到方法调用完的耗时信息,同时也可以得到用户自定义的埋点信息。在这个过程中踩了一些坑,也尝试了各种方法

Interceptor介绍

1
2
3
4
5
6
7
8
9
10
11
  /*
*主要是这两个方法,我们要拿到此时调用的方法名称,需要从handler中入手
*/

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.得到方法名称。2.得到开始时间。3.得到远端传过来的TraceID ... etc
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//1.得到结束时间 2.回传一些必要信息3.上报信息给agent
}

该handler是什么呢?通过DispatcherServlet类源码我们可以看到该handler是HandlerExecutionChain中的Object对象,顾名思义,该类代表了这次request请求的执行链,里面包括了这次执行中的所有interceptor。那么这个handler对象是Method对象吗?并不完全是这样的…

高版本SpringMVC(3.1+)

那么HandlerExecutionChain是怎么初始化的呢?它是靠HandlerMapping来初始化的,HandlerMapping的实例可以自己配置,或者使用默认配置,SpringMVC会默认的加载DispatcherServlet.properties配置文件中的这几种配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

HandlerMapping的工作就是将request和handler映射起来,但是我们会有多种方式,比如通过controller的名称、或者在xml中配置、又或者使用annotation的方式。所以mapping有很多种,当然也可以配置多个HandlerMapping,SpringMVC通过适配器模式为你找到匹配的HandlerMapping。那么这个Handler究竟是什么呢?

AbstractUrlHandlerMapping抽象类的registerHandler方法可以找到答案,handler默认是Controller实例,通过beanName被抽象类获取到实例(controller应该都会加载到容器这是毋庸置疑的)。那么结局就有点尴尬了,拿到Controller实例没什么大的作用。根本拿不到对应的方法。

但是SpringMVC3.1以上版本annotation-driven配置把DefaultAnnotationHandlerMappingAnnotationMethodHandlerAdapter默认修改成了RequestMappingHandlerMapping和对应的adapter。后者的一系列类把request和Method对象Mapping在了一起。通过以下方法使用

  • 使用annotation-driven配置xml,可以自动注入RequestMappingHandlerMapping和adapter
  • 手动配置RequestMappingHandlerMapping的bean

使用了RequestMappingHandlerMapping之后,handler的实例就变成了HandlerMethod这个对象,我们可以直接获得方法名称,皆大欢喜!

低版本SpringMVC(3.1以下)

如果是低版本的SpringMVC 那就没办法了,只能拿到Controller实例的对象,这里心生一计,既然能得到Controller对象,是否可以通过request中的url,在通过反射拿到所有方法的注解值然后mapping到方法呢?好想是可以的,但是这里有一个问题,就是url匹配的问题,SpringMVC包含了多种url匹配,比如RESTFUL,还有各种匹配格式,非常繁琐。要么自己重写SpringMVC的匹配,要么就使用内部的匹配方法。这一点也提醒了我,SpringMVC最后肯定会通过一种方式找到对应的方法然后invoke的。这也就是adapter的责任。看看DispatcherServlet(前两个)源码细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1.Determine handler adapter for the current request.
//通过对应的handler得到合适的adatper对象,这里实际上就已经初始化了methodResolver对象,放到了一个map中
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 2.Actually invoke the handler. 执行对应handler中的handler方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//3.annodationMethodHandlerAdapter的handle方法中,发现了得到method对象的足迹
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

//通过request对象得到Method对象,然后invoke得到result,渲染modelAndView
ServletHandlerMethodResolver methodResolver = getMethodResolver(handler);

Method handlerMethod = methodResolver.resolveHandlerMethod(request);
ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ExtendedModelMap implicitModel = new BindingAwareModelMap();

Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
ModelAndView mav =
methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest);
return mav;
}

流程大概是这样的:

  • 通过handler对象找到对应的adapter对象,同时初始化自己的methodResolver,同时放入到adapter的一个map当中初始化过程详细见ServletHandlerMethodResolver和它的父类HandlerMethodResolver
  • adapter调用handle方法的时候,传入request,调用resolveHandlerMethod(request)方法,通过SpringMVC自己的匹配规则,最终得到Method对象。

好了,终于找到了url匹配的方法,这个方法要用两个东西,一个是handler,一个是request。我们要如何使用它呢?由于adapter在拦截器之前执行,所以方法映射都已经初始化完毕了。所以我们只能使用初始化完毕之后的map对象,这里就只有使用反射:大概的代码是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 ApplicationContext context = applicationContext;
AnnotationMethodHandlerAdapter myadatper = (AnnotationMethodHandlerAdapter) context.getBean("myadatper", AnnotationMethodHandlerAdapter.class);
Class<? extends AnnotationMethodHandlerAdapter> clazz = myadatper.getClass();
//得到Map字段,然后得到自己的实例
Field map = clazz.getDeclaredField("methodResolverCache");
map.setAccessible(true);
Map methodResolver = (Map) map.get(myadatper);
//通过handler对象得到map的value,也就是该controller所对应的methodResolver
Object resovler = methodResolver.get(handler.getClass());
Class<?> resovler_clazz = resovler.getClass();
//得到methodResolver中的解析request对象的转换方法,得到method对象
Method resolveHandlerMethod = resovler_clazz.getDeclaredMethod("resolveHandlerMethod", HttpServletRequest.class);
resolveHandlerMethod.setAccessible(true);
//invoke此方法,得到被调用的method对象
Method invoke = (Method) resolveHandlerMethod.invoke(resovler, request);

这样就能完美的得到被调用的方法名称了,回顾一下整个流程,看起来很简单,其实是一个源码探究的过程,SpringMVC整个过程还是非常复杂的,但是扩展性有些地方很好,有些地方却差强人意。这种方式不好的地方就死对Spring使用了反射,这种侵入性还是有一点,不过我验证之后发现,从2.5开始每个版本的AnnotationMethodHandlerAdapter类都有此方法,所以还算合格。还有一个缺点就是目前只正对annotaion方式做了除了,比如基本的SimpleUrlHandlerMapping等暂时还没有做处理。那么在整个途中还延伸了一种AOP的方法

利用AspectJ AOP代理

想到拦截器,自然也想到了代理机制,我们使用AOP环绕或者before、after的方式给方法埋点是否更好呢?其实这种方式对Controller层都会织入我们的ASpectJ代码。使用最简单的方式就行给加上Trace注解的方法都织如aop代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
public class AspectModule {
@Pointcut("@annotation(com.aspectj.demo.aspect.trace) ")
public void zhiru(){

}
@Before("zhiru()")
public void doBeforeTask(JoinPoint point){
//这里可以通过point得到method方法
//同时可以通过ThreadLocal得到request对象,这样也能同时获得远程的信息了
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
}

@after 同理

编译之后,代码大概会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping({"/hello"})
@Trace
JoinPoint var2 = Factory.makeJP(ajc$tjp_0, this, this, name);

ModelAndView var5;
try {
Aspectj.aspectOf().doBeforeTask2(var2);
System.out.println("hell");
var5 = new ModelAndView("hello", "name", name);
} catch (Throwable var6) {
Aspectj.aspectOf().doAfterTask(var2);
throw var6;
}

Aspectj.aspectOf().doAfterTask(var2);
return var5;

总结

  • 文章并没有详细的深入到SpringMVC的源码中去,建议读者自行去调试。只是给了大家一个解决问题的思路
  • 有不妥之处,望斧正!不胜感激
谢照东 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!