Spring Boot Using Kotlin

发布于:2023-01-01 ⋅ 阅读:(246) ⋅ 点赞:(0)


Spring 是最著名的 Web 框架之一,许多开发人员选择 Spring 是因为它功能强大并且支持依赖注入和面向切面编程。而且 Spring Boot 还支持 Kotlin 和 Gradle,这意味着我们不必使用 Java 和编写 XML 文件。

在这篇文章中,将建立一个水果市场。在整个过程中,我们将:

  • 初始化一个 Spring Boot 项目
  • 运行 Web 应用程序
  • 编写控制器、模型、配置、异常和中间件
  • 使用数据库和 MyBatis
  • 编写和运行测试

初始化项目

假设我们在一家初创公司找到了一份新工作,担任 Web 开发人员。老板要求我们建立一个水果市场。他希望移动优先,因此需要构建 REST API 端点来开发移动应用程序用于交易水果。

什么是 REST API

为什么要使用RESTful架构

要创建新的 Spring Boot 项目,我们只需要访问 Spring Initializr,这是一个用于初始化 Spring Boot 项目的网站:

1

Project 中选择 Gradle Project,然后 Language 选择 KotlinSpring Boot 用默认版本即可。

Project Metadata 部分中,在 Group 中设置 com.example,然后在 ArtifactName 中填写 fruitmarketplace,在 Description 中输入 The Fruits Marketplace

Package name 使用 com.example.fruitmarketplace ,保留 Packaging 的默认值,Java11

然后点击 Add the Dependencies :

2

我们可以在对话框中搜索任何 Spring 依赖项。搜索 Spring Web,因为它带有一个 Web 服务器,我们将在其中部署 Web 应用程序。

最终如下所示:

3

单击页面底部的 Generate ,浏览器将下载项目的zip文件,我们只需要将这个zip 文件拷贝到某个目录中并解压即可。

开发 Java Web项目,一般需要 IntelliJ IDEA 或者 Eclipse。如果你没有 IntelliJ IDEA,暂时使用 Android Studio 也可以。

运行项目

使用 IntelliJ IDEA 打开这个解压后的项目源码,Gradle 将同步这个项目。

如果你开发过Android项目,一定会对 Gradle 非常熟悉。

在 Spring Boot 项目中,Web 应用程序代码放在 src 文件夹中,我们将在 main/kotlin 文件夹中编写 Kotlin 代码。

1

在项目的根目录中有个 build.gradle.kts 文件。这是 Spring Boot Gradle 构建文件,里面包含了 Web 应用程序的依赖项和项目的配置。

55

打开 FruitmarketplaceApplication.kt,通过单击主函数左侧的绿色播放按钮运行 Web 应用程序。将出现一个弹出对话框。点击运行‘FruitmarketplaceApplic…’:

6

我们将在IntelliJ IDEA 的运行工具窗口的输出中看到:

7

Spring 项目带有一个嵌入式 Tomcat Web 服务,该服务运行在 8080 端口上。在 Web 浏览器中打开 http://localhost:8080 ,将看到:

11

由于 Spring Boot 没有默认索引页面,因此只能返回一个 404 NOT FOUND 页面。

接下来我们将正式开发 Web 应用程序。

创建控制器

首先,我们将构建一个控制器,特别是一个 REST 控制器。在 com.example.fruitmarketpalce 路径下,通过右键单击创建一个名为 controller 的包。

然后,在 controller 目录下创建一个名为 MarketplaceController.kt 的新文件:

将文件内容替换为:

package com.example.fruitmarketpalce.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController // 1
class MarketplaceController {

  @GetMapping("/homepage") // 2
  fun getHomePage() = "Fruits Marketplace"
}

下面是代码分解:

  1. 首先,使用 @RestController 对类进行注解。 Spring Boot 将对其进行扫描并将其识别为 Controller 类。 Spring Boot 中的自动配置意味着无需手动编写 XML 文件来注册 Controller。

  2. 要将 GET 请求映射到方法,需要使用 @GetMapping/homepage 路径作为参数传递给方法。在此示例中,该方法将返回一个字符串。

Controller 是 SpringBoot 里最基本的组件,其作用是把用户提交来的请求通过对URL的匹配,分配给不同的接收器,再进行处理,然后向用户返回结果。

单击重新运行项目:

88

