SpringMVC supports version manageable Restful interfaces

need

With the advent of the mobile Internet era, the mode of software development is also changing. I remember that I used to do B/S background development, basically there is no Http interface, all the final html is displayed to end users through rendering template technology (jsp, freemark). Now it has completely changed. Based on the background interface provider, we have never targeted the background output that is only displayed by the browser, but various terminals, such as android and ios. So you must be careful when designing the interface, once the interface is released, it may be difficult to change forever (unless you force client users to upgrade). We know that Restful API has become a business principle of interface design. If you still don't know what Restful is, I recommend you to read this article:  RESTful API Design Guide  . In fact, we are designing a set of business interfaces based on the http protocol, but as time changes, business changes, or the optimization of our protocol itself, it is possible to change the existing interfaces. At this time, it is very important to perform version management for all interfaces. For example, for an interface that adds users, due to the great development of the business, the field attributes of the interface have changed greatly, so we can only redefine a new interface, which is set by /v1/user /add becomes /v2/user/add, so we need to maintain the logic of two sets of interfaces and map them into the code, that is, to maintain two different business methods. So this article mainly talks about the application developed based on SpringMVC, how to facilitate us to manage different version interfaces at the code level through extension development.

Overview of SpringMVC Principles

The core idea of ​​SpringMVC is to forward the request to each execution method (Controller's method) through a servlet (DispatchServlet). The official picture is as follows:

 It is to map a certain form of URL (of course, the url is not the only determining condition, as well as the request method, get or post, the information in the request header) to the specific method of a certain class, this core component is in SpringMVC Called: HandlerMapping. We generally initialize and load an implementation class of HanlderMapping when we do the following configuration in spring's config file: RequestMappingHandlerMapping:

1

<mvc:annotation-driven/>

As for what this line of configuration does, you can start to see it from the org.springframework.web.servlet.config.MvcNamespaceHandler class. We now define a Controller as follows:

1

2

3

4

5

6

7

8

9

@Controller

public class HelloController {

    @RequestMapping("hello/")

    @ResponseBody

    public String hello(HttpServletRequest request){

        System.out.println("haha1..........");

        return "hello";

    }

}

So we can call it through /hello/. Now if our business for this interface has undergone great changes (involving changes in fields and packets, which are not compatible with the previous ones), but the old interface cannot be abandoned, because you cannot guarantee that the released interface will not be called by anyone. . So we can only change the code to support multiple versions of the interface as follows:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

@Controller

public class HelloController {

    @RequestMapping("v1/hello/")

    @ResponseBody

    public String hello1(HttpServletRequest request){

        System.out.println("haha1..........");

         

        return "hello";

    }

     

    @RequestMapping("v2/hello/")

    @ResponseBody

    public String hello2(HttpServletRequest request){

        System.out.println("haha2.........");

         

        return "hello";

    }

}

Now we can access the interfaces corresponding to the v1 and v2 versions through /v1/hello, /v2/hello respectively. This seems to solve the problem, because every time an interface changes, we just need to write a new method corresponding to the version. But the corresponding problem also comes:

  • The interfaces we generally publish are published at http://api.custom.com/v1 and http://api.custom.com/v2. From v1 to v2, we often only change a small amount of them. Part of the interface, but the client must call with a unified version number. 
  • Cannot be intelligent upward compatible interface. If the highest version of one of our interfaces is v2 now, such as /v2/hello, now we can automatically adapt to /v2/hello through /v3/hello.

So we add several extension classes through Spring's powerful extension mechanism to complete this work. Let's first look at the process of HandlerMapping loading initialization and dynamic url to handler in SringMVC:

 可以看到,HandlerMapping就是通过继承InitializingBean接口在完成实例后,扫描所有的Controller和标识RequestMapping的方法,缓存这个映射对应关系。然后在应用运行的时候,根据请求的request来找到相应的handler来处理这个请求。所以,我们添加扩展类:

  • ApiVersion
  • ApiVesrsionCondition
  • CustomRequestMappingHandlerMapping
  • WebConfig

现分别来看下这个类,首先看下ApiVersion这个注解:

1

2

3

4

5

6

7

8

9

10

11

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Mapping

public @interface ApiVersion {

    /**

     * 版本号

     * @return

     */

    int value();

}

这个注解用来标识某个类或者方法要处理的对应版本号,使用如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

@Controller

@RequestMapping("/{version}/")

public class HelloController {

 

    @RequestMapping("hello/")

    @ApiVersion(1)

    @ResponseBody

    public String hello(HttpServletRequest request){

        System.out.println("haha1..........");

         

        return "hello";

    }

     

    @RequestMapping("hello/")

    @ApiVersion(2)

    @ResponseBody

    public String hello2(HttpServletRequest request){

        System.out.println("haha2.........");

         

        return "hello";

    }

     

    @RequestMapping("hello/")

    @ApiVersion(5)

    @ResponseBody

    public String hello5(HttpServletRequest request){

        System.out.println("haha5.........");

         

        return "hello";

    }

}

现在我们就可以通过 /v1/hello/, /v2/hello/, /v5/hello来分别调用版本1,2,5的管理。当然我们也要解决刚才说的两点问题,如果用户通过 /v4/hello/来访问接口,则要自动适配到 /v2/hello/,因为 v2是比v4低的版本中最新的版本。

再来看下 ApiVersionCondition 这个类。这个类就是我们自定义一个条件筛选器,让SpringMVC在原有逻辑的基本上添加一个版本号匹配的规则:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> {

 

    // 路径中版本的前缀, 这里用 /v[1-9]/的形式

    private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/");

     

    private int apiVersion;

     

    public ApiVesrsionCondition(int apiVersion){

        this.apiVersion = apiVersion;

    }

     

    public ApiVesrsionCondition combine(ApiVesrsionCondition other) {

        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义

        return new ApiVesrsionCondition(other.getApiVersion());

    }

 

    public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) {

        Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getPathInfo());

        if(m.find()){

            Integer version = Integer.valueOf(m.group(1));

            if(version >= this.apiVersion) // 如果请求的版本号大于配置版本号, 则满足

                return this;

        }

        return null;

    }

 

    public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) {

        // 优先匹配最新的版本号

        return other.getApiVersion() - this.apiVersion;

    }

 

    public int getApiVersion() {

        return apiVersion;

    }

 

}

要把这个筛选规则生效的话,要扩展原胡的HandlerMapping,把这个规则设置进去生效,看下CustomRequestMappingHandlerMapping的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

 

    @Override

    protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {

        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);

        return createCondition(apiVersion);

    }

 

    @Override

    protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {

        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);

        return createCondition(apiVersion);

    }

     

    private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {

        return apiVersion == null null new ApiVesrsionCondition(apiVersion.value());

    }

}

 

 最后,得让SpringMVC加载我们定义的CustomRequestMappingHandlerMapping以覆盖原先的RequestMappingHandlerMapping, 所以要去掉前面说的<mvc:annotation-driven/>这个配置,我们通过JavaConfig的方式注入:

1

2

3

4

5

6

7

8

9

10

11

12

@Configuration

public class WebConfig extends WebMvcConfigurationSupport{

 

    @Override

    @Bean

    public RequestMappingHandlerMapping requestMappingHandlerMapping() {

        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();

        handlerMapping.setOrder(0);

        handlerMapping.setInterceptors(getInterceptors());

        return handlerMapping;

    }

}

参考: https://github.com/Ivan97/coco-mvc-demo

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325554748&siteId=291194637