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

来源:互联网 发布:单片机 多少位 编辑:程序博客网 时间:2024/06/06 09:31

背景

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

Interceptor介绍

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

1234567891011121314151617181920
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolverorg.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolverorg.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMappingorg.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapterorg.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolverorg.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslatororg.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolverorg.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(前两个)源码细节

12345678910111213141516171819202122
// 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,渲染modelAndViewServletHandlerMethodResolver 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对象,这里就只有使用反射:大概的代码是这样的。

123456789101112131415
 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所对应的methodResolverObject 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代理:

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

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

12345678910111213141516
@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的源码中去,建议读者自行去调试。只是给了大家一个解决问题的思路
  • 有不妥之处,望斧正!不胜感激
0 0
原创粉丝点击