将出现一个弹出对话框,询问您是否确定要重建项目。单击停止并重新运行:

000

打开 http://localhost:8080/homepage 。这次你会在网页上看到一个文本:

000

REST API 不仅需要简单的字符串,还需要以 JSON 格式返回数据。但在此之前,我们将创建一个模型来表示 Fruit

创建模型

com.example.fruitmarketpalce 路径下创建一个新包并将其命名为 model。然后在 model 下新建一个类,命名为 Fruit。将文件内容替换为:

package com.example.fruitmarketpalce.model

data class Fruit(val id: Int, var name: String, var floor_price: Double)

支持 JSON GET 请求

回到 MarketplaceController.kt 代码。创建 Fruit 的样本数据。然后,在类中添加以下代码:

private var Fruits = mutableListOf(
            Fruit(1, "Apple", 100.0),
            Fruit(2, "Banana", 36.9),
            Fruit(3, "Pear", 0.6),
            Fruit(4, "Peach", 1.1),
            Fruit(5, "Strawberry", 2.5),
)

然后导入:

import com.example.fruitmarketpalce.model.Fruit

我们只需要返回对象,不需要其他特殊操作就可以返回 JSON 格式的数据。在类中添加以下方法:

@GetMapping("")
fun getFruits() = Fruits

然后导入:

import org.springframework.web.bind.annotation.GetMapping

Spring Boot 将在后台将我们的数据类实例列表序列化为 JSON 字符串。查看 build.gradle.kts,你会注意到一个 JSON 库,jackson-module-kotlin

454

我们没有显式安装它,但在安装 Spring Web 组件时已包含它。

重新运行项目并打开 http://localhost:8080

Web 应用程序将以 JSON 格式显示 Fruit。

可以在Chrome 和 Edge 浏览器中安装 “JSONView” 扩展,方便在浏览器中查看JSON 文件。

为了将新的 Fruit 添加到可变列表,我们需要支持 JSON POST 请求。

支持 JSON POST 请求

要支持 POST 请求,需要 @PostMapping 注解。在 controller 类中创建一个方法:

@PostMapping("") // 1
@ResponseStatus(HttpStatus.CREATED) // 2
fun postFruit(@RequestBody fruit: Fruit): Fruit { // 3
  val maxId = Fruits.map { it.id }.maxOrNull() ?: 0 // 4
  val nextId = maxId + 1 // 5
  val newFruit = Fruit(id = nextId, name = fruit.name, floor_price = fruit.floor_price) // 6
  Fruits.add(newFruit) // 7
  return newFruit
}

添加以下导入:

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.ResponseStatus

下面分解代码:

  1. 首先,使用 @PostMapping 注释该方法以指示此方法处理 POST 请求。这里使用了与前一个 GET 请求相同的路径 “”。
    这不是问题,因为一条路径可以服务两个不同的请求。 然后添加 @ResponseStatusHttpStatus.CREATED 参数,因为通常在创建新 Fruit 之类的资源后,会发出 201 CREATED 响应状态。如果省略此注释,将获得默认的 200 OK 响应状态。

  2. @RequestBody 的方法参数是我们将发送到此路径的 JSON 对象。

  3. 然后会得到现有 Fruit 中最大的 id。

  4. 计算新 Fruit 的 id。

  5. 然后,使用请求正文中计算的 idnamefloor_price 创建一个新的 Fruit。

  6. 最后,将新的 Fruit 添加到列表中并将其作为响应返回。

重新运行项目并创建一个执行此 POST 请求的新 Fruit:

通过向 http://localhost:8080 发送 GET 请求来检查 Fruit 的完整列表:

映射路径变量

现在我们想获取一个水果的信息。只需在 Controller 类中来创建新的 GET 路径:

@GetMapping("/{id}") // 1
fun getFruitById(@PathVariable id: Int) : Fruit? { // 2
  return Fruits.firstOrNull { it.id == id } // 3
}

添加以下导入:

import org.springframework.web.bind.annotation.PathVariable

下面是代码分解:

  1. 这一次,在 @GetMapping 注解中,大括号之间有一个 id 参数。

  2. 因为还有一个方法参数 id: Int 使用 @PathVariable 注释进行注释,只要服务器收到 /1/2 或任何数字的 GET 请求,此方法就会收到该参数。请注意,返回类型是可选的 Fruit

  3. 尝试找到具有所需 id 的 Fruit 并将其返回。如果它不存在,将返回 null

