Functional Controllers in Spring MVC – Spring MVC中的功能控制器

最后修改: 2019年 7月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

Spring 5 introduced WebFlux, a new framework that lets us build web applications using the reactive programming model.

Spring 5引入了WebFlux,这是一个新的框架,使我们能够使用反应式编程模型构建Web应用程序。

In this tutorial, we’ll see how we can apply this programming model to functional controllers in Spring MVC.

在本教程中,我们将看到如何将这种编程模型应用于Spring MVC的功能控制器。

2. Maven Setup

2.Maven设置

We’ll be using Spring Boot to demonstrate the new APIs.

我们将使用Spring Boot来演示新的API。

This framework supports the familiar annotation-based approach of defining controllers. But it also adds a new domain-specific language that provides a functional way of defining controllers.

这个框架支持我们熟悉的基于注解的定义控制器的方法。但它也增加了一种新的特定领域语言,提供了一种定义控制器的功能方式。

From Spring 5.2 onwards, the functional approach will also be available in the Spring Web MVC framework. As with the WebFlux module, RouterFunctions and RouterFunction are the main abstractions of this API.

从Spring 5.2开始,函数式方法也将在Spring Web MVC框架中使用。与WebFlux模块一样,RouterFunctionsRouterFunction是该API的主要抽象部分。

So let’s start by importing the spring-boot-starter-web dependency:

因此,让我们从导入spring-boot-starter-web依赖项开始。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. RouterFunction vs @Controller

3.RouterFunction与@Controller对比

In the functional realm, a web service is referred to as a route and the traditional concept of @Controller and @RequestMapping is replaced by a RouterFunction.

在功能领域,Web服务被称为路由@Controller@RequestMapping的传统概念被RouterFunction取代。

To create our first service, let’s take an annotation-based service and see how it can be translated into its functional equivalent.

为了创建我们的第一个服务,让我们采取一个基于注释的服务,看看如何将其转化为功能上的等价物。

We’ll use the example of a service that returns all the products in a product catalog:

我们将使用一个返回产品目录中所有产品的服务的例子。

@RestController
public class ProductController {

    @RequestMapping("/product")
    public List<Product> productListing() {
        return ps.findAll();
    }
}

Now, let’s look at its functional equivalent:

现在,让我们来看看它的功能等同物。

@Bean
public RouterFunction<ServerResponse> productListing(ProductService ps) {
    return route().GET("/product", req -> ok().body(ps.findAll()))
      .build();
}

3.1. The Route Definition

3.1.路线定义

We should note that in the functional approach, the productListing() method returns a RouterFunction instead of the response body. It’s the definition of the route, not the execution of a request.

我们应该注意到,在函数式方法中,productListing()方法返回一个RouterFunction,而不是响应体。这是路由的定义,而不是请求的执行。

The RouterFunction includes the path, the request headers, a handler function, which will be used to generate the response body and the response headers. It can contain a single or a group of web services.

RouterFunction包括路径、请求头、处理函数,它将被用来生成响应体和响应头。它可以包含单个或一组网络服务。

We’ll cover groups of web services in more detail when we look at Nested Routes.

当我们研究嵌套路由时,我们将更详细地介绍网络服务组。

In this example, we’ve used the static route() method in RouterFunctions to create a RouterFunction. All the requests and response attributes for a route can be provided using this method.

在这个例子中,我们使用了RouterFunctions中的static route()方法来创建一个RouterFunction一个路由的所有请求和响应属性都可以用这个方法提供。

3.2. Request Predicates

3.2.请求谓词

In our example, we use the GET() method on route() to specify this is a GET request, with a path provided as a String.

在我们的例子中,我们在route()上使用GET()方法来指定这是一个GET请求,并以字符串的形式提供路径。

We can also use the RequestPredicate when we want to specify more details of the request.

当我们想要指定请求的更多细节时,我们也可以使用RequestPredicate

For example, the path in the previous example can also be specified using a RequestPredicate as:

例如,前面的例子中的路径也可以用RequestPredicate指定为。

RequestPredicates.path("/product")

Here, we’ve used the static utility RequestPredicates to create an object of RequestPredicate.

