spring实现定制化网关,动态修改url


前言

在项目中,我们常常使用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时,具体会经历什么流程呢:

  1. 根据当前请求url找到能处理这个请求的目标请求处理器HandlerExecutionChain,
  2. 根据请求处理器,获取对应的适配器类HandlerAdapter,
  3. 调用HandlerAdapter对象的的handle()方法,执行目标方法,并返回一个ModelAndView对象,
  4. 根据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()这两个个方法里:

  1. String lookupPath =
    getUrlPathHelper().getLookupPathForRequest(request);
    通过request获取到url路径
  2. 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);
}

如图所示,方法总共有三个参数:

  1. handler: 毫无疑问,就是目标请求处理器的实例
  2. method: 处理器中对应的处理方法
  3. 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信息,可以结合实际需要来使用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值