重新运行项目并执行以下 GET 请求:

http://localhost:8080/0

你会得到:

{
	"id": 0,
	"name": "Apple",
	"floor_price": 100.0
}

现在执行:

http://localhost:8080/100

可以看到我们没有收到任何响应,因为 id=100 的 Fruit 不存在。即使 Fruit 不存在,服务也会响应 200 OK

显然 Fruit 不存在的更好方法是返回 404 NOT FOUND 结果。我们将在下一节中执行此操作。

处理异常

现在,我们将为丢失的 Fruit 创建一个自定义异常。

异常的基本概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。

com.example.fruitmarketpalce 下,创建一个名为 exception 的包。然后在包中,创建 FruitNotFoundException 类并将文件的内容替换为:

package com.example.fruitmarketpalce.exception

class FruitNotFoundException : Exception()

在同一个包中,创建 ControllerAdvice 类。将其命名为 FruitErrorHandler 并将文件内容替换为:

package com.example.fruitmarketpalce.exception

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import javax.servlet.http.HttpServletRequest

@ControllerAdvice // 1
class FruitErrorHandler  {
  @ExceptionHandler(FruitNotFoundException::class) // 2
  fun handleFruitNotFoundException(servletRequest: HttpServletRequest, exception: Exception): ResponseEntity<String> {
    return ResponseEntity("Fruit not found", HttpStatus.NOT_FOUND) // 3
  }
}

下面是代码分解:

  1. 当我们使用 @ControllerAdvice 注释类时,Spring Boot 会扫描并将此类注册为 Controller 的 ControllerAdviceControllerAdvice 类允许我们将异常处理程序应用于应用程序中的多个或所有控制器。

  2. 使用 @ExceptionHandler 注释该方法,它接受我们创建的异常类,让 Spring Boot 知道该方法可以处理该异常。

  3. 返回一个 ResponseEntityResponseEntity 的第一个参数是我们发送给客户端的结果,可以是简单的文本字符串或 JSON 字符串,第二个参数是状态类型。

为什么称为 “Controller Advice” ?
“Advice” 一词来自面向切面的编程(AOP),它允许我们围绕现有方法注入横切代码(称为 “Advice”)。Controller Advice 允许我们拦截和修改控制器方法的返回值,在我们的例子中是为了处理异常。

如果我们想选择性地将 ControllerAdvice 的范围应用或限制到特定控制器或包,我们可以使用注释提供的属性:

  • @ControllerAdvice("com.guiying712.controller"):我们可以在注解的 valuebasePackages 参数中传递一个包名或包名列表。这样,ControllerAdvice 将只处理此包控制器的异常。
  • @ControllerAdvice(annotations = Advised.class):只有标有 @Advised 注解的控制器才会被 ControllerAdvice 处理。

在我们的异常处理函数中除了异常参数,还可以将 HttpServletRequestWebRequestHttpSession 类型作为参数。

同样,异常处理函数支持各种返回类型,例如 ResponseEntityString 甚至 void

查找更多 @ExceptionHandler 的输入和返回类型可以查看 ExceptionHandler 文档

我们的工作还没有完成,返回 Controller 并将 getFruitById 替换为:

import com.example.fruitmarketpalce.exception.FruitNotFoundException
...

@GetMapping("/{id}")
fun getFruitById(@PathVariable id: Int): Fruit {
  val fruit = Fruits.firstOrNull { it.id == id }
  return fruit ?: throw FruitNotFoundException()
}

现在,不是返回 null,而是抛出异常。因此,如果我们尝试检索一个不存在的 Fruit,将得到 404 NOT FOUND 结果。

重新运行项目并转到 http://localhost:8080/100

使用中间件

我们收到老板的需求,她想记录 UTM 营销标志,这样就可以奖励那些推广水果市场的人。

我们可以修改所有 Controller 的方法,但这样太麻烦,这种情况可以使用中间件。

com.example.fruitmarketpalce 下,创建一个名为 middleware 的新包。在 middleware 内部创建一个名为 RequestLoggingFilter 的类,并将文件内容替换为:

package com.example.fruitmarketpalce.middleware

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse

@Component
class RequestLoggingFilter : Filter {
  val loggerFactory = LoggerFactory.getLogger("Fruit Logger")

