Simple MVC dynamically generated request interface | abandon all annotations reduce duplication of effort it

background

At present the request to create a back-end interface to provide service to others, whether using SpringMVC way annotation, or use SpringCloud of Feign notes, are needed to fill @ RequestMap, @ Controller, @ Pathvariable and other annotations and parameters. Each interface requires repetitive labor, very tedious. In particular governance framework service interface layer is not springmvc, which are connected by TCP to do RPC communication interfaces, such an interface more trouble debugging, testing personnel can not perceive the interface parameters, when the stress test did not have to use JMETER easy.

purpose

For hands free, so that the back-end service provides developers with an interface while others only need to pay more attention to logic. Less attention frame content developers to reduce the parameter information on each concern @ comment, do not check whether the path has already been used. No longer sense the presence of SpringMVC or Feign.

We do deal with unity, the class and method names do as request interface url, no longer explicitly declared url, default POST request, returns JSON form, request parameters support @ RequestBody, @ RequestParam.

Front understand

Spring hooks class, hook method

scenes to be used

If your request interface package frame by a RPC, not the underlying springMVC, but want to add MVC interface.

If your request interface package frame by a RPC, not the underlying springMVC, but want to use HTML supplied to the front end.

If your request by encapsulating the RPC interfaces the frame, not the underlying springMVC, but want to provide easy reading testers, but also easy to do with JMETER stress testing.

If your interface is Feign or already springMVC, but still fill url, path, request method, parameter analytical way, every time there is no check ur re-use and other tedious work, you can put down these operate.

Simple take a look at to what extent

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

}

复制代码
  1. Only need to use @Contract comments, we will generate a good class under all methods POST request interface, and mapped to a corresponding method.
  2. Let developers only need to focus on the request within a logical interface, no longer need to focus on how to generate Controller. MVC code is a no comment, no perception of mvc interface generation.
  3. Bean entity class are not embedded build process.
  4. Compared to normal @Controller class, such as notes and write less @RequestMapping the above parameters, write less @RequestBody, write less @RequestBody parameters analytically. These do not have to explicitly fill. We only need to add custom annotations, and complete simple MVC dynamically generated when the service starts.

The code generated features:

  1. url is / UserContract / getUserBody of uri,
  2. POST request method
  3. And requests submission user11 way to support body objects
  4. If the parameter is the base type, then the request is a default mode @RequestParam
  5. Way to return JSON
  6. Colleagues say the front end of a url is valid and will be able to locate where in the code

You see, is not no need to fill in any of the MVC, Feign annotated

demand

  1. Create a url and only method to achieve mvc class of the relationship, not the bean implementation class to create an object container, only concerned with MVC level, not the level of coupling other functions.
  2. Support POST requests
  3. Class and method names stitching become uri
  4. Request parameter support @ RequestParam, @ RequestBody
  5. Return data as JSON
  6. Based springboot

Previously

Take a look at how the original URL and MVC binding method

consice.png

We own realization mainly deal with the second step, into our own RequestMappingHandler. Then do the first step 6,7 rewrite, let's instead look for methods to find @Controller @Contract, approach to generating final url rewrite.

achieve

1. Start the way

First, to achieve start-up mode, using the following annotation on the service starts based on Springboot, indicating that request interface class code in which path. Then @Import (ContractAutoHandlerRegisterConfiguration.class) when the service starts, add the url and relationship classes.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ContractAutoHandlerRegisterConfiguration.class)
public @interface EnableContractConciseMvcRegister {

    /**
     * Contract 注解的请求包扫描路径
     * @return
     */
    String[] basePackages() default {};

}
复制代码

2. import url responsible for loading classes and associated methods of handling relations

Use ImportBeanDefinitionRegistrar, will trigger logic when @import, so that class BeanDefinition registered into the container.

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 into the container utilizing registerBeanDefinitions form.
  2. One important only ContractAutoHandlerRegisterHandlerMapping, ContractReturnValueWebMvcConfigurer. ContractAutoHandlerRegisterHandlerMapping, association method is responsible url and implementation classes (such as UserContractImpl). ContractReturnValueWebMvcConfigurer, parsing and processing the request parameter data conversion method returns.

Here the use of annotations and ImportBeanDefinitionRegistrar realize the need to support springboot 6 container.

3. Methods and URL mapping

Creating ContractAutoHandlerRegisterHandlerMapping inherited RequestMappingHandlerMapping. Rewrite some of the more important ways, one of which is 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;
    }
