springboot之mvc原理(三)-自定义注册

一、背景

目前spring系列在java编程领域,是除了jdk之外最流行的基础框架依赖,基本上所有的应用都使用spring作为基本框架进行架构。
大到一线大厂(阿里、腾讯),小到个人应用,项目上线后都会留有一些后门操作进行线上运行状态探查以及数据订正等等,并且这些后门不面向用户直接开放或者对用户透明,那么我们就需要使用一些方法将这些后门接口进行保护或者分环境屏蔽。常用的方式一般有两种:

  • 接口注册上线做权限管控:ip维度,访问者维度限制
  • 分环境注册:线上环境不注册,在预发或者灰度环境以下注册

当然基于以往经验以及风控安全维度考虑,基本不会使用第一种,那么我们今天主要聊一聊第二种。

二、原理分析

从前一篇文章《springboot之mvc原理(二)-能力支持》我们了解了springmvc对于web能力支持的原理,那么简单回顾一下springboot在启动时候对mvc的支持:
RequestMappingHandlerMapping创建:
image.png
RequestMappingHandlerMapping初始化:
image.png
本质上就是把Controller中对应中接口层url与method封装并注入到HandlerMapping中供DispatcherServlet处理请求时使用。

回到我们本篇文章的主旨,如果要实现接口分环境注册,就要以RequestMappingHandlerMapping创建以及初始化作为切入点做文章。首先我们看一下springboot对mvc能力支撑的WebMvcAutoConfiguration类:

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
		TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    //...省略
    /**
	 * Configuration equivalent to {@code @EnableWebMvc}.
	 */
	@Configuration
	public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {

		private final WebMvcProperties mvcProperties;

		private final ListableBeanFactory beanFactory;

		private final WebMvcRegistrations mvcRegistrations;

		public EnableWebMvcConfiguration(
				ObjectProvider<WebMvcProperties> mvcPropertiesProvider,
				ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
				ListableBeanFactory beanFactory) {
			this.mvcProperties = mvcPropertiesProvider.getIfAvailable();
			this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
			this.beanFactory = beanFactory;
		}
        @Bean
		@Primary
		@Override
		public RequestMappingHandlerMapping requestMappingHandlerMapping() {
			// Must be @Primary for MvcUriComponentsBuilder to work
			return super.requestMappingHandlerMapping();
		}
        @Override
		protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
			if (this.mvcRegistrations != null
					&& this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
				return this.mvcRegistrations.getRequestMappingHandlerMapping();
			}
			return super.createRequestMappingHandlerMapping();
		}
    }       
    
复制代码

WebMvcAutoConfiguration层配置上有一个注解:

@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
复制代码

表示如果spring容器中没有注入WebMvcConfigurationSupport类或者其子类实现时才注入改配置。而WebMvcConfigurationSupport是mvc能力的主要配置类,一般将@EnableWebMvc加到应用启动类来开启mvc能力,或者使用另外一种高级实现,直接从此类扩展并根据需要重写方法。

WebMvcAutoConfiguration类中定义了一个配置类EnableWebMvcConfiguration,从类注释可以看出该配置类等价于@EnableWebMvc,也是springboot新版本默认开启web能力的一种实现。那么EnableWebMvcConfiguration就是mvc能力支持的核心实现,看一下其继承关系:
image.png
可以看到,EnableWebMvcConfiguration继承于WebMvcConfigurationSupport,所以前边提到@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)目的就是如果用户自定义了WebMvcConfigurationSupport或者其子类实现,那么就放弃springboot默认实现这一套。
从前边第一张时序图可以看出来,对于HandlerMapping的定义最终会使用EnableWebMvcConfiguration#createRequestMappingHandlerMapping

@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
	if (this.mvcRegistrations != null
			&& this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
		return this.mvcRegistrations.getRequestMappingHandlerMapping();
	}
	return super.createRequestMappingHandlerMapping();
}
复制代码