  override fun doFilter(
    servletRequest: ServletRequest,
    servletResponse: ServletResponse,
    filterChain: FilterChain
  ) {
    val utmSource = servletRequest.getParameter("utm_source")
    loggerFactory.info("Logging UTM source: $utmSource")
    filterChain.doFilter(servletRequest, servletResponse)
  }
}

要在中间件中自动执行代码,我们需要创建一个 Filter 类并使用 @Component 对其进行注释,以便 Spring Boot 可以在中间件中注册我们的类。

@Component 是一个元注解,意思是可以注解其他类注解,如@Controller @Service @Repository @Aspect。官方的原话是:带此注解的类看为组件,当使用基于注解的配置和类路径扫描的时候,这些类就会被实例化。其他类级别的注解也可以被认定为是一种特殊类型的组件,比如@Repository @Aspect。所以,@Component可以注解其他类注解。

要在中间件中添加代码,需要重写 doFilter 方法,它接受 RequestResponse过滤器链,我们可以从请求对象中获取 UTM 参数。

在这个用例中,没有更改 Response,但如果有必要,是可以更改的。

重新运行应用程序并执行:

http://localhost:8080/0?utm_source=京东生鲜

将会得到下面的响应:

{
	"id": 0,
	"name": "Apple",
	"floor_price": 100.0
}

检查运行工具窗口上的日志输出:

添加配置

我们的老板有了另一个想法,现在她想构建一个中性化的水果市场应用程序,以便将 Web 应用程序出售给任何想要建立水果市场的人。

因为老板想将 Web 应用程序出售给许多公司,因此我们不能在代码中嵌入公司代码,需要更灵活地显示公司名称,这就是配置的来源。

我们一般在 main 文件夹的 kotlin 中编写代码,但是在 main 中还有另一个子文件夹 resources

97

resources包含静态文件,如图片、包含 HTML 文件的 template 以及 application.properties,这个文件是放置配置的地方。

打开 application.properties 并添加:

company_name=京东水果

这是一个键值对,变量在左边,值在右边,由 = 分隔。

要在 Controller 中使用此配置,需使用 @Value 注释并将其绑定到类中的状态变量。在 Controller 类中添加状态变量:

@Value("\${company_name}")
private lateinit var name: String

添加以下导入:

import org.springframework.beans.factory.annotation.Value

@Value 接受在字符串内插值的 company_name。 Spring Boot 会将结果值绑定到名称。

更改 getHomePage 以使用名称:

@GetMapping("/homepage")
fun getHomePage() = "$name: Fruits Marketplace"

重新运行项目并执行以下命令:

http://localhost:8080/homepage

将得到输出:

京东水果: Fruits Marketplace

配置放在同一个地方,除了公司名称之外,还可以包括数据库配置和网络配置等信息。

请求映射

现在老板有了另一个灵感:现在她想要一个也显示 HTML 页面的传统 Web 应用程序,而不是移动优先。

因此,我们必须在 API 路径前添加 /fruits,以便 http://localhost:8080/2 变为 http://localhost:8080/fruits/2 。我们将为传统 Web 应用程序保留根 URL。

我们可以将所有方法的字符串替换,是不是很容易。但是有更好的方法,使用 @RequestMapping 注释 Controller 。然后在 Controller 类声明上方添加以下代码:

@RequestMapping("/fruits")

然后导入:

import org.springframework.web.bind.annotation.RequestMapping

现在,Spring Boot 会在所有 API 路径前添加 /fruits。重新运行项目并执行:

http://localhost:8080/fruits/0

下面是响应结果:

{
	"id": 0,
	"name": "Apple",
	"floor_price": 100.0
}

添加数据库

在上面我们使用 Fruit 的样本数据来存储和查询数据,但是这些数据不是持久化的,Web 应用程序重启后,这些数据就会消失。为了持久化 Fruit 数据,我们将引入数据库。下面将以 mysql 为例,如果你的本机上还没有安装 mysql 服务,请先安装和配置 mysql 。

打开 build.gradle.kts ,并在 dependencies 添加如下依赖:

dependencies {
	...
	implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2")
	runtimeOnly("mysql:mysql-connector-java")
	...
}

打开 application.properties 配置数据库信息:

spring.datasource.url=jdbc:mysql://localhost:3306/fruitmarketplace?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true  // 1
spring.datasource.username=sansi    // 2
spring.datasource.password=xxxxxxx    // 3
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

