31

自定义SpringMVC中的RequestMappingHandlerMapping

 4 years ago
source link: https://monkeywie.github.io/2020/06/22/custom-springmvc-requestmappinghandlermapping/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

RequestMappingHandlerMappingSpringMVC 中的一个重要组件,作用是扫描 @Controller@RequestMapping 注解修饰的类,然后生成 请求方法 的对应关系,当有一个 HTTP 请求进入 SpringMVC 时,就会通过请求找到对应的方法进行执行。

可以简单的想象一下,在 RequestMappingHandlerMapping 会维护一个 Map<String,Handle> ,key 存放的是 URI ,value 存放的是对应处理的 handle ,例如:

map.put("GET /user",UserController#get)
map.put("POST /user",UserController#create)

这样通过解析请求就可以很快的找到对应的方法去执行,当然 SpringMVC 的实现肯定不会像上面一样这么简单,不过思路是差不多的。

加载流程

  • 流程图

A7VbQjM.png!web

  1. RequestMappingHandlerMapping 实现了 InitializingBean 接口,在应用启动时会触发 afterPropertiesSet 方法。
  2. initHandlerMethods 方法中,会遍历所有候选的 Bean,并通过 processCandidateBean 方法进行处理。

    • AbstractHandlerMethodMapping.java
    protected void initHandlerMethods() {
        //遍历所有候选的Bean name
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                //处理Bean name
                processCandidateBean(beanName);
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }
    
  3. processCandidateBean 方法中,会通过 isHandler 判断 Bean 是否为 @Controller@RequestMapping 注解修饰的类,是的话调用 detectHandlerMethods 来检查类中的 Handler method

  4. detectHandlerMethods 中会遍历类中所有方法,通过 getMappingForMethod 方法筛选出 @RequestMapping 注解修饰的方法,然后解析成 method -> mapping 的 Map 结构存起来,再遍历使用 registerHandlerMethod 方法注册到 SpringMVC 中

    • AbstractHandlerMethodMapping.java
    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());
    
        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            //查询Class中的方法
            Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<T>) method -> {
                        //通过匿名内部类的方式来进行method的过滤,没有通过@RequestMapping修饰的方法会返回null
                        try {
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            if (logger.isTraceEnabled()) {
                logger.trace(formatMappings(userType, methods));
            }
            //遍历methods进行注册
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
    
    1. 通过 registerHandlerMethod 将对应的关系存放到 mappingRegistry 对象中,里面有很多的 Map 用于存储映射关系
    • AbstractHandlerMethodMapping.java
    //封装HandlerMethod,实际上就是bean name+method,在拦截器中就是暴露的这个对象
    HandlerMethod handlerMethod = createHandlerMethod(handler, method);
    validateMethodMapping(handlerMethod, mapping);
    //将mapping对象和handlerMethod关系存放至mappingLookup
    this.mappingLookup.put(mapping, handlerMethod);
    
    List<String> directUrls = getDirectUrls(mapping);
    for (String url : directUrls) {
        //将非通配符形式的路径与mapping对象关系存放至urlLookup
        this.urlLookup.add(url, mapping);
    }
    
    String name = null;
    if (getNamingStrategy() != null) {
        name = getNamingStrategy().getName(handlerMethod, mapping);
        addMappingName(name, handlerMethod);
    }
    
    CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
    if (corsConfig != null) {
        this.corsLookup.put(handlerMethod, corsConfig);
    }
    
    this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
    

    通过源码可以得知,目前有这两个 mappingLookupurlLookup 对象存放了请求映射关系,在请求到来的时候就会通过这两个 Map 去寻找要执行的方法。

请求流程

先上一张 springMVC 流程图:

Nn6fQrR.png!web

入口由 DispatcherServlet 统一接管,然后通过上一步生成好的 HandlerMapping 映射关系来查找请求对应的处理方法。

  • DispatcherServlet.java
// 寻找当前请求的处理方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}

getHandler 方法中就是对应的逻辑了,代码如下:

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        //遍历handlerMappings,只要能根据请求匹配到一个handler就返回
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

这里值得一提的是 handlerMappings 是一组 HandlerMapping 接口的实现, SpringMVC 默认提供的是 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping ,如果有需要我们也可以自定义一个 HandlerMapping 实现来处理请求。

接着一路跟踪源码,直到 AbstractHandlerMethodMapping#lookupHandlerMethod(String lookupPath, HttpServletRequest request) 方法,就可以看到具体的实现了。

  • AbstractHandlerMethodMapping.java
//先直接使用URI进行匹配,适用于没使用通配符修饰的接口路径,对应urlLookup
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
    //路径匹配到之后,还要根据method、header、consume、produce等等条件继续进行匹配
    addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
    //如果没匹配到,再通过通配符的方式去匹配,对应mappingLookup
    addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}

至此与 RequestMappingHandlerMapping 有关的请求流程就已经介绍完了,最后再附上一张类图:

QR7Jri7.png!web

大部分的实现逻辑都在父类 AbstractHandlerMethodMapping 中。

自定义 RequestMappingHandlerMapping

终于步入主题了,在了解 RequestMappingHandlerMapping 的大概的原理之后,就很清楚的如何来魔改 RequestMappingHandlerMapping

需求

项目中有一个 BaseController 基础类,当有新的需求开发时只需要继承该类就会拥有对应的 CRUD 接口,例如:

  • BaseController.java
public class BaseController<T> {
    @PostMapping
    public Result<T> insert(@Validated @RequestBody T vo) {
        //...
    }

    @PutMapping("{id}")
    public Result<T> update(@PathVariable @NotNull String id, @RequestBody @Validated T vo) {
        //...
    }

    @DeleteMapping("{id}")
    public Result<T> delete(@PathVariable @NotNull String id) {
        //...
    }

    @GetMapping("{id}")
    public Result<T> get(@PathVariable @NotNull String id) {
        //...
    }
}
  • AppController.java
@RestController
@RequestMapping("/app")
public class AppController extends BaseController<App>{

}

这样 AppController 就拥有了基本的 CRUD 接口功能,但是在某些情况的时候我需要屏蔽掉某个接口,可以通过重写方法来实现:

  • AppController.java
@RestController
@RequestMapping("/app")
public class AppController extends BaseController<App>{

    //屏蔽get接口
    @Override
    @GetMapping("{id}")
    public Result<T> get(@PathVariable @NotNull String id) {
        throw new UnsupportedOperationException();
    }
}

这样实现其实也没啥问题,不过会占用一个路由,如果想重写这个接口,并且返回不同的响应体,就实现不了了,例如:

  • 重写父类方法编译不通过,因为泛型不兼容 Result<App>!=Result<AppDetailDTO>
//返回特殊的AppDetailDTO
@Override
@GetMapping("{id}")
public Result<AppDetailDTO> get(@PathVariable @NotNull String id) {
    //...
}
  • 屏蔽父类接口,并声明一个新的方法来实现
//屏蔽get接口
@Override
@GetMapping("{id}")
public Result<T> get(@PathVariable @NotNull String id) {
    throw new UnsupportedOperationException();
}

//声明一个新方法来实现
@GetMapping("/detail/{id}")
public Result<AppDetailDTO> getDetail(@PathVariable @NotNull String id) {
    //...
}

通过重新定义一个新的 路由 来实现,虽然说可以达到目的,但是感觉不够优雅, /{id} 路由白白就浪费了,这个时候就只能通过自定义 RequestMappingHandlerMapping 来实现了。

思路

通过上面的分析可以得知,在应用启动时 RequestMappingHandlerMapping 会去扫描所有的 handle 进行关系映射,可不可以实现一个注解,在扫描某个方法时,如果有该注解修饰的时候就跳过。

根据源码可以得知 getMappingForMethod ,是扫描 method 的处理入口,方法签名如下:

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType)

