动态生成简约MVC请求接口|抛弃一切注解减少重复劳动吧

背景

目前创建一个后端请求接口给别人提供服务,无论是使用SpringMVC方式注解,还是使用SpringCloud的Feign注解,都是需要填写好@RequestMap、@Controller、@Pathvariable等注解和参数。每个接口都需要重复的劳动,非常繁琐。特别是服务治理框架的接口层不是springmvc,而都是通过TCP连接来做RPC通信的接口,这样的接口调试起来比较麻烦,测试人员也不能感知接口参数,压力测试的时候没得使用JMETER方便。

目的

为了解放双手,让后端服务开发人员提供接口给别人时,只需要更关注逻辑。减少开发人员关注框架内容,减少关注每个@注解上的参数信息,不用再校验path是否已经被使用过。无须再感知SpringMVC或者Feign的存在。

我们统一做处理,把类名和方法名来做为请求接口url,不再显式声明url,默认POST请求、返回为JSON形式,请求参数支持@RequestBody、@RequestParam。

前置了解

Spring的钩子类、钩子方法

使用场景

如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想增添MVC接口。
如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想提供给前端HTML使用。
如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想提供给测试人员方便阅读,也方便用JMETER做压力测试。
如果你的接口是Feign或者已经是springMVC,但是还在填写url、path、请求method、参数解析方式、每次都要核对ur有没有重复使用等繁琐工作,可以放下这些操作了。

先看看简约到什么程度

@Contract
public interface UserContract {
       
    User getUserBody(User user);
}
@Component
public class UserContractImpl implements UserContract {

    @Override
    public User getUserBody(User user11) {
        user11.setAge(123);
        return user11;
    }

}

  1. 只需要使用@Contract注解,我们就会生成好一个类下所有方法的POST请求接口,并映射到对应方法。
  2. 让开发人员只需要关注请求接口内逻辑,不再需要关注Controller如何生成。
    代码一个MVC注解都没有,对mvc接口生成无感知。
  3. 不嵌入实体类构建Bean过程。
  4. 相较正常的@Controller类,少写@RequestMapping 等注解和上面的参数,少写@RequestBody、少写@RequestBody等参数解析方式。这些都不用再显式填写。只需要添加我们自定义注解,并在服务启动时的动态生成简约MVC完成。

上述代码已生成的功能:

  1. url为 /UserContract/getUserBody的uri,
  2. 请求方法为POST
  3. 并且请求方式支持body方式提交user11对象
  4. 如果参数是基本类型的话默认是作为@RequestParam方式请求
  5. 返回方式为JSON

大家看,是不是不用再填写任何的MVC、Feign注解了

需求

  1. 只创建mvc的url与实现类的方法的关联关系,不为实现类创建bean对象入容器,只关注MVC层面,不耦合其他层面的功能。
  2. 支持POST请求
  3. 类名和方法名拼接成为uri
  4. 请求参数支持@RequestParam,@RequestBody
  5. 返回数据为JSON
  6. 基于springboot

Previously

先看看原生MVC如何绑定URL和方法


consice.png

我们自己的实现主要处理第二步,注入我们自己的RequestMappingHandler。然后做第6、7步重写,让找@Controller的方法改为找@Contract,最后重写处理url生成的方法。

实现

1. 启动方式

首先实现启动方式,使用下述注解放在在Springboot服务启动类上,标明请求接口的实现类代码在哪个路径。然后通过@Import(ContractAutoHandlerRegisterConfiguration.class) 在服务启动时,添加url和类的关联关系。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ContractAutoHandlerRegisterConfiguration.class)
public @interface EnableContractConciseMvcRegister {

    /**
     * Contract 注解的请求包扫描路径
     * @return
     */
    String[] basePackages() default {};

}

2. import加载负责url和方法关联关系处理的类

利用ImportBeanDefinitionRegistrar ,就会在@import时触发逻辑,让类BeanDefinition注册到容器中。

