動態生成簡約MVC請求接口|拋棄一切註解減小重複勞動吧

背景

目前建立一個後端請求接口給別人提供服務,不管是使用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

  1. url爲 /UserContract/getUserBody的uri,
  2. 請求方法爲POST
  3. 而且請求方式支持body方式提交user11對象
  4. 若是參數是基本類型的話默認是做爲@RequestParam方式請求
  5. 返回方式爲JSON
  6. 前端同事一說url是啥,就能定位在代碼的哪一個地方了

你們看,是否是不用再填寫任何的MVC、Feign註解了!!!!!spring

  1. 只須要使用@Contract註解,咱們就會生成好一個類下全部方法的POST請求接口,並映射到對應方法。
  2. 讓開發人員只須要關注請求接口內邏輯,再也不須要關注Controller如何生成。 代碼一個MVC註解都沒有,對mvc接口生成無感知。
  3. 不嵌入實體類構建Bean過程。
  4. 相較正常的@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有沒有重複使用等繁瑣工做,能夠放下這些操做了。

需求

  1. 只建立mvc的url與實現類的方法的關聯關係,不爲實現類建立bean對象入容器,只關注MVC層面,不耦合其餘層面的功能。
  2. 支持POST請求
  3. 類名和方法名拼接成爲uri
  4. 請求參數支持@RequestParam,@RequestBody
  5. 返回數據爲JSON
  6. 基於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映射關係");

    }
}
  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

總結

到目前爲止,咱們沒有在工程代碼中使用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

相關文章
相關標籤/搜索