vlambda博客
学习文章列表

读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务

Building Your First Spring RESTful Service with Kotlin

在上一章中,我们刚刚启动了 Spring 应用程序。如您所见,它运行并停止。这是因为我们没有在我们的应用程序中实现任何可以使用的东西。您还会注意到,当您启动应用程序时,其中一个引人注目的日志显示 :: Spring Boot ::。那是因为我们使用 Spring Boot 来运行应用程序。 Spring Boot 是一个框架,我们将使用它来简化应用程序的引导和开发。它使开发人员无需定义样板配置。

正如我们已经说过的,当我们运行我们的 Spring 应用程序时,Spring Boot 会启动和终止,因为没有什么可做的。在本章中,我们将定义一些东西。我们将创建我们的第一个实现!

在这里,您将了解以下主题:

  • Defining dependencies for our project
  • Our first controller
  • Adding a data class
  • Adding an @Service component

Defining dependencies for our project

我们需要为我们的项目添加几个依赖项。我们需要的第一个是 Spring Context 依赖项。我们还需要 Spring AOP、Boot Starter Web、Actuator、Spring Web 和 Spring Web MVC。该应用程序几乎是裸露的,所以让我们扩展我们的依赖项:

... 
dependencies { 
    compile 'org.springframework:spring-context' 
    compile 'org.springframework:spring-aop' 
    compile('org.springframework.boot:spring-boot-starter') 
    compile('org.springframework.boot:spring-boot-starter-web') 
    compile 'org.springframework.boot:spring-boot-starter-actuator' 
    compile 'org.springframework:spring-web' 
    compile 'org.springframework:spring-webmvc' 
    compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:${kotlinVersion}") 
    compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") 
    testCompile('org.springframework.boot:spring-boot-starter-test') 
} 
... 
这些依赖关系至关重要!

Spring Context 将为我们的应用程序提供以下内容:

  • Core support for dependency injection
  • Core support for transaction management
  • Core support for web applications
  • Core support for data access
  • Core support for messaging
  • Core support for testing
  • Miscellaneous

Spring AOP 将为我们提供面向方面编程的支持。

Spring Actuator 是 Spring Boot 的一个子项目。它为您的 Spring 应用程序添加了几个重要的服务。在您的 Spring Boot 应用程序中配置 Actuator 时,您可以通过执行 Spring Boot Actuator 公开的其他 HTTP 端点来执行交互并监控您的应用程序行为。 Actuator 提供以下开箱即用的端点:

  • Application health
  • Bean details
  • Version details
  • Configurations
  • Logger details, and many more

Spring Web 和 Web MVC 将为我们的应用程序提供对 Spring MVC、请求映射和所有相关功能的访问。需要注意的是,由于我们没有执行测试,因此感谢 Spring Boot Gradle 插件,我们获得了以下好处:

  • Collecting all JARs on the classpath and building a single, runnable JAR
  • Searching for the main() application method to flag as a runnable class that is an entry point to our application
  • Providing a dependency resolver that sets the version number to match Spring Boot dependencies

除此之外,Spring Boot Starter Web (spring-boot-starter-web) 添加了构建 Web 应用程序所需的依赖项。这包括使用 Spring MVC 的 RESTful 应用程序,这正是我们所需要的。 Tomcat 被用作我们应用程序的默认嵌入式容器。没有它,我们将无法运行我们的应用程序。

Our first controller

什么是控制器?你还记得我们说过 Spring Framework 的主要好处之一是 MVC 吗?你记得 C 代表控制器。好吧,我们现在将定义一个。在应用程序的根包中创建一个controller包和一个成员类NoteController

package com.journaler.api.controller 
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration 
import org.springframework.http.MediaType 
import org.springframework.web.bind.annotation.GetMapping 
import org.springframework.web.bind.annotation.RequestMapping 
import org.springframework.web.bind.annotation.RestController 
 
@RestController 
@RequestMapping("/notes") 
@EnableAutoConfiguration 
class NoteController { 
 
    @GetMapping( 
            value = "/obtain", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes() : String { 
        return "Work in progress." 
    } 
 
} 

让我们解释一下这段代码发生了什么。 NoteController 类将负责与 note 实体相关的所有操作。该类本身应用了几个非常重要的注释:

  • @RestController is an annotation that is itself annotated with @Controller and @ResponseBody. The difference between a standard MVC controller (@Controller) and @RestController is in how the HTTP response body is created. @Controller is used with View technology (usually returning HTML as a result). @RestController returns an instance of a class that is serialized in an HTTP response as JSON or XML.
  • @RequestMapping("/notes") is an annotation with which we map all requests, starting with "notes/", to this class. We can map the whole path of an API call or map just methods, as in our case.
  • @GetMapping(value = "/obtain",produces = arrayOf(MediaType.APPLICATION_JSON_VALUE)), we map GET HTTP method to "obtain" path. The result we expect is JSON. So the getNotes() method will be triggered each time the client triggers the GET method for the URL http://host/notes/obtain. We will, for now, just return a simple string.

What else is available?

我们可以通过多种方式映射请求,并支持具有不同媒体类型(JSON、XML 等)的多种方法。对于 HTTP 方法映射,Spring 支持以下注解:

  • @PostMapping maps the POST method
  • @PutMapping maps the PUT method
  • @DeleteMapping maps the DELETE method, and so on

同样可以这样实现:

@RequestMapping( 
            method = arrayOf(RequestMethod.GET), 
            value = "/obtain", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
) fun getNotes(): String ... 
请注意,我们可以分配多个 HTTP 方法来请求映射!

我们来看看注解接受的参数。打开 @GetMapping 注释的源代码。您会注意到,实际上,所有参数都在 @RequestMapping 注释中进行了解释。按照它并打开类文件。可以将以下参数传递给注解:

  • name: Assigning a name to mapping.
  • value: Mapping we assign. This parameter can also accept multiple mappings, for example:
value={"", "/something", "something*", "something/*, **/something"}  
  • path: Only in the Servlet environment, the path mapping URIs
  • method: HTTP method to map GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, and TRACE
  • params: Parameters of the mapping
  • headers: Headers of the mapping
  • consumes: Consumable media types of the mapping
  • produces: Producible media types of the mapping

Adding a data class

目前,getNotes() 方法只返回一个值为 Work in progress 的字符串。这不是我们期望的笔记列表。为了能够返回笔记,我们必须定义它。使用 Note 成员类创建一个名为 data 的新包。 确保它的定义如下:

package com.journaler.api.data 
data class Note( 
        var id: String = ,"" 
        var title: String, 
        var message: String, 
         
var location: String = "" 
) 

这是一个普通的 Kotlin 数据类。我们将 Note 类定义为具有三个必填字段和一个可选字段的类。更新 NoteController 使其返回两个硬编码的注释:

@RestController 
@RequestMapping("/notes") 
class NoteController { 
 
    @GetMapping( 
            value = "/obtain", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes(): List<Note> { 
        return listOf( 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My first note", 
                        "This is a message for the 1st note." 
                ), 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My second note", 
                        "This is a message for the 2nd note." 
                ) 
         
) 
} 
} 

Drafting other API calls

在下一章中,我们将从数据库中获取数据并动态返回所有内容,但现在,我们至少会很高兴拥有这些虚拟数据。让我们将目前为止学到的所有知识应用到我们其余的 API 调用中。

为 TODO 定义一个实体并创建一个具有以下字段的 Todo 数据类:

 
package com.journaler.api.data 
 
data class Todo( 
        var id: String = "", 
        var title: String, 
        var message: String, 
        var schedule: Long, 
         
var location: String = "" 
) 

我们准备为 TODO 编写一个控制器类。在我们这样做之前,让我们草拟其余的 Note 实体 API 调用:

 
package com.journaler.api.controller 
 
import com.journaler.api.data.Note 
import org.springframework.http.MediaType 
import org.springframework.web.bind.annotation.* 
import java.util.* 
 
@RestController 
@RequestMapping("/notes") 
class NoteController { 
 
