Spring 5: Function Web Framework

Hôm nay mình sẽ giới thiệu về một trong những điểm mới của Spring 5. Đó chính là HandlerFunction, RouterFunction, and FilterFunction. Bình thường khi làm việc với Spring của các phiên bản trước, nếu muốn handle các request thì ta dùng các annotation quen thuộc của Spring là @Controller, @RequestMapping. Tuy nhiên, với Spring 5 ta sẽ có thêm cách handle khác, trước hết ta xem ví dụ dứới đây, ở đây là ví dụ về việc các API expose ra Person object

public interface PersonRepository {
  Mono<Person> getPerson(int id);
  Flux<Person> allPeople();
  Mono<Void> savePerson(Mono<Person> person);
}

Về mặt structure có vẻ như khá giống với cách truyền thống, ngoại trừ 1 vài điểm mới lạ. Ta cùng đi giải thích các điểm mới lạ này. Flux

sẽ trả về một List<Person> như truyền thống, and Mono<Person> sẽ trả về một object Person. Mono<Void> sẽ tương đương với void trong phương pháp truyền thống.

Dưới đây là cách mà ta expose mọi thứ ở controller với việc dùng cách mới của Spring 5:

RouterFunction<?> route = route(GET("/person/{id}"),
  request -> {
    Mono<Person> person = Mono.justOrEmpty(request.pathVariable("id"))
      .map(Integer::valueOf)
      .then(repository::getPerson);
    return Response.ok().body(fromPublisher(person, Person.class));
  })
  .and(route(GET("/person"),
    request -> {
      Flux<Person> people = repository.allPeople();
      return Response.ok().body(fromPublisher(people, Person.class));
    }))
  .and(route(POST("/person"),
    request -> {
      Mono<Person> person = request.body(toMono(Person.class));
      return Response.ok().build(repository.savePerson(person));
    }));

Sau khi start server và access http://localhost:8080/person/1 thì ta được kết quả sau

{
"name": "Tam Nguyen",
"age": 29
}

Trên đây là phần giới thiệu về một ví dụ cách handle request một cách mới lạ với Spring 5 Tiếp theo ta cùng nhau đi sau vào chi tiết.

Key Components

Như đã nói ở trên thì Spring 5 có 3 component quan trọng về mảng request handler, chính là HandlerFunction, RouterFunction, and FilterFunction.

HandlerFunction

Đây thực tế chính là một bản sao của @RequestMapping. Một ví dụ rất đơn giản về handler function với ví dụ huyền thoại HelloWorld:

HandlerFunction<String> helloWorld =
  request -> Response.ok().body(fromObject("Hello World"));

Ngoài ra, theo như ví dụ về PersonRepository ở trên, thì HandlerFunction hỗ trợ đầy đủ các reactive như Flux (list of object), Mono (single object) hay đơn giản là Void (void).

RouterFunction

RouterFunction có chức năng tương tự như một @RequestMapping annotation. Tuy nhiên, có một sự khác biệt quan trọng: với annotation thì request của ta bị hạn chế đối với những gì có thể được thể hiện thông qua các giá trị của annotation, và việc xử lý chúng không phải là không thể override; còn với router function thì code xử lý là ngay trước mặt bạn: bạn có thể override hoặc replace nó khá dễ dàng.

Dưới đây là một ví dụ về một router function với một chức năng xử lý in-line. Nó có vẻ hơi rờm rà, nhưng đừng lo lắng về điều đó: chúng ta sẽ tìm cách để làm cho nó ngắn hơn ở phần dưới.

RouterFunction<String> helloWorldRoute = 
  request -> {
    if (request.path().equals("/hello-world")) {
      return Optional.of(r -> Response.ok().body(fromObject("Hello World")));
    } else {
      return Optional.empty();
    }
  };

Thông thường, bạn không viết các router function hoàn chỉnh mà đúng hơn là import RouterFunctions.route(), cái mà cho phép bạn tạo RouterFunction sử dụng một RequestPredicate (i.e. Predicate<Request>) và một HandlerFunction. Nếu mà Predicate đc apply thì handler function sẽ return, nếu không thì sẽ empty. Xem ví dụ về HelloWorld dưới đây 😃

RouterFunction<String> helloWorldRoute =
  RouterFunctions.route(request -> request.path().equals("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

Function kết hợp

Hai router function có thể được viết chung thành một chức năng, khi đó nó sẽ hoạt động như sau: nếu chức năng đầu tiên không khớp, chức năng thứ hai được thực hiện. Keyword ở đây là RouterFunction.and() , thế thôi.

RouterFunction<?> route =
  route(path("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")))
  .and(route(path("/the-answer"),
    request -> Response.ok().body(fromObject("42"))));

Kiểu method tham chiếu

Các ví dụ ở trên được code theo kiểu inline thông qua lambda, mặc dù nhìn rất ngắn gọn, tuy nhiên về mặt readability thì không được clean cho lắm. Do vậy nếu muốn mọi thứ rõ ràng hơn thì ta vẫn có thể implement theo cách truyền thống, như sau :

class DemoHandler {
  public Response<String> helloWorld(Request request) {
    return Response.ok().body(fromObject("Hello World"));
  }
  public Response<String> theAnswer(Request request) {
    return Response.ok().body(fromObject("42"));
  }
}

Nào bây giờ thì dùng nó trong router function thôi 😃

@Autowired
private DemoHandler handler;
RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer));

FilterFunction

Cái này có chức năng tương tự như @ControllerAdvice hoặc là ServletFilter của cách truyền thống. Rất hữu dụng cho việc ghi log, bắt exception, hay filter request theo một tiêu chí nào đấy. Cái này có thể áp dụng cho nhiều RouterFunction cùng một lúc. Xem ví dụ ghi log dưới đây:

RouterFunction<?> route =
  route(GET("/hello-world"), handler::helloWorld)
  .and(route(GET("/the-answer"), handler::theAnswer))
  .filter((request, next) -> {
    System.out.println("Before handler invocation: " + request.path());
    Response<?> response = next.handle(request);
    Object body = response.body();
    System.out.println("After handler invocation: " + body);
    return response;
  });

Chạy server

Dùng RouterFunctions.toHttpHandler() để start tomcat như sau:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(route);
HttpServlet servlet = new ServletHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
Context rootContext = server.addContext("",
  System.getProperty("java.io.tmpdir"));
Tomcat.addServlet(rootContext, "servlet", servlet);
rootContext.addServletMapping("/", "servlet");
tomcatServer.start();

Tóm lược

  • Handler function handle request bằng cách trả về một response
  • Router function chuyển tiếp tới handler function, và có thể kết hợp nhiều router function với nhau
  • Router function có thể được filter bởi FilterFunction
  • Router function có thể chạy trên một reactive web server

Có một ví dụ khá đơn giản, mọi người có thể tham khảo ở đây 😃


All Rights Reserved