public class ContractAutoHandlerRegisterConfiguration implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        log.info("开始注册MVC映射关系");
        Map<String, Object> defaultAttrs = metadata
                .getAnnotationAttributes(EnableContractConciseMvcRegister.class.getName(), true);

        if (defaultAttrs == null ||  !defaultAttrs.containsKey("basePackages"))
            throw new IllegalArgumentException("basePackages not found");

        //获取扫描包路径
        Set<String> basePackages = getBasePackages(metadata);
        //生成BeanDefinition并注册到容器中
        BeanDefinitionBuilder mappingBuilder = BeanDefinitionBuilder
                .genericBeanDefinition(ContractAutoHandlerRegisterHandlerMapping.class);
        mappingBuilder.addConstructorArgValue(basePackages);
        registry.registerBeanDefinition("contractAutoHandlerRegisterHandlerMapping", mappingBuilder.getBeanDefinition());

        BeanDefinitionBuilder processBuilder = BeanDefinitionBuilder.genericBeanDefinition(ContractReturnValueWebMvcConfigurer.class);
        registry.registerBeanDefinition("contractReturnValueWebMvcConfigurer", processBuilder.getBeanDefinition());
        log.info("结束注册MVC映射关系");

    }
}

  1. 利用Import形式registerBeanDefinitions时注入容器。
  2. 其中重要的只有ContractAutoHandlerRegisterHandlerMapping,ContractReturnValueWebMvcConfigurer。
    ContractAutoHandlerRegisterHandlerMapping ,负责url与实现类(如UserContractImpl)方法的关联关系。
    ContractReturnValueWebMvcConfigurer,处理请求参数解析和方法返回数据转换。

这里利用注解和ImportBeanDefinitionRegistrar 实现了需求6 支持springboot容器。

3. 方法与URL映射

创建ContractAutoHandlerRegisterHandlerMapping继承RequestMappingHandlerMapping。
重写几个比较重要的方法,其中一个是isHandler。

/**
     * 判断是否符合触发自定义注解的实现类方法
     */
    @Override
    protected boolean isHandler(Class<?> beanType) {
        // 注解了 @Contract 的接口, 并且是这个接口的实现类

        // 传进来的可能是接口,比如 FactoryBean 的逻辑
        if (beanType.isInterface())
            return false;

        // 是否是Contract的代理类,如果是则不支持
        if (ClassUtil.isContractTargetClass(beanType))
            return false;

        // 是否在包范围内,如果不在则不支持
        if (!isPackageInScope(beanType))
            return false;

        // 是否有标注了 @Contract 的接口
        Class<?> contractMarkClass = ClassUtil.getContractMarkClass(beanType);
        return contractMarkClass != null;
    }

继承这个类重写这个方法的主要原因是

  1. 经过上面第一步已经把这个关联关系放入容器中后,启动SpringMVC注册时,上述RequestMappingHandlerMapping这个类有继承InitializingBean接口,就是通过这个InitializingBean的afterPropertiesSet方法执行后续的逻辑,这个是入口的关键,这个就是告诉等bean都构建完成后初始工作完成后处理的工作方法。(如流程图第5步)
  2. springMVC原生RequestMappingHandlerMapping的afterPropertiesSet 这个时候会扫你工程代码里所有类,并且会触发我们自定义的ContractAutoHandlerRegisterHandlerMapping上述的isHandler方法
  3. 这个isHandler方法就需要我们去判断,扫到的这个类是否符合创建mvc接口的类。
  4. 我们继承了RequestMappingHandlerMapping,就可以自定义判断的逻辑。判断的逻辑就是这个class字节码是个类,不是interface,并且这个类上面必须有implement了一个interface,而且这个interface需要有@Contract注解(这个类没有贴代码,就是自定义普通的注解,写个名字就好了)
  5. 这样就可以标记这是我们需要动态创建简约MVC的类,这个类下的所有方法,都会被创建springMVC请求接口,那些被标记需要创建MVC的类就如前面样例的UserContractImpl

3. 如何动态创建MVC接口(关键点)

在ContractAutoHandlerRegisterHandlerMapping我们这个自定义类下,重写getMappingForMethod这个方法,这个方法就是用来生成接口的URL,我们要有自己的方式所以要重写。

因为当经过上一节,逻辑找到你代码工程下符合创建简约MVC的类后,如找到UserContractImpl后,ContractAutoHandlerRegisterHandlerMapping的父类RequestMappingHandlerMapping逻辑会去找到UserContractImpl所有方法并进行创建url,然后绑定方法和url关系。(如流程图的第7~9步)

@Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        Class<?> contractMarkClass = ClassUtil.getContractMarkClass(handlerType);
        try {
            // 查找到原始接口的方法,获取其注解解析为 requestMappingInfo
            Method originalMethod = contractMarkClass.getMethod(method.getName(), method.getParameterTypes());
            RequestMappingInfo info = buildRequestMappingByMethod(originalMethod);

            if (info != null) {
                RequestMappingInfo typeInfo = buildRequestMappingByClass(contractMarkClass);
                if (typeInfo != null)
                    info = typeInfo.combine(info);
            }
            return info;
        } catch (NoSuchMethodException ex) {
            return null;
        }
    }

    private RequestMappingInfo buildRequestMappingByClass(Class<?> contractMarkClass) {

        String simpleName = contractMarkClass.getSimpleName();
        String[] paths = new String[] { simpleName };
        RequestMappingInfo.Builder builder = RequestMappingInfo.paths(resolveEmbeddedValuesInPatterns(paths));

        // 通过反射获得 config
        if (!isGetSupperClassConfig) {
            BuilderConfiguration config = getConfig();
            this.mappingInfoBuilderConfig = config;
        }

        if (this.mappingInfoBuilderConfig != null)
            return builder.options(this.mappingInfoBuilderConfig).build();
        else
            return builder.build();
    }

    private RequestMappingInfo buildRequestMappingByMethod(Method originalMethod) {
        String name = originalMethod.getName();
        String[] paths = new String[] { name };
        // 用名字作为url
        // post形式
        // json请求
        RequestMappingInfo.Builder builder = RequestMappingInfo.paths(resolveEmbeddedValuesInPatterns(paths))
                .methods(RequestMethod.POST);
//                .params(requestMapping.params())
//                .headers(requestMapping.headers())
//                .consumes(MediaType.APPLICATION_JSON_VALUE)
//                .produces(MediaType.APPLICATION_JSON_VALUE)
//                .mappingName(name);
        return builder.options(this.getConfig()).build();
    }

    RequestMappingInfo.BuilderConfiguration getConfig() {
        Field field = null;
        RequestMappingInfo.BuilderConfiguration configChild = null;
        try {
            field = RequestMappingHandlerMapping.class.getDeclaredField("config");
            field.setAccessible(true);
            configChild = (RequestMappingInfo.BuilderConfiguration) field.get(this);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            log.error(e.getMessage(),e);
        } catch (NoSuchFieldException | SecurityException e) {
            log.error(e.getMessage(),e);
        }
        return configChild;
    }
  1. getMappingForMethod这个方法就是为了,处理实现类UserContractImpl下所有方法的url,得到url后会处理绑定关系到MVC的容器中。后续请求进来了,就会从这个MVC的容器map中根据url为key,找到value,value就是实现类的方法。
  2. getMappingForMethod里的自己定义的buildRequestMappingByClass这个方法就是解析类名,我们的逻辑就是把类名作为接口uri的第一部分。如:/UserContract
  3. 自定义的buildRequestMappingByMethod就是处理方法,把方法名作为uri的第二部分,如/getUser。并且在这里设定了为post作为请求方式.

这里完成了需求3:类名和方法名拼接成为uri、需求2 POST请求方式

  1. 鉴于springmvc请求接口进来时,即使我们接口方法getUser的参数没有注解,都会默认使用@RequestParam通过参数名字来映射,请求接口的参数。
  2. 如果是有成员变量的类对象,springmvc也会默认成@RequestBody来处理

这里完成了需求4 请求参数支持@RequestParam,@RequestBody

4. 处理请求接口返回

之前第一步注册的ContractReturnValueWebMvcConfigurer,就是做参数与返回处理。

public class ContractReturnValueWebMvcConfigurer implements BeanFactoryAware, InitializingBean {

    private WebMvcConfigurationSupport webMvcConfigurationSupport;
    private ConfigurableBeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (beanFactory instanceof ConfigurableBeanFactory) {
            this.beanFactory = (ConfigurableBeanFactory) beanFactory;
            this.webMvcConfigurationSupport = beanFactory.getBean(WebMvcConfigurationSupport.class);
        }
    }

    public void afterPropertiesSet() throws Exception {

        try {
            Class<WebMvcConfigurationSupport> configurationSupportClass = WebMvcConfigurationSupport.class;
            List<HttpMessageConverter<?>> messageConverters = ClassUtil.invokeNoParameterMethod(configurationSupportClass, webMvcConfigurationSupport, "getMessageConverters");
            List<HandlerMethodReturnValueHandler> returnValueHandlers = ClassUtil.invokeNoParameterMethod(configurationSupportClass, webMvcConfigurationSupport, "getReturnValueHandlers");
            List<HandlerMethodArgumentResolver> argumentResolverHandlers = ClassUtil.invokeNoParameterMethod(configurationSupportClass, webMvcConfigurationSupport, "getArgumentResolvers");

            //只要匹配@Contract的方法,并将所有返回值都当作 @ResponseBody 注解进行处理  
            returnValueHandlers.add(new ContractRequestResponseBodyMethodProcessor(messageConverters));
}

利用InitializingBean把WebMvcConfigurationSupport拿出来。对有自定义注解@Contract的interface的方法才会有特殊处理,这些方法都会使用@ResponseBody返回,就不用再在实现类的方法写@ResponseBody了

这里完成需求4 支持@ResponseBody

使用与测试

  1. 前面样例的UserContractImpl已经写了,只需要注意在UserContractImpl的interface(UserContract)上填@Contract。请求接口的代码类就不重复贴了。
  2. 现在编写springboot启动类,注意basePackages 为请求接口的实现类的包路径。
@Configuration
@EnableAutoConfiguration 
@ComponentScan
@SpringBootApplication
@EnableContractConciseMvcRegister(basePackages = "com.dizang.concise.mvc.controller.impl")
public class ConsicesMvcApplication {
    
    public static void main(String[] args) throws Exception {
        SpringApplication.run(ConsicesMvcApplication.class, args);
    }

}
  1. 启动后,打开swagger-ui.html


    image.png

总结

到目前为止,我们没有在工程代码中使用springmvc注解,也能生成接口映射关系了。
这样大家以后就再也不用写SpringMVC的注解也能使用SpringMVC了,如果你公司框架默认是tcp连接的RPC接口,只要使用了这种方式,就可以自己本地调试,不用再编写一个RPC客户端来访问自己的接口。使用Swagger调试又比较方便,而且测试同时也能看到请求参数,也可以对其做JMETER压力测试。
不过代码都有一个问题,就是做法越统一,约束就越多。想自由,就约束少。所以我们这个框架,就只能用POST请求,并且ResponseBody来返回,就不适合要跳转重定向页面的那种,也不支持@PathVariable的参数解析方式,没那么RestFul风格(但可以把GET POST方式更改为用int值放在请求参数里),但是支持@RequestParam和@RequestBody形式,我觉得也是足够了。

代码样例

https://gitee.com/kelvin-cai/concise-mvc-register


欢迎关注公众号,文章更快一步

我的公众号 :地藏思维

掘金:地藏Kelvin

简书:地藏Kelvin

我的Gitee: 地藏Kelvin https://gitee.com/kelvin-cai

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容