I recently used Spring Cloud Feign as an HTTP client in a lot of projects, and encountered a lot of pitfalls, and also generated some ideas about RESTFUL design, which I’d like to document here.

SpringMVC’s request parameter binding mechanism

Users who know the history of Feign will know that Feign itself is a Netflix product, Spring Cloud Feign is based on the native Feign encapsulation, the introduction of a large number of SpringMVC annotation support, making it easier to use by the majority of Spring users, but also produced a not small confusing effect.

So before using Spring Cloud Feign, I would like to introduce a parameter binding mechanism of SpringMVC. A RestController is preconfigured to start an application on the local port 8080 to receive http requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestController
public class BookController {

<!-- more -->

    @RequestMapping(value = "/hello") // <1>
    public String hello(String name) { // <2>
        return "hello" + name;
    }

}

This demo is very simple to write, but the actual springmvc does a lot of compatibility, so that this interface can accept multiple requests.

  1. RequestMapping represents the mapped path to which the endpoint can be mapped using GET,POST,PUT,DELETE methods.
  2. The common request parameter annotations in SpringMVC are @RequestParam, @RequestBody, @PathVariable and so on. name is treated as @RequestParam by default. The parameter String name on the method is obtained by the framework using bytecode techniques to get the name name and automatically detect parameters with key value of name in the request parameters, or you can use @RequestParam("name") to override the name of the variable itself. It is fetched when we carry the name parameter in the url or the name parameter in the form form.
1
2
3
4
5
POST /hello HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

name=formParam

or

1
2
GET /hello?name=queryString HTTP/1.1
Host: localhost:8080

Feign’s request parameter binding mechanism

The above SpringMVC parameter binding mechanism should be very familiar to everyone, but it’s all a little different in Feign.

Let’s look at a very simple, but actually wrong way to write an interface.

1
2
3
4
5
6
7
8
// Note: Wrong way to write the interface
@FeignClient("book")
public interface BookApi {

    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    String hello(String name);

}

Configure the request path.

1
2
3
4
5
6
7
ribbon:
  eureka:
   enabled: false

book:
  ribbon:
    listOfServers: http://localhost:8080

We wrote a FeignClient as we are used to writing SpringMVC’s RestController, and as we thought at the beginning, since the request method is specified as GET, the name should be spliced into the Url as a QueryString, right? Send a GET request like this.

1
2
GET /hello?name=xxx HTTP/1.1
Host: localhost:8080

Whereas, in fact, the RestController does not receive it, we get some hints on the RestController side of the application.

  • Instead of sending the request using the GET method as expected, the POST method
  • The name parameter is not handled correctly and a null value is obtained

Looking at the documentation, we see that without the default annotation, Feign adds the @RequestBody annotation to the parameters by default, and the RequestBody must be included in the request body, while the GET method does not have a request body. So the above two phenomena are explained. Feign forces a POST request when the GET request contains a RequestBody, instead of reporting an error.

By understanding this mechanism, we can avoid many pitfalls in developing Feign interfaces. The solution to the above problem is also simple

  • Add the @RequestParam("name") annotation to the name in the Feign interface. name must be specified and the request parameters of Feign will not be automatically given a default name using the SpringMVC bytecode mechanism.
  • Since Feign uses @RequestBody by default, it is possible to retrofit RestController to receive using @RequestBody. However, request parameters are usually multiple, and it is recommended to use @RequestParam as described above, while @RequestBody is generally only used for submission objects.

Feign Binding Composite Parameters

The above problem is actually due to the analogy with SpringMVC without clear understanding of Feign’s internal mechanism. Similarly, when using objects as parameters, you need to pay attention to such issues.

For such an interface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@FeignClient("book")
public interface BookApi {

    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestBody Book book); // <1>
  
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam("id") String id,@RequestParam("name") String name); // <2>
  
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam Map map); // <3>
  
    // Wrong way of writing
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    Book book(@RequestParam Book book); // <4>

}
  1. Submitting objects using @RequestBody is the most common way to do this.
  2. If there are not many parameters, you can use more than one @RequestParam
  3. Using Map, which is perfectly fine, is not quite object-oriented and it is not immediately obvious from the code what kind of parameters the interface needs.
  4. Wrong usage, Feign does not provide such a mechanism to automatically convert entities to Map.

Using @PathVariable with the RESTFUL specification in Feign

This involves a topic of how to design RESTFUL interfaces, we know that since RESTFUL was proposed in the early 2000s, there is no shortage of articles mentioning resources, contract specifications, CRUD corresponding to add, delete, change and check operations, and so on. The following is my opinion from two practical interfaces.

Find user interface based on id

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user/{userId}",method = RequestMethod.GET)
    String findById(@PathVariable("id") String userId);

}

