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

498 阅读6分钟
原文链接: zhuanlan.zhihu.com

背景

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

Interceptor介绍

/*
  *主要是这两个方法,我们要拿到此时调用的方法名称,需要从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配置文件中的这几种配置

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配置把DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter默认修改成了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.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对象,这里就只有使用反射:大概的代码是这样的。

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代理:

@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 同理

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

@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的源码中去,建议读者自行去调试。只是给了大家一个解决问题的思路
  • 有不妥之处,望斧正!不胜感激
原文:在SpingMVC的Interceptor中如何得到被调用方法名