复制代码

The main reason to extend this class override this method is

  1. After this first step of the above relationship has been put into a container, SpringMVC registration start the class inheritance RequestMappingHandlerMapping InitializingBean interface is performed by the subsequent logic afterPropertiesSet the method *** *** InitializingBean, this is the key entrance, this is to tell the working methods such as bean are built after the completion of the initial processing is completed. (E.g., flowchart step 5)
  2. springMVC native RequestMappingHandlerMapping this time afterPropertiesSet'll sweep your project code for all classes, and will trigger our custom ContractAutoHandlerRegisterHandlerMapping above isHandler method
  3. This isHandler method requires us to judge, to sweep this class meets mvc create interface classes.
  4. We have inherited RequestMappingHandlerMapping, it can be self-defined logic judgment. Determination logic is this class is a class byte code, not interface, and above this class must implement an interface, and this interface requires @Contract annotation (the class code is not posted, is common custom annotations write name!)
  5. This is so that you can mark that we need to dynamically create simple MVC classes, all the methods in this class will be created springMVC request interface, those classes are required to create MVC mark just as the previous sample of *** UserContractImpl *** .

3. How to dynamically create MVC Interface (critical point)

In ContractAutoHandlerRegisterHandlerMapping our custom class, override getMappingForMethod this method, which is used to generate a URL interface, we have our own way so be rewritten.

Because when through on a logical next in line to find your code works after you create simple MVC classes, such as after find UserContractImpl, ContractAutoHandlerRegisterHandlerMapping parent RequestMappingHandlerMapping logic would go to find UserContractImpl all methods and create url, url and then bind method relations . (E.g., of 7-9 steps of the flowchart)

@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 this approach is to deal url all methods under implementation class UserContractImpl, after obtaining url will deal with binding relationship to the MVC container. Subsequent request came, will from this container in accordance map MVC url is key, to find the value, value is the class that implements the method.
  2. getMappingForMethod in the buildRequestMappingByClass own definition of this method is to resolve the class name, our logic is the class name as the first part of the interface uri. Such as: / UserContract
  3. Since buildRequestMappingByMethod processing method is defined as the second part of the method name uri, like / getUser. And here it is set to post a request mode.

3 is completed here needs: the class and method names stitching become uri, needs 2 POST request method

  1. In view of the request interface when springmvc come in, even though the interface method parameter we getUser no comment will be mapped by default @RequestParam parameter name, parameter interface request.
  2. If there is a member variable of the class object, springmvc will default to @RequestBody to handle

Here the need to complete the request parameter 4 support @ RequestParam, @ RequestBody

4. The processing request interface has returned

The first step before registration ContractReturnValueWebMvcConfigurer, is to do with the return process parameters.

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));
}
复制代码

The use InitializingBean WebMvcConfigurationSupport out. To have a custom interface method of annotation @Contract will have special treatment, these methods will be used @ResponseBody return, you do not have to write @ResponseBody in the method of the implementation class

Here demand complete support @ResponseBody 4

Use and Testing

  1. The sample in front of UserContractImpl has written, only need to pay attention to fill @Contract on UserContractImpl the interface (UserContract). Class interface request code posted is not repeated.
  2. Now write springboot startup class, note basePackages request packet path implementation class interface.
@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. After starting, open swagger-ui.html
    image.png

to sum up

So far, we do not use springmvc notes in the project code, can also generate a mapping between interfaces. After this we would never have to write notes SpringMVC can also use SpringMVC, if your company is the default frame RPC interface tcp connections, just use this approach, you can debug their own local, you do not have to write an RPC client access its own interface. Swagger debugging and more convenient to use, and the test can also see the request parameters, it can also be done JMETER stress tests. However, the code has a problem, that is, the more unified approach, the more constraints. We want freedom, constraints on less. So we have this framework, you can only use POST requests and ResponseBody to return, not to jump for the kind of redirect, do not support parameters of analytical methods @PathVariable, not so RestFul style (but can GET POST change the way to an int value on the request parameters), but supports @RequestParam and @RequestBody form, I think it is enough.

Sample Code

gitee.com/kelvin-cai/…


Welcome attention to public numbers, the article step faster

My public number: Jizo thinking

Nuggets: possession of Kelvin

Jane book: possession of Kelvin

My Gitee: Jizo Kelvin gitee.com/kelvin-cai

Guess you like

Origin juejin.im/post/5e8be8bc6fb9a03c6e641f56