下面分解代码:

  1. url 代表数据库的 JDBC URL。具体规则为:
host=jdbc:mysql://localhost
port=3306
databaseName=fruitmarketplace
  1. username 代表数据库的登录用户名。

  2. password 代表数据库的登录密码。

  3. driver-class-name JDBC 驱动程序的完全限定名称,默认根据 URL 自动检测。

JDBC 指 Java 数据库连接,是一种标准Java应用编程接口( JAVA API),用来连接 Java 编程语言和广泛的数据库。

配置完数据库后,我们就可以使用 Mybatis 操作数据库。MyBatis 是一款优秀的持久层框架,本质上是JDBC的一次封装。MyBatis 提供了两种基本用法: ① XML方式,② 注解方式 。

什么是 MyBatis?

MyBatis-XML实现SQL的映射

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。

mybatis XML 映射器

首先在 com.example.fruitmarketpalce 下,创建一个名为 mapper 的新包。在 mapper 内部,创建一个名为 MarketplaceXmlMapper 的类,并将文件内容替换为:

@Mapper
@Component
interface MarketplaceXmlMapper {

    fun getAllFruit(): List<Fruit>

    fun getFruitById(id: Int): Fruit?

    fun insert(fruit: Fruit): Int

    fun update(fruit: Fruit): Int

    fun delete(id: Int): Int
}

Mapper 创建好之后,还要配置 mapper 扫描。

有两种方式,一种是直接在 Mapper 接口上面添加 @Mapper 注解,这种方式有一个弊端就是所有的Mapper都要手动添加,要是落下一个就会报错。

还有一个一劳永逸的办法就是直接在启动类上添加 @MapperScan,如下:

@SpringBootApplication
@MapperScan("com.example.fruitmarketpalce")
class FruitMarketApplication

@MapperScan 注解主要是扫描某个包路径下的 Mybatis Mapper,将 Mapper 接口类交给 Spring 进行管理。扫描包路径可以是一个或者多个,也可以在路径中可以使用 * 作为通配符对包名进行匹配。

在 Controller 类中添加 mapper 变量:

@Autowired
private lateinit var marketplaceMapper: MarketplaceXmlMapper

