前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >springboot之mvc原理(三)-自定义注册

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

作者头像
叔牙
修改2021-04-27 20:36:41
1.5K0
修改2021-04-27 20:36:41
举报

一、背景

目前spring系列在java编程领域,是除了jdk之外最流行的基础框架依赖,基本上所有的应用都使用spring作为基本框架进行架构。

大到一线大厂(阿里、腾讯),小到个人应用,项目上线后都会留有一些后门操作进行线上运行状态探查以及数据订正等等,并且这些后门不面向用户直接开放或者对用户透明,那么我们就需要使用一些方法将这些后门接口进行保护或者分环境屏蔽。常用的方式一般有两种:

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

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

二、原理分析

从前一篇文章《springboot之mvc原理(二)-能力支持》我们了解了springmvc对于web能力支持的原理,那么简单回顾一下springboot在启动时候对mvc的支持:

RequestMappingHandlerMapping创建:

RequestMappingHandlerMapping初始化:

本质上就是把Controller中对应中接口层url与method封装并注入到HandlerMapping中供DispatcherServlet处理请求时使用。

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

代码语言:javascript
复制
@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层配置上有一个注解:

代码语言:javascript
复制
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

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

WebMvcAutoConfiguration类中定义了一个配置类EnableWebMvcConfiguration,从类注释可以看出该配置类等价于@EnableWebMvc,也是springboot新版本默认开启web能力的一种实现。那么EnableWebMvcConfiguration就是mvc能力支持的核心实现,看一下其继承关系:

可以看到,EnableWebMvcConfiguration继承于WebMvcConfigurationSupport,所以前边提到@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)目的就是如果用户自定义了WebMvcConfigurationSupport或者其子类实现,那么就放弃springboot默认实现这一套。

从前边第一张时序图可以看出来,对于HandlerMapping的定义最终会使用EnableWebMvcConfiguration#createRequestMappingHandlerMapping

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

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

代码语言:javascript
复制
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注入到实现类中:

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

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

1:自定义HandlerMapping

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

代码语言:javascript
复制
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;
}
代码语言:javascript
复制
    getMappingForMethod方法又调到了createRequestMappingInfo方法:
代码语言:javascript
复制
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:

代码语言:javascript
复制
@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;
    }
}

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

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

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

代码语言:javascript
复制
@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:

代码语言:javascript
复制
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类

代码语言:javascript
复制
@Configuration
public class CustomWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
    @Override
    protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
        return new FilterRequestMappingHandlerMapping();
    }
}

我们把isLimit调为永为true并启动应用:

从打印的日志中发现我们已经实现了分环境注册接口能力。

  • 自定义WebMvcRegistrations进行扩展

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

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

重新启动应用:

也实现了接口分环境注册的能力。

四、总结

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

注意:

  1. 在代码中实现时,要根据具体场景来界定要注册接口的环境,一般情况下我们会屏蔽线上
  2. 在实现自定义HandlerMapping时要注意观察有没有和框架层实现发生冲突,比如有些网关框架会给予@RequestMapping改造注册服务到网关
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、原理分析
  • 三、接口分环境注册
    • 1:自定义HandlerMapping
      • 2:应用自定义HandlerMapping
      • 四、总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档