1323 字
7 分钟
SpringMVC 实现动态新增路由

前言#

去年面试的时候被问到如何实现动态新增路由,当时觉得挺奇怪,没听说过这种需求,想不出实现的思路,后面又问了其他场景题,好在最后面试顺利通过,在入职后我实现了这个功能,感觉比想象中要麻烦一些

SpringMVC 执行流程#

我们先回顾一下 SpringMVC 的执行流程,可以用一张经典的执行流程图简单概括一下,图是网上随便找的,不需要记忆(这里要批判一下八股文),其实是 DispatcherServlet 的 doDispatch 方法里的执行逻辑,需要用到直接看 doDispatch 方法的源码就好。通过流程图可以发现,路由主要是通过 HandlerMapping 完成的

图1

HandlerMapping#

SpringMVC 会在 Servlet 初始化过程中调用 DispatcherServlet 的 onRefresh() -> initStrategies() -> initHandlerMappings() 方法,从上下文中获取 Spring 扫描的 HandlerMapping 类型的 Bean 放到 DispatcherServlet 的handlerMappings 列表里

图2

HandlerMapping

主要作用是把请求映射到对应的处理器上

  • RequestMappingHandlerMapping 负责解析 @RequestMapping、@GetMapping、@PostMapping 等注解,处理请求路径和方法的映射关系
  • RouterFunctionMapping 是 Spring WebFlux 提供的函数式编程接口,代替传统注解方式的 API
  • BeanNameHandlerMapping 基于 Bean 名称进行请求映射
  • ResourceHandlerMapping 把特定的请求路径映射到文件系统、类路径或者其他资源位置中的静态资源上

这些 HandlerMapping 类型的 Bean 在初始化后会调用 afterPropertiesSet() -> initHandlerMethods() 方法,查找出它们对应负责解析的 Bean 进行解析

图3

这里以最常用的 RequestMappingHandlerMapping 为例,通过调用 detectHandlerMethods 方法进行解析

图4

图5

通过反射遍历 Controller 的所有方法,交给 getMappingForMethod 回调方法判断出带有 @RequestMapping 注解的方法并解析封装成 RequestMappingInfo 对象,然后把 Method 和 RequestMappingInfo 放到 map 中,再遍历 map 注册到 HandlerMapping 里

图6

请求过程#

后续请求过程中,在 doDispatch 方法里通过 DispatcherServlet 的 handlerMappings 列表匹配请求的 url 找到对应的 HandlerMethod 和拦截器,把它们封装到 HandlerExecutionChain 中

图7

因为 HandlerMethod 有注解和继承 Controller 接口等实现方式,所以需要先找到对应的 HandlerAdapter 适配器,然后通过它调用对应的 HandlerMethod 方法把结果封装成 ModelAndView 对象返回,最后交给视图解析器去处理

图8

实现思路#

通过上面的原理分析,我们知道路由是在 HandlerMapping 类型的 Bean 初始化后进行解析的,我们可以模仿 RequestMappingHandlerMapping 初始化后执行 afterPropertiesSet 方法的核心逻辑,调用 detectHandlerMethods 方法解析 Controller 就可以实现动态路由

我们先把 Controller 注册到 IOC 容器中,这里就不做演示了,然后通过反射调用 RequestMappingHandlerMapping 的 detectHandlerMethods 核心方法进行解析处理

/**
* 必须在 Controller 注解下才能注入
*/
private final RequestMappingHandlerMapping handlerMapping;
private void detectHandlerMethods(String beanName) {
Method method;
try {
// 反射调用 RequestMappingHandlerMapping 解析 Controller 的核心方法
method = handlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredMethod("detectHandlerMethods", Object.class);
method.setAccessible(true);
// 参数是 RequestMappingHandlerMapping 和 Controller 的 beanName
method.invoke(handlerMapping, beanName);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.error(e.getMessage(), e);
}
}

每次把 Controller 注册到 IOC 容器,都要手动调用一下这个方法,比较麻烦,我们可以在 Controller 注册到 IOC 容器之前记录它的 beanName,同时也方便后续注销 Controller,通过添加 Bean 的后置处理器,在 Bean 初始化后判断如果是目标的 beanName 就调用这个方法

public class ControllerBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 判断 beanName 是否目标 Controller
if (routeManager.containsBeanNames(beanName)) {
// 初始化后自动调用 detectHandlerMethods 解析 Controller
detectHandlerMethods(beanName);
}
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}

注销 Controller 需要先把 RequestMappingHandlerMapping 里的路由信息注销掉,再注销 Bean

RequestMappingHandlerMapping 中有一个 unregisterMapping 方法,参数是 RequestMappingInfo ,通过调用它可以注销路由信息

我们参考 detectHandlerMethods 方法的处理方式,把注册逻辑改成注销即可,通过反射获取 Controller 所有方法,调用 getMappingForMethod 方法判断并解析成 RequestMappingInfo 然后调用 RequestMappingHandlerMapping 的 unregisterMapping 方法,最后从 IOC 容器中注销 Controller

/**
* 必须在 Controller 注解下才能注入
*/
private final RequestMappingHandlerMapping handlerMapping;
private void unregisterMapping(String beanName) {
Object controller = AppContextUtil.getBean(beanName);
Class<?> targetClass = controller.getClass();
// 反射获取 Controller 所有的方法
ReflectionUtils.doWithMethods(targetClass, method -> {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
try {
// 判断并解析 RequestMappingInfo
Method createMappingMethod = RequestMappingHandlerMapping.class
.getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
createMappingMethod.setAccessible(true);
// 注销 RequestMappingInfo
RequestMappingInfo requestMappingInfo =
(RequestMappingInfo) createMappingMethod.invoke(handlerMapping, specificMethod, targetClass);
if (Objects.nonNull(requestMappingInfo)) {
handlerMapping.unregisterMapping(requestMappingInfo);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
}

理论上只要找到能往 HandlerMapping 类型的 Bean 里新增路由信息的方法,它们都可以实现动态路由

最后#

可能读者会疑惑 Controller 不是在启动过程中就被 Spring 扫描注册了吗?为什么需要手动注册?

确实,在运行过程中动态新增路由是非常少见的需求,一般都是 Controller 在程序运行时从外部引入,启动的时候就不存在,所以不会被扫描到,在某些插件化和低代码平台等复杂架构中可能会有这样的需求

在运行中动态修改容器可能会引入风险,并且 Controller 里面的方法还可能涉及到动态代理,事务等复杂情况,需要深入理解 Spring 的原理

SpringMVC 实现动态新增路由
https://cloop.zone.id/posts/technology/dynamic-route-by-spring-mvc/
作者
Cloop
发布于
2025-04-16
许可协议
CC BY-NC-SA 4.0