该方法的定义是,如果用户自定义了WebMvcRegistrations并且实现getRequestMappingHandlerMapping方法,那么就使用用户自己定义的HandlerMapping,否则使用默认的RequestMappingHandlerMapping,WebMvcRegistrations:

public interface WebMvcRegistrations {
	/**
	 * Return the custom {@link RequestMappingHandlerMapping} that should be used and
	 * processed by the MVC configuration.
	 * @return the custom {@link RequestMappingHandlerMapping} instance
	 */
	default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
		return null;
	}
	/**
	 * Return the custom {@link RequestMappingHandlerAdapter} that should be used and
	 * processed by the MVC configuration.
	 * @return the custom {@link RequestMappingHandlerAdapter} instance
	 */
	default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
		return null;
	}
	/**
	 * Return the custom {@link ExceptionHandlerExceptionResolver} that should be used and
	 * processed by the MVC configuration.
	 * @return the custom {@link ExceptionHandlerExceptionResolver} instance
	 */
	default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
		return null;
	}
}
复制代码

其提供了RequestMappingHandlerMapping/RequestMappingHandlerAdapter/ExceptionHandlerExceptionResolver三个基础组件的自定义扩展,从这里也能看出spring开闭原则设计的良苦用心。

通过上边的描述,如果我们想自定义HandlerMapping或者其他组件,有两种思路:

  1. 继承WebMvcConfigurationSupport或者其子类实现进行自定义扩展
  2. 自定义WebMvcRegistrations进行扩展

三、接口分环境注册

既然提到分环境注册,那么在实现之前我们简单说一下环境相关的组件EnvironmentAware和Environment,
EnvironmentAware是一个接口,在应用启动后将环境信息封装成Environment注入到实现类中:

public interface EnvironmentAware extends Aware {
	/**
	 * Set the {@code Environment} that this component runs in.
	 */
	void setEnvironment(Environment environment);
}
复制代码

接下来我们就开始从代码层面实现接口分环境注册.

1.自定义HandlerMapping

image.png
从RequestMappingHandlerMapping的继承关系中可以看出其已经对HandlerMapping提供了比较成熟的实现,那么我们就按照借鸡生蛋的思维,在RequestMappingHandlerMapping的基础上进行扩展,从RequestMappingHandlerMapping初始化时序图中我们又发现对于接口的发现最终会调到RequestMappingHandlerMapping的getMappingForMethod方法:

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
	RequestMappingInfo info = createRequestMappingInfo(method);
	if (info != null) {
		RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
		if (typeInfo != null) {
			info = typeInfo.combine(info);
		}
		String prefix = getPathPrefix(handlerType);
		if (prefix != null) {
			info = RequestMappingInfo.paths(prefix).build().combine(info);
		}
	}
	return info;
}
复制代码

getMappingForMethod方法又调到了createRequestMappingInfo方法:

