Spring RequestMappingHandler对BridgeMethod的处理

148 阅读2分钟

问题

定义了一个BaseController

abstract class BaseController<T> {
  @PostMapping("doSomething")
  public Boolean do(@RequestBody T request){
    // do xxxx
    return true;
  }
  
  @PostMapping("try")
  public T try(@RequestBody Wrapper<T> request){
    // do xxxx
    return request.data(); // T对象
  }
}

以及一个子类

@RestController
@RequestMapping("/admin/rule")
public class RuleController extends BaseController<RuleRequest>
{
 
}

期望Application启动之后,调用/admin/rule/doSomething,可以正常处理完成之后,返回true,但是实际上报错404:

No mapping found for HTTP request with URI [/admin/rule/doSomething] in DispatcherServlet with name 'dispatcherServlet'

与此同时,调用/admin/rule/try却能拿到结果,这一差别表现令人困惑。


debug分析

本地启动,并开启debug,发现核心逻辑代码: org.springframework.util.ReflectionUtils#doWithMethods

/**
 * Perform the given callback operation on all matching methods of the given
 * class and superclasses (or given interface and super-interfaces).
 * <p>The same named method occurring on subclass and superclass will appear
 * twice, unless excluded by the specified {@link MethodFilter}.
 * @param clazz the class to introspect
 * @param mc the callback to invoke for each method
 * @param mf the filter that determines the methods to apply the callback to
 */
1  public static void doWithMethods(Class<?> clazz, MethodCallback mc, MethodFilter mf) {
2  // Keep backing up the inheritance hierarchy.
3    Method[] methods = getDeclaredMethods(clazz);
4    for (Method method : methods) {
5      if (mf != null && !mf.matches(method)) {
6       continue;
7      }
8      try {
9       mc.doWith(method);
10     }
11     catch (IllegalAccessException ex) {
12       throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex);
13     }
14   }
15   if (clazz.getSuperclass() != null) {
16    doWithMethods(clazz.getSuperclass(), mc, mf);
17   }
18   else if (clazz.isInterface()) {
19    for (Class<?> superIfc : clazz.getInterfaces()) {
20      doWithMethods(superIfc, mc, mf);
21    }
22   }
23 }

其中,对于Line3 getDeclaredMethods,只获取到了一个方法RuleController#do,继续向下走,发现do方法的判定条件 !mf.matches(method) = true。这里会检查此方法是否为用户自定义的方法,如果不是,则跳过处理,逻辑为:不为bridge方法,并且不是Object对象。

public static final ReflectionUtils.MethodFilter USER_DECLARED_METHODS = new ReflectionUtils.MethodFilter() {
  public boolean matches(Method method) {
    return !method.isBridge() && method.getDeclaringClass() != Object.class;
  }
};

RuleController自身的方法处理完成之后,Line16 doWithMethods接着处理父类的方法,其实还是这个方法处理,相当于递归。但是对于父类来说,getDeclaredMethods变成了两个,同时由于当前已经是BaseController层级,bridge方法的判定失效了,这两个方法会双双进入Line9的处理逻辑里。

Line9的处理逻辑如下:

@Override
public void doWith(Method method) {
 Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
 T result = metadataLookup.inspect(specificMethod);
 if (result != null) {
  Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
  if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
   methodMap.put(specificMethod, result);
  }
 }
}

do方法和try方法,最终的区别就体现在了这里:

对于do方法来说,specificMethod是RuleController#do,bridgedMethod是BaseController#do,两者不一致,因此不做mapping处理;

对于try方法来说,specificMethod与bridgedMethod是一致的,都是BaseController#try,成功完成map处理。


问题解决

所以debug之后,关键问题就出现了,为什么getDeclaredMethods,只返回一个方法?

借助万能的stackOverflow之后,找到了原因,问题指引

最终,只需要对BaseController加上public修饰,两个方法就都不会被RuleController.getDeclaredMethods获取到了,成功mapping两个uri

其他

其实一直以来,以为getDeclaredMethods不会返回父类方法,这么看来也不是绝对的。 另外,debug过程中学习了一波bridgeMethod的由来,每天一个小知识get!