这里,我们使用静态工具RequestPredicates来创建RequestPredicate的对象。

3.3. Response

3.3.响应

Similarly, ServerResponse contains static utility methods that are used to create the response object.

同样地,ServerResponse包含静态实用方法,用于创建响应对象

In our example, we use ok() to add an HTTP Status 200 to the response headers and then use the body() to specify the response body.

在我们的例子中,我们使用ok()在响应头中添加HTTP状态200,然后使用body()来指定响应体。

Additionally, ServerResponse supports the building of response from custom data types using EntityResponse. We can also use Spring MVC’s ModelAndView via RenderingResponse.

此外,ServerResponse支持使用EntityResponse从自定义数据类型构建响应。我们还可以通过RenderingResponse使用Spring MVC的ModelAndView

3.4. Registering the Route

3.4.注册路线

Next, let’s register this route using the @Bean annotation to add it to the application context:

接下来,让我们使用@Bean注解来注册这个路由,将其添加到应用上下文中。

@SpringBootApplication
public class SpringBootMvcFnApplication {

    @Bean
    RouterFunction<ServerResponse> productListing(ProductController pc, ProductService ps) {
        return pc.productListing(ps);
    }
}

Now, let’s implement some common use cases we come across while developing web services using the functional approach.

现在,让我们来实现一些我们在使用功能方法开发Web服务时遇到的常见用例。

4. Nested Routes

4.嵌套路线

It’s quite common to have a bunch of web services in an application and also have them divided into logical groups based on function or entity. For example, we may want all services related to a product, to begin with,/product.

在一个应用程序中拥有一堆网络服务,并且根据功能或实体将它们划分为逻辑组,这是非常常见的。例如,我们可能希望所有与产品有关的服务,以/product开始。

Let’s add another path to the existing path /product to find a product by its name:

让我们在现有的路径/product上添加另一个路径,以便根据产品的名称找到它。

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route().nest(RequestPredicates.path("/product"), builder -> {
        builder.GET("/name/{name}", req -> ok().body(ps.findByName(req.pathVariable("name"))));
    }).build();
}

In the traditional approach, we’d have achieved this by passing a path to @Controller. However, the functional equivalent for grouping web services is the nest() method on route().

在传统的方法中,我们会通过向@Controller传递一个路径来实现这一目标。然而,分组Web服务的功能等价物是路由()的nest()方法。

Here, we start by providing the path under which we want to group the new route, which is /product. Next, we use the builder object to add the route similarly as in the previous examples.

在这里,我们首先提供了我们想要分组的新路由的路径,也就是/product。接下来,我们使用构建器对象来添加路由,与前面的例子类似。

The nest() method takes care of merging the routes added to the builder object with the main RouterFunction.

nest()方法负责将添加到构建器对象的路由与主RouterFunction合并。

5.  Error Handling

5. 错误处理

Another common use case is to have a custom error handling mechanism. We can use the onError() method on route() to define a custom exception handler.

另一个常见的用例是要有一个自定义的错误处理机制。我们可以使用route()上的onError()方法来定义一个自定义异常处理程序

This is equivalent to using the @ExceptionHandler in the annotation-based approach. But it is far more flexible since it can be used to define separate exception handlers for each group of routes.

这等同于在基于注解的方法中使用@ExceptionHandler。但它要灵活得多,因为它可以用来为每组路由定义单独的异常处理程序。

Let’s add an exception handler to the product search route we created earlier to handle a custom exception thrown when a product is not found:

让我们给我们先前创建的产品搜索路线添加一个异常处理程序,以处理未找到产品时抛出的自定义异常。

public RouterFunction<ServerResponse> productSearch(ProductService ps) {
    return route()...
      .onError(ProductService.ItemNotFoundException.class,
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.NOT_FOUND)
           .build())
      .build();
}

The onError() method accepts the Exception class object and expects a ServerResponse from the functional implementation.

onError()方法接受Exception类对象,并期望从功能实现获得ServerResponse

We have used EntityResponse which is a subtype of ServerResponse to build a response object here from the custom datatype Error. We then add the status and use EntityResponse.build() which returns a ServerResponse object.

