背景
目前建立一個後端請求接口給別人提供服務,不管是使用SpringMVC方式註解,仍是使用SpringCloud的Feign註解,都是須要填寫好@RequestMap、@Controller、@Pathvariable等註解和參數。每一個接口都須要重複的勞動,很是繁瑣。特別是服務治理框架的接口層不是springmvc,而都是經過TCP鏈接來作RPC通訊的接口,這樣的接口調試起來比較麻煩,測試人員也不能感知接口參數,壓力測試的時候沒得使用JMETER方便。html
目的
爲了解放雙手,讓後端服務開發人員提供接口給別人時,只須要更關注邏輯。減小開發人員關注框架內容,減小關注每一個@註解上的參數信息,不用再校驗path 否已經被使用過。無須再感知SpringMVC或者Feign的存在。前端
咱們統一作處理,把類名和方法名來作爲請求接口url,再也不顯式聲明url,默認POST請求、返回爲JSON形式,請求參數支持@RequestBody、@RequestParam。java
點贊再看,關注公衆號:【地藏思惟】給你們分享互聯網場景設計與架構設計方案 掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7git
先看看簡約到什麼程度
@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; } }
上述代碼已生成的功能:web
- url爲 /UserContract/getUserBody的uri,
- 請求方法爲POST
- 而且請求方式支持body方式提交user11對象
- 若是參數是基本類型的話默認是做爲@RequestParam方式請求
- 返回方式爲JSON
- 前端同事一說url是啥,就能定位在代碼的哪一個地方了
你們看,是否是不用再填寫任何的MVC、Feign註解了!!!!!spring
- 只須要使用@Contract註解,咱們就會生成好一個類下全部方法的POST請求接口,並映射到對應方法。
- 讓開發人員只須要關注請求接口內邏輯,再也不須要關注Controller如何生成。 代碼一個MVC註解都沒有,對mvc接口生成無感知。
- 不嵌入實體類構建Bean過程。
- 相較正常的@Controller類,少寫@RequestMapping 等註解和上面的參數,少寫@RequestBody、少寫@RequestBody等參數解析方式。這些都不用再顯式填寫。只須要添加咱們自定義註解,並在服務啓動時的動態生成簡約MVC完成。
使用場景
若是你的請求接口框架經過封裝RPC,底層不是springMVC,但又想增添MVC接口。json
若是你的請求接口框架經過封裝RPC,底層不是springMVC,但又想提供給前端HTML使用。後端
若是你的請求接口框架經過封裝RPC,底層不是springMVC,但又想提供給測試人員方便閱讀,也方便用JMETER作壓力測試。springboot
若是你的請求接口框架經過封裝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。而後作第六、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