前言
在项目中,我们常常使用api网关来统一对接前端的请求,在api网关中,通过Rpc调用后端服务,理论上每一个rpc服务都要有一个固定url与之匹配,按照正常的开发流程,
@RequestMapping("/test.do")
@ResponseBody
public String test(){
return xxService.xxx();
}
我们会写出如上的代码,每次都是固定的代码格式,url一开始就写死了,有没有一种方式支持可以自定义url映射呢,并且动态删除呢。
源码分析
@RequestMapping 这个注解就是配置url映射的
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
//请求路径
@AliasFor("path")
String[] value() default {};
//请求路径,在spring4.x版本中新增,和value相同
@AliasFor("value")
String[] path() default {};
//对应 http method,如GET,POST,PUT,DELETE等
RequestMethod[] method() default {};
//对应http request parameter
String[] params() default {};
//对应http request 的请求头
String[] headers() default {};
//对应request的提交内容类型content type,如application/json, text/html
String[] consumes() default {};
//指定返回的内容类型的content type,仅当request请求头中的(Accept)类型中包含该指定类型才返回
String[] produces() default {};
}
在mvc启动时会扫描所有的@RequestMapping并封装成对应的RequestMapingInfo,转换过程我们可以在RequestMappingHandlerMapping中找到:
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, RequestCondition<?> customCondition) {
return RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name())
.customCondition(customCondition)
.options(this.config)
.build();
}
在spring mvc 中,所有的请求都是经过DispatcherServlet的,那么当一个请求经过DispatcherServlet时,具体会经历什么流程呢:
- 根据当前请求url找到能处理这个请求的目标请求处理器HandlerExecutionChain,
- 根据请求处理器,获取对应的适配器类HandlerAdapter,
- 调用HandlerAdapter对象的的handle()方法,执行目标方法,并返回一个ModelAndView对象,
- 根据ModelAndView对象的信息转发到具体页面。
通过对整个流程分析,最关键的是第一步,根据url找到目标请求处理器, 我们如何让一个没有注册的url来能找到处理它的请求处理器呢?
在这之前,我们先看看源码中通过url是怎么匹配处理器的
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace(
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
通过遍历注册好的HandlerMapping,看哪个HandlerMapping中request能匹配到HandlerExecutionChain,匹配到就返回。
@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " + lookupPath);
}
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
if (logger.isDebugEnabled()) {
if (handlerMethod != null) {
logger.debug("Returning handler method [" + handlerMethod + "]");
}
else {
logger.debug("Did not find handler method for [" + lookupPath + "]");
}
}
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
}
接上,重点在getHandlerInternal()和lookupHandlerMethod()这两个个方法里:
- String lookupPath =
getUrlPathHelper().getLookupPathForRequest(request);
通过request获取到url路径 - List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
根据url路径在mappingRegistry对象里取出匹配的映射处理器。
结合以上源码分析,我们发现最终获取目标处理器的流程实际上是通过url路径在mappingRegistry里获取匹配的目标处理器,而mappingRegistry里的数据是在mvc容器初始化的时候输入进去的,那么如果要实现我们的目标,是不是构造好对应的映射放入mappingRegistry之后,DisPatchServlet就能正常执行呢?
找到入口
通过翻阅源码,在AbstractHandlerMethodMapping类中,我们找到了registerMapping()方法
public void registerMapping(T mapping, Object handler, Method method) {
this.mappingRegistry.register(mapping, handler, method);
}
如图所示,方法总共有三个参数:
- handler: 毫无疑问,就是目标请求处理器的实例
- method: 处理器中对应的处理方法
- mapping: mapping 是什么呢? 在文章最前面,我们说了标识@RequestMapping注解的类,在被扫描到之后,会根据注解里的参数生成RequestMapingInfo对象,那么这个mapping参数是不是就是指的是这个信息呢?
代码实践
我们来用代码来实践一下
@Controller
@RequestMapping("/api")
public class TestController1 {
@Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
@RequestMapping("/testAdd.action")
@ResponseBody
public String testAdd(String path) throws Exception {
//构建 RequestMappingInfo对象
RequestMappingInfo requestMappingInfo = RequestMappingInfo
.paths(path)
.methods(RequestMethod.GET)
.build();
// 获取目标处理类的
Class<?> myController = TestController1.class.getClassLoader().loadClass("com.controller.BaseTestController");
Object obj = myController.newInstance();
//最关键的一步,注册mapping对象
requestMappingHandlerMapping.registerMapping(requestMappingInfo, obj, myController.getDeclaredMethod("test", String.class));
return "success";
}
}
public class BaseTestController {
@ResponseBody
public String test(String str) {
System.out.println(str);
return str;
}
}
启动项目,在浏览器中执行 http://localhost:8080/api/testAdd.action?path=/test/add.action, 返回成功
最关键的来了,添加的/test/add.action能否成功映射并找到设置的处理器呢
执行 http://localhost:8080/test/add.action?str=123, 并设置BaseTestController 断点,
发现成功进入。
总结
至此,我们已经成功找到动态增加url并设置处理器的方法里,后续如何让这些测试代码变成功能模块是业务代码的事情了,可以结合项目需要自行开发。
在AbstractHandlerMethodMapping中也提供了unregister(T mapping)的方法来卸载RequestMapping信息,可以结合实际需要来使用。