这个方法可以拿到 Method ,只有重写该方法并且判断 Method 上有自定义的注解修饰直接返回 null 就可以达到取消路由注册的目的了。

实现

定义一个 @Disable 注解,用于标识方法不进行路由注册:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Disable {
}

通过实现 WebMvcRegistrations 接口来自定义 RequestMappingHandlerMapping 类,并重写 getMappingForMethod 方法:

@Configuration
@ConditionalOnWebApplication
public class WebAutoConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new RequestMappingHandlerMapping() {
            @Override
            @Nullable
            protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
                //如果方法上有@Disable注解,直接返回null
                if (AnnotationUtils.findAnnotation(method, Disable.class) != null) {
                    return null;
                }
                //否则还是按照以前的逻辑进行处理
                return super.getMappingForMethod(method, handlerType);
            }
        };
    }

}

这样之前的需求就可以解决了:

//屏蔽get接口
@Disable
@Override
@GetMapping("{id}")
public Result<T> get(@PathVariable @NotNull String id) {
    throw new UnsupportedOperationException();
}

//声明一个新方法来实现,并且路由不变
@GetMapping("{id}")
public Result<AppDetailDTO> getDetail(@PathVariable @NotNull String id) {
    //...
}

父类的方法用 @Disable 注解修饰了,SpringMVC 并不会加载这个路由,在项目重启的时候就不会报错提示有两个相同的路由存在。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK