动态生成简约MVC请求接口|抛弃一切注解减少重复劳动吧
背景
目前创建一个后端请求接口给别人提供服务,无论是使用SpringMVC方式注解,还是使用SpringCloud的Feign注解,都是需要填写好@RequestMap、@Controller、@Pathvariable等注解和参数。每个接口都需要重复的劳动,非常繁琐。特别是服务治理框架的接口层不是springmvc,而都是通过TCP连接来做RPC通信的接口,这样的接口调试起来比较麻烦,测试人员也不能感知接口参数,压力测试的时候没得使用JMETER方便。
目的
为了解放双手,让后端服务开发人员提供接口给别人时,只需要更关注逻辑。减少开发人员关注框架内容,减少关注每个@注解上的参数信息,不用再校验path是否已经被使用过。无须再感知SpringMVC或者Feign的存在。
我们统一做处理,把类名和方法名来做为请求接口url,不再显式声明url,默认POST请求、返回为JSON形式,请求参数支持@RequestBody、@RequestParam。
点赞再看,关注公众号:【地藏思维】给大家分享互联网场景设计与架构设计方案
掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7
先看看简约到什么程度
@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;
}
}
上述代码已生成的功能:
- url为 /UserContract/getUserBody的uri,
- 请求方法为POST
- 并且请求方式支持body方式提交user11对象
- 如果参数是基本类型的话默认是作为@RequestParam方式请求
- 返回方式为JSON
- 前端同事一说url是啥,就能定位在代码的哪个地方了
大家看,是不是不用再填写任何的MVC、Feign注解了!!!!!
- 只需要使用@Contract注解,我们就会生成好一个类下所有方法的POST请求接口,并映射到对应方法。
- 让开发人员只需要关注请求接口内逻辑,不再需要关注Controller如何生成。
代码一个MVC注解都没有,对mvc接口生成无感知。 - 不嵌入实体类构建Bean过程。
- 相较正常的@Controller类,少写@RequestMapping 等注解和上面的参数,少写@RequestBody、少写@RequestBody等参数解析方式。这些都不用再显式填写。只需要添加我们自定义注解,并在服务启动时的动态生成简约MVC完成。
使用场景
如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想增添MVC接口。
如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想提供给前端HTML使用。
如果你的请求接口框架通过封装RPC,底层不是springMVC,但又想提供给测试人员方便阅读,也方便用JMETER做压力测试。
如果你的请求接口框架通过封装RPC,底层不是springMVC,想自测接口的时候,没有http接口来做自测,要么写个单元测试每次都启动一下spring来自测整个接口逻辑,这样耗时的情况。
如果你的接口是Feign或者已经是springMVC,但是还在填写url、path、请求method、参数解析方式、每次都要核对ur有没有重复使用等繁琐工作,可以放下这些操作了。
需求
- 只创建mvc的url与实现类的方法的关联关系,不为实现类创建bean对象入容器,只关注MVC层面,不耦合其他层面的功能。
- 支持POST请求
- 类名和方法名拼接成为uri
- 请求参数支持@RequestParam,@RequestBody
- 返回数据为JSON
- 基于springboot
前置了解
Spring的钩子类、钩子方法
Previously
先看看原生MVC如何绑定URL和方法
我们自己的实现主要处理第二步,注入我们自己的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映射关系");
}
}
- 利用Import形式registerBeanDefinitions时注入容器。
- 其中重要的只有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;
}
继承这个类重写这个方法的主要原因是
- 经过上面第一步已经把这个关联关系放入容器中后,启动SpringMVC注册时,上述RequestMappingHandlerMapping这个类有继承InitializingBean接口,就是通过这个InitializingBean的afterPropertiesSet方法执行后续的逻辑,这个是入口的关键,这个就是告诉等bean都构建完成后初始工作完成后处理的工作方法。(如流程图第5步)
- springMVC原生RequestMappingHandlerMapping的afterPropertiesSet 这个时候会扫你工程代码里所有类,并且会触发我们自定义的ContractAutoHandlerRegisterHandlerMapping上述的isHandler方法
- 这个isHandler方法就需要我们去判断,扫到的这个类是否符合创建mvc接口的类。
- 我们继承了RequestMappingHandlerMapping,就可以自定义判断的逻辑。判断的逻辑就是这个class字节码是个类,不是interface,并且这个类上面必须有implement了一个interface,而且这个interface需要有@Contract注解(这个类没有贴代码,就是自定义普通的注解,写个名字就好了)
- 这样就可以标记这是我们需要动态创建简约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;
}
- getMappingForMethod这个方法就是为了,处理实现类UserContractImpl下所有方法的url,得到url后会处理绑定关系到MVC的容器中。后续请求进来了,就会从这个MVC的容器map中根据url为key,找到value,value就是实现类的方法。
- getMappingForMethod里的自己定义的buildRequestMappingByClass这个方法就是解析类名,我们的逻辑就是把类名作为接口uri的第一部分。如:/UserContract
- 自定义的buildRequestMappingByMethod就是处理方法,把方法名作为uri的第二部分,如/getUser。并且在这里设定了为post作为请求方式.
这里完成了需求3:类名和方法名拼接成为uri、需求2 POST请求方式
- 鉴于springmvc请求接口进来时,即使我们接口方法getUser的参数没有注解,都会默认使用@RequestParam通过参数名字来映射,请求接口的参数。
- 如果是有成员变量的类对象,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
使用与测试
- 前面样例的UserContractImpl已经写了,只需要注意在UserContractImpl的interface(UserContract)上填@Contract。请求接口的代码类就不重复贴了。
- 现在编写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);
}
}
- 启动后,打开swagger-ui.html
总结
到目前为止,我们没有在工程代码中使用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
动态生成简约MVC请求接口|抛弃一切注解减少重复劳动吧的更多相关文章
- Roslyn 编译器Api妙用:动态生成类并实现接口
在上一篇文章中有讲到使用反射手写IL代码动态生成类并实现接口. 反射的妙用:C#通过反射动态生成类型继承接口并实现 有位网友推荐使用 Roslyn 去脚本化动态生成,今天这篇文章就主要讲怎么使用 Ro ...
- 动态生成验证码———MVC版
上面有篇博客也是写的验证码,但那个是适用于asp.net网站的. 今天想在MVC中实现验证码功能,弄了好久,最后还是看博友文章解决的,感谢那位博友. 首先引入生成验证码帮助类. ValidateCod ...
- Spring MVC请求到处理方法注解配置的几种方式
@RequestMapping 这个是最常用的注解,可以配置在类上,也可以配置在方法上,两个一起作用组成方法能够响应的请求路径,举例如下 package org.zln.myWeb.controlle ...
- 反射的妙用:C#通过反射动态生成类型继承接口并实现
起因 最近想自己鼓捣个RPC,想着简化RPC调用方式,直接申明接口,然后根据接口的属性去配置RPC调用的相关信息.有一种说法叫申明式调用. 简单来说就是,申明一个interface,动态继承并实例化, ...
- 《React后台管理系统实战 :三》header组件:页面排版、天气请求接口及页面调用、时间格式化及使用定时器、退出函数
一.布局及排版 1.布局src/pages/admin/header/index.jsx import React,{Component} from 'react' import './header. ...
- js动态生成数据列表
我们通常会使用table标签来展示数据内容,由于需要展示的数据内容是随时更换的,所以不可能将展示的数据列表写死在html写死在页面中,而是需要我们根据后台传来的数据随时更换,这个时候就需要我们使用js ...
- C# 动态创建SQL数据库(二) 在.net core web项目中生成二维码 后台Post/Get 请求接口 方式 WebForm 页面ajax 请求后台页面 方法 实现输入框小数多 自动进位展示,编辑时实际值不变 快速掌握Gif动态图实现代码 C#处理和对接HTTP接口请求
C# 动态创建SQL数据库(二) 使用Entity Framework 创建数据库与表 前面文章有说到使用SQL语句动态创建数据库与数据表,这次直接使用Entriy Framwork 的ORM对象关 ...
- C# 动态生成word文档 [C#学习笔记3]关于Main(string[ ] args)中args命令行参数 实现DataTables搜索框查询结果高亮显示 二维码神器QRCoder Asp.net MVC 中 CodeFirst 开发模式实例
C# 动态生成word文档 本文以一个简单的小例子,简述利用C#语言开发word表格相关的知识,仅供学习分享使用,如有不足之处,还请指正. 在工程中引用word的动态库 在项目中,点击项目名称右键-- ...
- 使用Python的Flask框架,结合Highchart,动态渲染图表(Ajax 请求数据接口)
参考链接:https://www.highcharts.com.cn/docs/ajax 参考链接中的示例代码是使用php写的,这里改用python写. 需要注意的地方: 1.接口返回的数据格式,这个 ...
随机推荐
- Shell编程—基础脚本
1. 使用多个命令 如果要两个命令或者多个命令一起运行,可以把它们放在同一行中,彼此间用分号隔开. 2. 创建 shell 脚本文件 例如: #!/bin/bash # This script dis ...
- java23种设计模式——八、组合模式
目录 java23种设计模式-- 一.设计模式介绍 java23种设计模式-- 二.单例模式 java23种设计模式--三.工厂模式 java23种设计模式--四.原型模式 java23种设计模式-- ...
- 谷歌分析(GA)新版的有哪些改变
http://www.wocaoseo.com/thread-221-1-1.html 最近GA做了两次大规模改版,修改了GA使用率最高的traffic source.content面板以及最核心的a ...
- npm install @wepy/cli -g 出错
npm install @wepy/cli -g 出错:npm ERR! Unexpected end of JSON input while parsing near '...1.0.0" ...
- 我用 Java 8 写了一段逻辑,同事直呼看不懂,你试试看。。
业务背景 首先,业务需求是这样的,从第三方电商平台拉取所有订单,然后保存到公司自己的数据库,需要判断是否有物流信息,如果有物流信息,还需要再进行上传. 而第三方接口返回的数据是 JSON 格式的,其中 ...
- tars
动手实践Tars服务的搭建 https://blog.csdn.net/sunshine1314/article/details/81151080 Tars-Go 服务 Hello World——从 ...
- stf-多设备管理平台搭建
项目地址: https://github.com/openstf/stf 安装.使用命令 # 安装stfbrew install rethinkdb graphicsmagick zeromq pro ...
- Fitness - 05.04
倒计时241天 运动38分钟,共计9组.拉伸10分钟. 每组跑步2分钟(6.3KM/h),走路2分钟(6KM/h). 上午下了课,直奔健身房. 手机坏了,没有听音乐. 没有吃午饭,但是上午喝的咖啡还是 ...
- java初探(1)之防止库存为负以及防超买
在秒杀业务中,会出现当只剩一个库存时,但有多个人仍然秒杀成功,且都减库存成功,因此,在减库存,更新数据库的时候,需要在sql语句上进行判断,是否库存大于0. @Update("update ...
- 剑指 Offer 42. 连续子数组的最大和
题目描述 输入一个整型数组,数组中的一个或连续多个整数组成一个子数组.求所有子数组的和的最大值. 要求时间复杂度为\(O(n)\). 示例1: 输入: nums = [-2,1,-3,4,-1,2,1 ...