目前创建一个后端请求接口给别人提供服务,无论是使用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; } } 复制代码
上述代码已生成的功能:
大家看,是不是不用再填写任何的MVC、Feign注解了
先看看原生MVC如何绑定URL和方法
我们自己的实现主要处理第二步,注入我们自己的RequestMappingHandler。然后做第6、7步重写,让找@Controller的方法改为找@Contract,最后重写处理url生成的方法。
首先实现启动方式,使用下述注解放在在Springboot服务启动类上,标明请求接口的实现类代码在哪个路径。然后通过@Import(ContractAutoHandlerRegisterConfiguration.class) 在服务启动时,添加url和类的关联关系。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(ContractAutoHandlerRegisterConfiguration.class) public @interface EnableContractConciseMvcRegister { /** * Contract 注解的请求包扫描路径 * @return */ String[] basePackages() default {}; } 复制代码
利用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映射关系"); } } 复制代码
这里利用注解和ImportBeanDefinitionRegistrar 实现了需求6 支持springboot容器。
创建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; } 复制代码
继承这个类重写这个方法的主要原因是
在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; } 复制代码
这里完成了需求3:类名和方法名拼接成为uri、需求2 POST请求方式
这里完成了需求4 请求参数支持@RequestParam,@RequestBody
之前第一步注册的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
@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); } } 复制代码
到目前为止,我们没有在工程代码中使用springmvc注解,也能生成接口映射关系了。 这样大家以后就再也不用写SpringMVC的注解也能使用SpringMVC了,如果你公司框架默认是tcp连接的RPC接口,只要使用了这种方式,就可以自己本地调试,不用再编写一个RPC客户端来访问自己的接口。使用Swagger调试又比较方便,而且测试同时也能看到请求参数,也可以对其做JMETER压力测试。 不过代码都有一个问题,就是做法越统一,约束就越多。想自由,就约束少。所以我们这个框架,就只能用POST请求,并且ResponseBody来返回,就不适合要跳转重定向页面的那种,也不支持@PathVariable的参数解析方式,没那么RestFul风格(但可以把GET POST方式更改为用int值放在请求参数里),但是支持@RequestParam和@RequestBody形式,我觉得也是足够了。