导入以下文件`:

import org.springframework.beans.factory.annotation.Autowired
import com.example.fruitmarketpalce.mapper.MarketplaceXmlMapper

@Autowired 注解是通过匹配数据类型自动装配 Bean。这里使用 @Autowired 来自动装配一个 MarketplaceXmlMapper bean。

将 Controller 中 getFruitById 方法更改为以下代码:

@GetMapping("/{id}")
fun getFruitById(@PathVariable id: Int): Fruit {
    val fruit = marketplaceMapper.getFruitById(id)

    return fruit ?: throw FruitNotFoundException()
}

然后在 resources 目录下,创建一个名为 mapper 的新包。在 mapper 内部,创建一个名为 MarketplaceXmlMapper 的 xml 文件作为 SQL 映射文件,并将文件内容替换为:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.fruitmarketpalce.mapper.MarketplaceXmlMapper">
    <select id="getAllFruit" resultType="com.example.fruitmarketpalce.model.Fruit">
        select * from fruit;
    </select>
    <select id="getFruitById" resultType="com.example.fruitmarketpalce.model.Fruit">
        select * from fruit where id=#{id};
    </select>
    <insert id="insert" parameterType="com.example.fruitmarketpalce.model.Fruit">
        insert into fruit (id,name,floor_price) values (#{id},#{name},#{floor_price});
    </insert>
    <update id="update" parameterType="com.example.fruitmarketpalce.model.Fruit">
        update fruit set name=#{name},floor_price=#{floor_price} where id=#{id}
    </update>
    <delete id="delete">
        delete from fruit where id=#{id}
    </delete>
</mapper>

最终如下:

76776

Mapper xml 文件放在 resources 目录下,不能自动被 mybatis 扫描到,需要添加额外配置告诉 mybatis 去哪里扫描 mapper。打开 application.properties 添加如下配置:

mybatis.mapper-locations=classpath:mapper/*.xml

Mybatis 的 SQL 映射文件除了可以放在 resources 目录下,还可以直接放在 Mapper 接口所在的包下面,放在这里的 Mapper.xml 会自动被扫描到。

但是源码目录下的 xml 资源在项目打包时会被忽略掉,如果 SQL 映射文件放在包下,需要在 build.gradle.kts 中再添加如下配置,避免打包时源码目录下的 xml 文件被自动忽略掉:

sourceSets {
	main {
		resources {
			srcDir("src/main/kotlin")
		}
	}
}

最终如下:

11

配置完后,mapper 就可以正常使用了。重新运行项目并执行:

http://localhost:8080/fruits/0

下面是响应结果:

{
	"id": 0,
	"name": "Apple",
	"floor_price": 100.0
}

MyBatis-注解实现SQL的映射

设计初期的 MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,映射语句也是定义在 XML 中的。而在 MyBatis 3 中提供了其它的配置方式。MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。

mybatis 注解官方文档

mapper 内部,创建一个名为 MarketplaceAnnotationMapper 的类,并将文件内容替换为:

@Mapper
@Component
interface MarketplaceAnnotationMapper {

    @Select("SELECT * FROM fruit")
    fun getAllFruit(): List<Fruit>

    @Select("SELECT * FROM fruit where id = #{id}")
    fun getFruitById(id: Int): Fruit?

    @Insert("INSERT INTO fruit (id,name,floor_price) VALUES (#{id},#{name},#{floor_price})")
    fun insert(fruit: Fruit): Int

    @Update("UPDATE fruit SET id = #{id}, name = #{name}, floor_price = #{floor_price}")
    fun update(fruit: Fruit): Int

    @Delete("DELETE FROM fruit WHERE id = #{id}")
    fun delete(id: Int): Int
}

@Select@Insert@Update 以及 @Delete 四个注解分别对应XML中的 selectinsertupdate 以及 delete标签。

在 Controller 类中添加 mapper 变量:

@Autowired
private lateinit var marketplaceAnnotationMapper: MarketplaceAnnotationMapper

导入以下文件:

import org.springframework.beans.factory.annotation.Autowired
import com.example.fruitmarketpalce.mapper.MarketplaceAnnotationMapper

将 Controller 中 getFruitById 方法更改为以下代码:

@GetMapping("/{id}")
fun getFruitById(@PathVariable id: Int): Fruit {
    val fruit = marketplaceAnnotationMapper.getFruitById(id)

    return fruit ?: throw FruitNotFoundException()
}

重新运行项目并执行:

http://localhost:8080/fruits/0

下面是响应结果:

{
	"id": 0,
	"name": "Apple",
	"floor_price": 100.0
}

REST API HTTP Status Code

REST API 到底是遵循 HTTP 的规定,根据不同的情况返回相应的状态码呢,还是不管对错,一律返回200。你是否还记得:甜豆腐脑和咸豆腐脑之争。目前基本上分成了两派:

200派:不管对错,一律返回200,在返回的JSON中再具体指明错误的原因。
正规派:坚持使用规范的HTTP状态码。如果是没有登录,就返回401,如果是没权限就返回403

如果你对有兴趣可以查看下面这篇文章:

Rest API的状态码和错误处理最佳实践

本文中采用了 200派 的做法。在 model 目录下新建一个类,命名为 RestResult,将文件内容替换为:

data class RestResult<T>(
        val code: String,
        val msg: String,
        val data: T?
) {
    companion object {

        @JvmStatic
        fun <T> success(data: T): RestResult<T> {
            return RestResult("0", "success", data)
        }

        @JvmStatic
        fun <T> error(code: String, msg: String = "error"): RestResult<T> {
            return RestResult(code, msg, null)
        }
    }
}

返回 Controller 并将 getFruitById 替换为:

@GetMapping("/{id}")
fun getFruitById(@PathVariable id: Int): RestResult<Fruit> {
    val fruit = marketplaceMapper.getFruitById(id)
    return if (fruit != null) RestResult.success(fruit) else throw FruitNotFoundException()
}

现在,不是返回 Fruit,而是 RestResult

重新运行项目并转到 http://localhost:8080/fruits/0 ,结果如下:

{
	"code": "0",
	"msg": "success",
	-"data": {
		"id": 0,
		"name": "Apple",
		"floor_price": 100
	}
}

编写测试用例

附录

  1. Github源码地址:https://github.com/guiying712/fruitmarketpalce

  2. SpringBoot官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-controller

  3. Mybatis中文官网:https://mybatis.org/mybatis-3/zh/index.html


网站公告

今日签到

点亮在社区的每一天
去签到