我们使用了EntityResponse,它是ServerResponse的一个子类型,在此从自定义数据类型Error构建一个响应对象。然后我们添加状态并使用EntityResponse.build(),它返回一个ServerResponse对象。

6. Filters

6.过滤器

A common way of implementing authentication as well as managing cross-cutting concerns such as logging and auditing is using filters. Filters are used to decide whether to continue or abort the processing of the request.

实现认证以及管理跨领域问题(如日志和审计)的一种常见方式是使用过滤器。过滤器用于决定是否继续或中止对请求的处理。

Let’s take an example where we want a new route that adds a product to the catalog:

让我们举个例子,我们想要一个新的路线,把一个产品添加到目录中。

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
    return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
      .onError(IllegalArgumentException.class, 
         (e, req) -> EntityResponse.fromObject(new Error(e.getMessage()))
           .status(HttpStatus.BAD_REQUEST)
           .build())
        .build();
}

Since this is an admin function we also want to authenticate the user calling the service.

由于这是一个管理函数,我们也想对调用服务的用户进行认证。

We can do this by adding a filter() method on route():

我们可以通过在route()上添加一个filter()方法来做到这一点:

public RouterFunction<ServerResponse> adminFunctions(ProductService ps) {
   return route().POST("/product", req -> ok().body(ps.save(req.body(Product.class))))
     .filter((req, next) -> authenticate(req) ? next.handle(req) : 
       status(HttpStatus.UNAUTHORIZED).build())
     ....;
}

Here, as the filter() method provides the request as well as the next handler, we use it to do a simple authentication which allows the product to be saved if successful or returns an UNAUTHORIZED error to the client in case of failure.

在这里,由于filter()方法提供了请求以及下一个处理程序,我们用它来做一个简单的认证,如果成功的话,允许保存产品,如果失败的话,则返回一个UNAUTHORIZED错误给客户端。

7. Cross-Cutting Concerns

7.跨领域的关注问题

Sometimes, we may want to perform some actions before, after or around a request. For example, we may want to log some attributes of the incoming request and outgoing response.

有时,我们可能想在一个请求之前、之后或周围执行一些操作。例如,我们可能想记录传入请求和传出响应的一些属性。

Let’s log a statement every time the application finds a matching for the incoming request. We’ll do this using the before() method on route():

让我们在应用程序每次为传入的请求找到匹配的时候记录一条语句。我们将使用route()上的before()方法来做到这一点:

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .before(req -> {
          LOG.info("Found a route which matches " + req.uri()
            .getPath());
          return req;
      })
      .build();
}

Similarly, we can add a simple log statement after the request has been processed using the after() method on route():

同样,我们可以在请求被处理后,使用route()上的after()方法添加一个简单的日志语句。

@Bean
RouterFunction<ServerResponse> allApplicationRoutes(ProductController pc, ProductService ps) {
    return route()...
      .after((req, res) -> {
          if (res.statusCode() == HttpStatus.OK) {
              LOG.info("Finished processing request " + req.uri()
                  .getPath());
          } else {
              LOG.info("There was an error while processing request" + req.uri());
          }
          return res;
      })          
      .build();
    }

8. Conclusion

8.结语

In this tutorial, we started with a brief introduction to the functional approach for defining controllers. We then compared Spring MVC annotations with their functional equivalents.

在本教程中,我们首先简要介绍了定义控制器的函数式方法。然后,我们比较了Spring MVC注解和它们的函数式等价物。

Next, we implemented a simple web service that returned a list of products with a functional controller.

接下来,我们实现了一个简单的网络服务,用一个功能控制器返回一个产品列表。

Then we proceeded to implement some of the common use cases for web service controllers, including nesting routes, error handling, adding filters for access control, and managing cross-cutting concerns like logging.

然后,我们继续实现Web服务控制器的一些常见用例,包括嵌套路由、错误处理、为访问控制添加过滤器,以及管理跨领域的问题,如日志。

As always the example code can be found over on GitHub.

像往常一样,可以在GitHub上找到示例代码。