文章目录
Spring 是最著名的 Web 框架之一,许多开发人员选择 Spring 是因为它功能强大并且支持依赖注入和面向切面编程。而且 Spring Boot 还支持 Kotlin 和 Gradle,这意味着我们不必使用 Java 和编写 XML 文件。
在这篇文章中,将建立一个水果市场。在整个过程中,我们将:
- 初始化一个 Spring Boot 项目
- 运行 Web 应用程序
- 编写控制器、模型、配置、异常和中间件
- 使用数据库和 MyBatis
- 编写和运行测试
初始化项目
假设我们在一家初创公司找到了一份新工作,担任 Web 开发人员。老板要求我们建立一个水果市场。他希望移动优先,因此需要构建 REST API 端点来开发移动应用程序用于交易水果。
要创建新的 Spring Boot 项目,我们只需要访问 Spring Initializr,这是一个用于初始化 Spring Boot 项目的网站:
在 Project 中选择 Gradle Project
,然后 Language 选择 Kotlin
,Spring Boot 用默认版本即可。
在 Project Metadata 部分中,在 Group 中设置 com.example
,然后在 Artifact 和 Name 中填写 fruitmarketplace
,在 Description 中输入 The Fruits Marketplace
。
Package name 使用 com.example.fruitmarketplace
,保留 Packaging 的默认值,Java 选 11
。
然后点击 Add the Dependencies :
我们可以在对话框中搜索任何 Spring 依赖项。搜索 Spring Web,因为它带有一个 Web 服务器,我们将在其中部署 Web 应用程序。
最终如下所示:
单击页面底部的 Generate ,浏览器将下载项目的zip文件,我们只需要将这个zip 文件拷贝到某个目录中并解压即可。
开发 Java Web项目,一般需要 IntelliJ IDEA 或者 Eclipse。如果你没有 IntelliJ IDEA,暂时使用 Android Studio 也可以。
运行项目
使用 IntelliJ IDEA 打开这个解压后的项目源码,Gradle 将同步这个项目。
如果你开发过Android项目,一定会对 Gradle 非常熟悉。
在 Spring Boot 项目中,Web 应用程序代码放在 src 文件夹中,我们将在 main/kotlin 文件夹中编写 Kotlin 代码。
在项目的根目录中有个 build.gradle.kts
文件。这是 Spring Boot Gradle 构建文件,里面包含了 Web 应用程序的依赖项和项目的配置。
打开 FruitmarketplaceApplication.kt
,通过单击主函数左侧的绿色播放按钮运行 Web 应用程序。将出现一个弹出对话框。点击运行‘FruitmarketplaceApplic…’:
我们将在IntelliJ IDEA 的运行工具窗口的输出中看到:
Spring 项目带有一个嵌入式 Tomcat Web 服务,该服务运行在 8080 端口上。在 Web 浏览器中打开 http://localhost:8080 ,将看到:
由于 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"
}
下面是代码分解:
首先,使用
@RestController
对类进行注解。 Spring Boot 将对其进行扫描并将其识别为 Controller 类。 Spring Boot 中的自动配置意味着无需手动编写 XML 文件来注册 Controller。要将 GET 请求映射到方法,需要使用
@GetMapping
将/homepage
路径作为参数传递给方法。在此示例中,该方法将返回一个字符串。
Controller 是 SpringBoot 里最基本的组件,其作用是把用户提交来的请求通过对URL的匹配,分配给不同的接收器,再进行处理,然后向用户返回结果。
单击重新运行项目:
将出现一个弹出对话框,询问您是否确定要重建项目。单击停止并重新运行:
打开 http://localhost:8080/homepage 。这次你会在网页上看到一个文本:
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
:
我们没有显式安装它,但在安装 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
下面分解代码:
首先,使用
@PostMapping
注释该方法以指示此方法处理 POST 请求。这里使用了与前一个 GET 请求相同的路径 “”。
这不是问题,因为一条路径可以服务两个不同的请求。 然后添加@ResponseStatus
和HttpStatus.CREATED
参数,因为通常在创建新 Fruit 之类的资源后,会发出201 CREATED
响应状态。如果省略此注释,将获得默认的200 OK
响应状态。@RequestBody
的方法参数是我们将发送到此路径的 JSON 对象。然后会得到现有 Fruit 中最大的 id。
计算新 Fruit 的 id。
然后,使用请求正文中计算的
id
、name
和floor_price
创建一个新的 Fruit。最后,将新的 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
下面是代码分解:
这一次,在
@GetMapping
注解中,大括号之间有一个id
参数。因为还有一个方法参数
id: Int
使用@PathVariable
注释进行注释,只要服务器收到/1
、/2
或任何数字的 GET 请求,此方法就会收到该参数。请注意,返回类型是可选的Fruit
。尝试找到具有所需 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
}
}
下面是代码分解:
当我们使用
@ControllerAdvice
注释类时,Spring Boot 会扫描并将此类注册为 Controller 的ControllerAdvice
。ControllerAdvice
类允许我们将异常处理程序应用于应用程序中的多个或所有控制器。使用
@ExceptionHandler
注释该方法,它接受我们创建的异常类,让 Spring Boot 知道该方法可以处理该异常。返回一个
ResponseEntity
。ResponseEntity
的第一个参数是我们发送给客户端的结果,可以是简单的文本字符串或 JSON 字符串,第二个参数是状态类型。
为什么称为 “Controller Advice” ?
“Advice” 一词来自面向切面的编程(AOP),它允许我们围绕现有方法注入横切代码(称为 “Advice”)。Controller Advice 允许我们拦截和修改控制器方法的返回值,在我们的例子中是为了处理异常。
如果我们想选择性地将 ControllerAdvice
的范围应用或限制到特定控制器或包,我们可以使用注释提供的属性:
@ControllerAdvice("com.guiying712.controller")
:我们可以在注解的value
或basePackages
参数中传递一个包名或包名列表。这样,ControllerAdvice
将只处理此包控制器的异常。@ControllerAdvice(annotations = Advised.class)
:只有标有@Advised
注解的控制器才会被ControllerAdvice
处理。
在我们的异常处理函数中除了异常参数,还可以将 HttpServletRequest
、WebRequest
或 HttpSession
类型作为参数。
同样,异常处理函数支持各种返回类型,例如 ResponseEntity
、String
甚至 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
方法,它接受 Request、Response 和过滤器链,我们可以从请求对象中获取 UTM 参数。
在这个用例中,没有更改 Response,但如果有必要,是可以更改的。
重新运行应用程序并执行:
http://localhost:8080/0?utm_source=京东生鲜
将会得到下面的响应:
{
"id": 0,
"name": "Apple",
"floor_price": 100.0
}
检查运行工具窗口上的日志输出:
添加配置
我们的老板有了另一个想法,现在她想构建一个中性化的水果市场应用程序,以便将 Web 应用程序出售给任何想要建立水果市场的人。
因为老板想将 Web 应用程序出售给许多公司,因此我们不能在代码中嵌入公司代码,需要更灵活地显示公司名称,这就是配置的来源。
我们一般在 main
文件夹的 kotlin
中编写代码,但是在 main
中还有另一个子文件夹 resources
:
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
下面分解代码:
url
代表数据库的 JDBC URL。具体规则为:
host=jdbc:mysql://localhost
port=3306
databaseName=fruitmarketplace
username
代表数据库的登录用户名。password
代表数据库的登录密码。driver-class-name
JDBC 驱动程序的完全限定名称,默认根据 URL 自动检测。
JDBC 指 Java 数据库连接,是一种标准Java应用编程接口( JAVA API),用来连接 Java 编程语言和广泛的数据库。
配置完数据库后,我们就可以使用 Mybatis 操作数据库。MyBatis 是一款优秀的持久层框架,本质上是JDBC的一次封装。MyBatis 提供了两种基本用法: ① XML方式,② 注解方式 。
MyBatis-XML实现SQL的映射
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
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>
最终如下:
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")
}
}
}
最终如下:
配置完后,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 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。
在 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中的 select
、insert
、update
以及 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
。
如果你对有兴趣可以查看下面这篇文章:
本文中采用了 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
}
}
编写测试用例
附录
Github源码地址:https://github.com/guiying712/fruitmarketpalce
SpringBoot官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-controller
Mybatis中文官网:https://mybatis.org/mybatis-3/zh/index.html