    /** 
     * Get notes. 
     */ 
    @GetMapping( 
            value = "/obtain", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes(): List<Note> { 
        return listOf( 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My first note", 
                        "This is a message for the 1st note." 
                ), 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My second note", 
                        "This is a message for the 2nd note." 
                ) 
        ) 
    } 
 
    /** 
     * Insert note. 
     * It consumes JSON, that is: request body Note. 
     */ 
    @PutMapping( 
            value = "/insert", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertNote( 
            @RequestBody note: Note 
    ): Note { 
        note.id = UUID.randomUUID().toString() 
        return note 
    } 
 
    /** 
     * Remove note by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/delete/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteNote(@PathVariable(name = "id") id: String): Boolean { 
        println("Removing: $id") 
        return true 
    } 
 
     /** 
     * Update item. 
     * It consumes JSON, that is: request body Note. 
     * As result it returns updated Note. 
     */ 
    @PostMapping( 
            value = "/update", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
     
fun updateNote(@RequestBody note:Note): Note { 
        note.title += " [ updated ]" 
        note.message += " [ updated ]" 
         
return note 
     
} 
 
 
} 

对于 Note,我们添加了 INSERTUPDATEDELETE 方法。正如您在此处看到的,我们演示了如何使用 consumes 参数。我们还介绍了@PathVariable的使用,所以我们的调用是通过路径参数化的。

现在在与 NoteController 相同的包中创建 TodoController

package com.journaler.api.controller 
 
import com.journaler.api.data.Todo 
import org.springframework.http.MediaType 
import org.springframework.web.bind.annotation.* 
import java.util.* 
 
@RestController 
@RequestMapping("/todos") 
class TodoController { 
 
    /** 
     * Get todos. 
     */ 
    @GetMapping( 
            value = "/obtain", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getTodos(): List<Todo> { 
        return listOf( 
                Todo( 
                        UUID.randomUUID().toString(), 
                        "My first todo", 
                        "This is a message for the 1st todo.", 
                        System.currentTimeMillis() 
                ), 
                Todo( 
                        UUID.randomUUID().toString(), 
                        "My second todo", 
                        "This is a message for the 2nd todo.", 
                        System.currentTimeMillis() 
                ) 
        ) 
    } 
 
    /** 
     * Insert item. 
     * It consumes JSON, that is: request body Todo. 
     */ 
    @PutMapping( 
            value = "/insert", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertTodo( 
            @RequestBody todo: Todo 
    ): Todo { 
        todo.id = UUID.randomUUID().toString() 
        return todo 
    } 
 
    /** 
     * Remove item by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/delete/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteTodo(@PathVariable(name = "id") id: String): Boolean { 
        println("Removing: $id") 
        return true 
    } 
 
    /** 
     * Update item. 
     * It consumes JSON, that is: request body Todo. 
     * As result it returns updated Todo. 
     */ 
    @PostMapping( 
            value = "/update", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateTodo(@RequestBody todo: Todo): Todo { 
        todo.title += " [ updated ]" 
        todo.message += " [ updated ]" 
        todo.schedule = System.currentTimeMillis() 
        return todo
    } 
 
} 

实现实际上是相同的,只是它处理 Todo 实体并引入了一个附加字段。

更新方法将字符串附加到标题和消息。在我们的回复中,我们可以看到这确实有效。在实践中,我们只会更新数据库中的数据,而不会对从有效负载到服务器端的数据进行任何修改。

由于我们已经知道基于 HTTP 方法对性能执行的操作,我们将从除 DELETE 之外的每个 HTTP 方法映射中删除 value 参数。 DELETE HTTP 映射将保留 ID 部分:

  • NoteController, responsible for Note entity-related things:
@RestController 
@RequestMapping("/notes") 
class NoteController { 
 
    /** 
     * Get notes. 
     */ 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes(): List<Note> { 
        return listOf( 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My first note", 
                        "This is a message for the 1st note." 
                ), 
                Note( 
                        UUID.randomUUID().toString(), 
                        "My second note", 
                        "This is a message for the 2nd note." 
                ) 
        ) 
    } 
 
    /** 
     * Insert note. 
     * It consumes JSON, that is: request body Note. 
     */ 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertNote( 
            @RequestBody note: Note 
    ): Note { 
        note.id = UUID.randomUUID().toString() 
        return note 
    } 
 
    /** 
     * Remove note by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteNote(@PathVariable(name = "id") id: String): Boolean { 
        println("Removing: $id") 
        return true 
    } 
 
    /** 
     * Update item. 
     * It consumes JSON, that is: request body Note. 
     * As result it returns updated Note. 
     */ 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateNote(@RequestBody note: Note): Note { 
        note.title += " [ updated ]" 
        note.message += " [ updated ]" 
        return note 
    } 
 
} 
  • TodoController, responsible for Todo entity-related things:
@RestController 
@RequestMapping("/todos") 
class TodoController { 
 
    /** 
     * Get todos. 
     */ 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getTodos(): List<Todo> { 
        return listOf( 
                Todo( 
                        UUID.randomUUID().toString(), 
                        "My first todo", 
                        "This is a message for the 1st todo.", 
                        System.currentTimeMillis() 
                ), 
                Todo( 
                        UUID.randomUUID().toString(), 
                        "My second todo", 
                        "This is a message for the 2nd todo.", 
                        System.currentTimeMillis() 
                ) 
        ) 
    } 
 
    /** 
     * Insert item. 
     * It consumes JSON, that is: request body Todo. 
     */ 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertTodo( 
            @RequestBody todo: Todo 
    ): Todo { 
        todo.id = UUID.randomUUID().toString() 
        return todo 
    } 
 
    /** 
     * Remove item by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteTodo(@PathVariable(name = "id") id: String): Boolean { 
        println("Removing: $id") 
        return true 
    } 
 
    /** 
     * Update item. 
     * It consumes JSON, that is: request body Todo. 
     * As result it returns updated Todo. 
     */ 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateTodo(@RequestBody todo: Todo): Todo { 
        todo.title += " [ updated ]" 
        todo.message += " [ updated ]" 
        todo.schedule = System.currentTimeMillis() 
        return todo 
    } 
 
} 

综上所述,我们刚刚优化了前面代码中的 API 定义,现在我们定义了以下 API 调用:

  • For Notes:
    • [ GET ] /notes, to obtain a list of notes
    • [ PUT ] /notes, to insert a new Note
    • [ DELETE ] /notes/{id}, to remove an existing Note with ID
    • [ POST ] /notes, to update an existing Note
  • For TODOs:
    • [ GET ] /todos, to obtain a list of TODOs
    • [ PUT ] /todos, to insert a new TODO
    • [ DELETE ] /todos/{id}, to remove an existing TODO with ID
    • [ POST ] /todos, to update an existing TODO

我们的 API 已起草并准备好运行。在下一节中,在我们进行一些配置并启动应用程序之后,我们将实际使用 Postman 尝试每个 API 调用

Running the application

接下来,我们需要运行我们的应用程序并触发 API 方法。将名为 application.properties 的文件添加到您的应用程序资源文件夹:

spring.application.name= Journaler API 
server.port= 9000 

这将代表我们本地开发环境的配置。感谢 Spring Boot,可以将配置外部化,这样我们就可以在不同的环境中使用相同的应用程序代码。

那么,我们在这三行中做了什么?

  • spring.application.name: We assigned the application name
  • server.port: We assigned the port on which our application will start and listen

还有什么可以配置的?首先,我们可以配置日志记录。让我们扩展我们的 application.properties

... 
logging.level.root=INFO 
logging.level.com.journaler.api=DEBUG 
logging.level.org.springframework.jdbc=ERROR 

支持以下日志级别:

  • TRACE
  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL
  • OFF

Spring Boot 对内部执行的所有日志记录使用 commons-logging (https://commons.apache.org/proper/commons-logging/)。 Spring log 实现没有关闭,所以可以扩展。

除了日志记录,经常配置的与环境相关的参数可能如下:

  • Defining custom values
  • Access to the data source
  • Defining keys (for example, apple.api.key= ...)
  • Overriding server-specific parameters (for example, spring.http.multipart.max-file-size= ...) and many more!

运行应用程序并等待它启动。几秒钟后,应用程序将启动并运行:

读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务

观察启动日志。你看到了什么?仔细查看每一行。您可以看到我们应用程序的整个生命周期:

  • Starting with ApiApplication.Kt on YOUR_HOST.local with PID... (started by YOUR_USERNAME...), the application start sequence is initialized
  • Running with Spring Boot v2.0.0.M4, Spring v5.0.0.RC4, the application will use Spring 5 with Spring Boot version 2
  • Tomcat initialized with the port: 9000 (HTTP), we will use Tomcat to serve the content running on the port we defined in our application.properties configuration

然后,再往前一点,我们定义的所有请求映射都会出现。先看一下第一个映射日志:

Mapped "{[/notes],methods=[GET],produces=[application/json]}" 
onto public java.util.List<com.journaler.api.data.Note> com.journaler.api.controller.NoteController.getNotes() 

如果您的某些映射不起作用,或者如果您想检查某些东西是否被映射,这将非常有用。好消息是我们看到了应用程序的所有映射!如果您正在观察您正在扩展的应用程序的启动日志,这将特别方便,但它是由其他人开发的。

最后,我们看到这些行:

Tomcat started on port(s): 9000 (http) 
Started ApiApplicationKt in 11.369 seconds 

现在我们可以触发 API 调用了!打开 Postman 试试看!

在下文中,我们将尝试 调用的 notes API。 让我们花几分钟来尝试 API 调用:

  • [ GET ] http://localhost:9000/notes:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ PUT ] http://localhost:9000/notes:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ DELETE ] http://localhost:9000/notes/f79464fc-21d6-4b3a-8871-6c5e853d7345:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ POST ] http://localhost:9000/notes:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务

在下文中,我们将进行TODO API 调用。

  • [ GET ] http://localhost:9000/todos:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ PUT ] http://localhost:9000/todos:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ POST ] http://localhost:9000/todos:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ DELETE ] http://localhost:9000/todos/38158621-3e63-44c2-b934-0dcc2c5b7dca:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务

Accessing Actuator endpoints

让我们看看 Actuator 开箱即用的一些端点:

  • /application/health: Application health
  • /application/info: Application information
  • /application/metrics: Metrics information for the application running
  • /application/trace: Trace information for the last few HTTP requests

这不是一个完整的列表。这是最常用的执行器端点的列表。由于我们访问的数据可能是敏感的,因此并非所有端点都默认启用。要启用所有这些调用,请打开您的 application.properties 并更新它:

endpoints.health.enabled=true 
endpoints.trace.enabled=true 
endpoints.info.enabled=true 
endpoints.metrics.enabled=true 

启动应用程序并一一尝试 Actuator 端点。你应该得到类似于这些的响应:

  • [ GET ] http://localhost:9000/application/status:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ GET ] http://localhost:9000/application/health:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ GET ] http://localhost:9000/application/metrics:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务
  • [ GET ] http://localhost:9000/application/trace:
读书笔记《building-applications-with-spring-5-and-kotlin》使用Kotlin构建您的第一个Spring RESTful服务

Adding an @Service component

我们将通过引入 Service 组件来继续我们的工作。那么,Spring 服务到底是什么? @Service 注释类是Service,最初由领域驱动设计定义。这是作为接口提供的操作,在模型中独立存在,没有封装状态

Spring 提供了以下常用的注解:

  • @Component
  • @Controller
  • @Repository
  • @Service

让我们解释一下它们之间的区别。

  • @Component is a generalization stereotype for any component managed by Spring Framework.

可以使用以下特化:@Repository@Service@Controller。每个都专门用于不同的用途:

  • @Repository annotation is a marker for any class that fulfills the role of a Data Access Object (DAO) of a repository. It also offers the automatic translation of exceptions. We will explain its usage soon.
  • @Controller annotation has already been covered in the previous examples.
  • @Service annotated class will have the purpose of connecting a controller with all operations to all operations related to data repository.
任何带有注释的类 @Controller, @Repository,或 @Service 实际上是用 @Component 也是。

下面的实现将演示 @Service 组件的使用。在项目的根包中创建一个service包和两个成员类:NoteServiceTodoService。我们将从 NoteService 开始:

package com.journaler.api.service 
 
import com.journaler.api.data.Note 
import org.springframework.stereotype.Service 
import java.util.* 
 
@Service("Note service") 
class NoteService { 
 
    fun getNotes(): List<Note> = listOf( 
            Note( 
                    UUID.randomUUID().toString(), 
                    "My first note", 
                    "This is a message for the 1st note." 
            ), 
            Note( 
                    UUID.randomUUID().toString(), 
                    "My second note", 
                    "This is a message for the 2nd note." 
            ) 
    ) 
 
    fun insertNote(note: Note): Note { 
        note.id = UUID.randomUUID().toString() 
        return note 
    } 
 
    fun deleteNote(id: String): Boolean = false 
 
    fun updateNote(note: Note): Boolean = true 
 
} 

该类使用 @Service 注释和分配的名称进行注释。该实现实际上将代表我们计划执行的 CRUD 操作。目前,我们将保留 dummy 实现。在下一章中,我们将把这些方法与 Repository 组件联系起来。

我们定义了 Service 组件。现在我们必须改变我们的控制器类:

@RestController 
@RequestMapping("/notes") 
class NoteController { 
 
    @Autowired 
    private lateinit var service: NoteService 
 
    /** 
     * Get notes. 
     */ 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getNotes() = service.getNotes() 
 
    /** 
     * Insert note. 
     * It consumes JSON, that is: request body Note. 
     */ 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertNote( 
            @RequestBody note: Note 
    ) = service.insertNote(note) 
 
    /** 
     * Remove note by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteNote( 
            @PathVariable(name = "id") id: String 
    ): Boolean = service.deleteNote(id) 
 
    /** 
     * Update item. 
     * It consumes JSON, that is: request body Note. 
     * As result it returns boolean, True == success. 
     */ 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateNote( 
            @RequestBody note: Note 
    ): Boolean = service.updateNote(note) 
 
} 

控制器执行相同的功能,只是将实现移至 Service 组件。最重要的新事物是 @Autowired 注释。使用 @Autowired,我们告诉 Spring Framework 一个特定的字段必须被依赖注入。该字段必须定义为后期初始化var

让我们对 TODO 做同样的事情。确保 TodoService 看起来像这样:

package com.journaler.api.service 
 
import com.journaler.api.data.Todo 
import org.springframework.stereotype.Service 
import java.util.* 
 
 
@Service("Todo service") 
class TodoService { 
 
    fun getTodos(): List<Todo> = listOf( 
            Todo( 
                    UUID.randomUUID().toString(), 
                    "My first todo", 
                    "This is a message for the 1st todo.", 
                    System.currentTimeMillis() 
            ), 
            Todo( 
                    UUID.randomUUID().toString(), 
                    "My second todo", 
                    "This is a message for the 2nd todo.", 
                    System.currentTimeMillis() 
            ) 
    ) 
 
    fun insertTodo(todo: Todo): Todo { 
        todo.id = UUID.randomUUID().toString() 
        return todo 
    } 
 
    fun deleteTodo(id: String): Boolean = false 
 
    fun updateTodo(todo: Todo): Boolean = true 
 
} 
 
TodoController now looks like this and it is using injected TodoService: 
@RestController 
@RequestMapping("/todos") 
class TodoController { 
 
    @Autowired 
    private lateinit var service: TodoService 
 
    /** 
     * Get todos. 
     */ 
    @GetMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun getTodos(): List<Todo> = service.getTodos() 
 
    /** 
     * Insert item. 
     * It consumes JSON, that is: request body Todo. 
     */ 
    @PutMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun insertTodo( 
            @RequestBody todo: Todo 
    ): Todo = service.insertTodo(todo) 
 
    /** 
     * Remove item by Id. 
     * We introduced path variable for Id to pass. 
     */ 
    @DeleteMapping( 
            value = "/{id}", 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun deleteTodo( 
            @PathVariable(name = "id") id: String 
    ): Boolean = service.deleteTodo(id) 
 
    /** 
     * Update item. 
     * It consumes JSON, that is: request body Todo. 
     * As result it returns boolean. True == success. 
     */ 
    @PostMapping( 
            produces = arrayOf(MediaType.APPLICATION_JSON_VALUE), 
            consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE) 
    ) 
    fun updateTodo(@RequestBody todo: Todo): Boolean = service.updateTodo(todo) 
 
} 

看,简单!提取此代码并将其委托给 Service 组件非常容易。

Summary

在本章中,我们定义了我们的第一个 API 调用并起草了其他的。我们涵盖了指定 RESTful 应用程序所需的所有内容。我们还通过引入 Service 将请求的相关内容与域相关的实现分开。在下一章中,我们将用真实的数据访问替换 dummy datadummy implementation 并介绍存储库。