背景
目前建立一個後端請求介面給別人提供服務,無論是使用SpringMVC方式註解,還是使用SpringCloud的Feign註解,都是需要填寫好@RequestMap、@Controller、@Pathvariable等註解和引數。每個介面都需要重複的勞動,非常繁瑣。特別是服務治理框架的介面層不是springmvc,而都是通過TCP連線來做RPC通訊的介面,這樣的介面除錯起來比較麻煩,測試人員也不能感知介面引數,壓力測試的時候沒得使用JMETER方便。
目的
為了解放雙手,讓後端服務開發人員提供介面給別人時,只需要更關注邏輯。減少開發人員關注框架內容,減少關注每個@註解上的引數資訊,不用再校驗path是否已經被使用過。無須再感知SpringMVC或者Feign的存在。
我們統一做處理,把類名和方法名來做為請求介面url,不再顯式宣告url,預設POST請求、返回為JSON形式,請求引數支援@RequestBody、@RequestParam。
前置瞭解
Spring的鉤子類、鉤子方法
先看看簡約到什麼程度
@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做壓力測試。
如果你的介面是Feign或者已經是springMVC,但是還在填寫url、path、請求method、引數解析方式、每次都要核對ur有沒有重複使用等繁瑣工作,可以放下這些操作了。
需求
- 只建立mvc的url與實現類的方法的關聯關係,不為實現類建立bean物件入容器,只關注MVC層面,不耦合其他層面的功能。
- 支援POST請求
- 類名和方法名拼接成為uri
- 請求引數支援@RequestParam,@RequestBody
- 返回資料為JSON
- 基於springboot
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形式,我覺得也是足夠了。
程式碼樣例
歡迎關注公眾號,文章更快一步
我的公眾號 :地藏思維
掘金:地藏Kelvin
簡書:地藏Kelvin
我的Gitee: 地藏Kelvin gitee.com/kelvin-cai