private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
	RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
	RequestCondition<?> condition = (element instanceof Class ?
			getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
	return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
复制代码

理论上我们继承RequestMappingHandlerMapping后重写createRequestMappingInfo就能满足诉求,奈何它是一个私有方法,无法重写,所以想要扩展就必须重写getMappingForMethod和自己实现createRequestMappingInfo。
自定义HandlerMapping实现FilterRequestMappingHandlerMapping:

@Slf4j
public class FilterRequestMappingHandlerMapping extends RequestMappingHandlerMapping implements EnvironmentAware {
    private CurrentEnv currentEnv;
    @Override
    public void afterPropertiesSet() {
        log.info("FilterRequestMappingHandlerMapping start initializing...");
        super.afterPropertiesSet();
    }
    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        //RequestMappingInfo requestMappingInfo = super.getMappingForMethod(method, handlerType);
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }
        }
        return info;
    }
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        if(null == requestMapping) {
            return null;
        }
        EnvLimit envLimit = AnnotatedElementUtils.findMergedAnnotation(element, EnvLimit.class);
        if(isEnvLimited(envLimit)) {
            log.info("FilterRequestMappingHandlerMapping.createRequestMappingInfo current env should not registry mapping;env={},url={}"
                    ,currentEnv,requestMapping.value());
            return null;
        }
        RequestCondition<?> condition = (element instanceof Class ?
                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        return createRequestMappingInfo(requestMapping, condition);
    }
    @Override
    public void setEnvironment(Environment environment) {
        String env =  environment.getActiveProfiles()[0];
        log.info("FilterRequestMappingHandlerMapping.setEnvironment current env is {}",env);
        this.currentEnv = CurrentEnv.of(env.toLowerCase());
    }

    /**
     * 检查当前环境是否需要注册mapping
     *
     * @param envLimit
     * @return
     */
    protected boolean isEnvLimited(EnvLimit envLimit) {
        if(null == envLimit) {
            return false;
        }
        for(CurrentEnv env : envLimit.exclude()) {
            if(env.equals(currentEnv)) {
                return true;
            }
        }
        return false;
    }
}
复制代码

首先,该类初始化时候会把环境信息注入进去:

    /**
     * 开发
     */
    DEV("dev",1,"开发"),
    /**
     * 测试
     */
    TEST("test",2,"测试"),
    /**
     * 灰度
     */
    GRAY("gray",3,"灰度"),
    /**
     * 生产
     */
    PROD("prod",4,"生产")
复制代码

然后应用afterPropertiesSet方法调用链最终调用createRequestMappingInfo创建接口映射信息,此处我们使用了自定义注解EnvLimit来判定当前启动环境是否要注册接口:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnvLimit {
    /**
     * 默认环境限制
     *
     * @return
     */
    CurrentEnv[] value() default {CurrentEnv.DEV,CurrentEnv.TEST,CurrentEnv.GRAY,CurrentEnv.PROD};

    CurrentEnv[] exclude() default {CurrentEnv.PROD};
}
复制代码

核心在于isEnvLimited对EnvLimit限制和当前环境currentEnv的对比检查来决定当前环境是否注册mapping:

protected boolean isEnvLimited(EnvLimit envLimit) {
    if(null == envLimit) {
        return false;
    }
    for(CurrentEnv env : envLimit.exclude()) {
        if(env.equals(currentEnv)) {
            return true;
        }
    }
    return false;
}
复制代码

2.应用自定义HandlerMapping

从第二节中我们知道,使用自定义web组件的方式有两种,这里不啰嗦直接分别实现:

  • 继承WebMvcConfigurationSupport或者其子类

我们直接继承WebMvcConfigurationSupport类

@Configuration
public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {

    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new FilterRequestMappingHandlerMapping();
    }
}
复制代码

我们把isLimit调为永为true并启动应用:
image.png
从打印的日志中发现我们已经实现了分环境注册接口能力。

  • 自定义WebMvcRegistrations进行扩展

自定义WebMvcRegistrations实现类并注入到spring:

@Configuration
public class WebMvcConfig  {
    @Bean
    public WebMvcRegistrations webMvcRegistrationsHandlerMapping() {
        return new WebMvcRegistrations() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new FilterRequestMappingHandlerMapping();
            }
        };
    }
}
复制代码

重新启动应用:
image.png
也实现了接口分环境注册的能力。

四、总结

本篇文章我们通过扩展spring组件实现了接口分环境注册的能力,对于两种实现方式我个人倾向于第二种,自定义WebMvcRegistrations实现,因为spring在整个WebMvcConfigurationSupport继承关系中帮我们加入了很多方便实用的组件,如果我们使用第一种的话,要么自己把这些重新撸一遍,要么大多数情况下直接丢弃了,使用第二种的话,我们再保留默认扩展特性的情况下扩展了自定义能力,优缺点自有分晓。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享