This should be uncontroversial, and note that the id in the @PathVariable("id") brackets should not be forgotten, as emphasized earlier. What if it’s “find users by email”? It is likely that the subconscious interface would be written like this

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {
  
    @RequestMapping(value = "/user/{email}",method = RequestMethod.GET)
    String findByEmail(@PathVariable("email") String email);

}
  • First look at the Feign problem. email usually contains the ‘. ‘, a special character, can have unexpected results if included in the path. I’m not going to explore how to fix it (you can actually use {email:. +}), because I don’t think it’s by design.
  • A few more questions about the specification. Are the two interfaces similar and should email be placed in the path? This brings us to the original intention of RESTFUL, why the userId attribute is generally considered suitable to appear in the RESTFUL path, because the id itself plays the role of resource location, he is the marker of the resource. Unlike email, which may be unique, it is more of an attribute of the resource, so I believe that non-locative dynamic parameters should not be present in the path. Instead, email should be used as the @RequestParam parameter.

RESUFTL Structured Query

The author has successfully moved from the topic of Feign to the design of the RESTFUL interface, which has led to the length of this article, but I’m not going to open another article about it.

Consider another interface design, query a month of a user’s order, may also carry paging parameters, then the parameters become a lot, according to the traditional design, this should be a query operation, that is, corresponding to the GET request, that does not mean that these parameters should be stitched to the url after? Thinking about Feign, as mentioned in the second paragraph of this article, it does not support GET requests to carry entity classes, which puts us in a design dilemma. In fact, reference to some DSL language design such as elasticSearch, is also the use of POST JSON way to query, so in the actual project, I do not particularly favor CRUD and the four types of requests corresponding to the so-called RESTFUL specification, if we say that the design of RESTFUL should follow what If there is a specification that should be followed for designing RESTFUL, it is probably some other term, such as contract specification and domain-driven design. Structured Queries

1
2
3
4
5
6
7
@FeignClient("order")
public interface BookApi {

    @RequestMapping(value = "/order/history",method = RequestMethod.POST)
    Page<List<Orders>> queryOrderHistory(@RequestBody QueryVO queryVO);

}

RESTFUL behavior qualification

In the actual interface design, I encountered such a requirement, the user module interface needs to support the modification of user password, modify the user’s email, modify the user’s name, and I have previously read an article, is also about abandoning CRUD but with domain-driven design to regulate the definition of RESTFUL interface, and my project coincides with the idea. It seems that these three properties are three properties of the same entity class, which can be designed as follows.

1
2
3
4
5
6
7
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user",method = RequestMethod.POST)
    User update(@RequestBody User user);

}

But in reality, if you consider one more layer, you should think about this: Do these three functions require the same permissions? Should they really be put into a single interface? In fact, I do not want the interface caller to pass an entity, because such behavior is not controllable, completely do not know what properties it actually modified, if you really want to limit the behavior, you also need to add an operation type field in the User, and then in the interface implementation side to check, which is too much trouble. In fact, I think the design of the specification should be as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@FeignClient("user")
public interface UserApi {

    @RequestMapping(value = "/user/{userId}/password/update",method = RequestMethod.POST)
    ResultBean<Boolean> updatePassword(@PathVariable("userId) String userId,@RequestParam("password") password);
    
    @RequestMapping(value = "/user/{userId}/email/update",method = RequestMethod.POST)
    ResultBean<Boolean> updateEmail(@PathVariable("userId) String userId,@RequestParam("email") String email);
    
    @RequestMapping(value = "/user/{userId}/username/update",method = RequestMethod.POST)
    ResultBean<Boolean> updateUsername(@PathVariable("userId) String userId,@RequestParam("username") String username);

}
  • In general, the RESTFUL interface should not have a verb, the update here is not an action, but marks the type of operation, because there may be many types of operations for a property, so I used to add an update suffix to clearly express the desired operation, rather than just rely on GET, POST, PUT, DELETE. In fact, the recommended request method for modifying an operation should be PUT, which is my understanding that update has been used to mark the behavior and PUT is not customary in actual development.
  • password, email, and username are all attributes of user, and userId is the identifier of user, so userId appears as PathVariable in the url, and the three attributes appear in ReqeustParam.

A word about logical deletion, in passing.

If a requirement is to delete a user’s common address, the operation type of this api, which I usually do not design as a DELETE request, also uses delete to mark the operation behavior.

1
2
@RequestMapping(value = "/user/{userId}/address/{addressId}/delete",method = RequestMethod.POST)
    ResultBean<Boolean> updateEmail(@PathVariable("userId") String userId,@PathVariable("userId") String email);

Summary

This article talks about the design of RESTFUL interfaces from Feign’s usage notes, which is actually a mutually complementary behavior. Interface design needs a carrier, so I talk about my understanding of RESTFUL design with Feign’s interface style, and some of the pitfalls in Feign are also the starting point for me to standardize RESTFUL design.


Reference https://www.cnkirito.moe/feign-1/