Spring Boot & Spring Data JPA – Complete Course(2) - NoTe0x004

Baobao0824

原链接
https://www.youtube.com/watch?v=5rNk7m_zlAg

写在前面

这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。

本篇是这个SpringBoot视频教程的第二部分,主要讲的就是Spring REST API,视频的时长大概在1小时左右,应该会比上一期稍微短一点?

OverView

REST是Representational State Transfer(表示层状态转移)的缩写,它是一种软件架构,定义了用于创建Web服务的约束集合的样式,这些网络服务通常被称为RESTful API,它是一种设计思路,由Roy Fielding于2000年在他的博士论文中定义。其核心思想是将网络资源视为对象,它们可以通过标准的HTTP方法来访问,例如GET、POST、PUT、DELETE等。REST同时规定了下面的原则:

  • 客户端-服务器架构,这一原则确立了二者之间相互独立,他们可以相互互动,但是双方都可以独立开发和更新。
  • 无状态。每一个HTTP请求都应该包括二者通信所需的所有信息,服务器不应该在请求期间存储任何数据(现在存也是存在Redis等外挂数据库,并不存在Tomcat的内存中)
  • 可缓存。服务器必须明文指出返回的数据是否可缓存。
  • 分层系统。系统架构允许多层级的用户。客户端不需要知道他最终连接的是哪一层服务器。
  • 按需编码(可选)。服务器在返回数据的同时,可以发送一些可执行代码,让客户端在本地临时执行,以此来动态扩展客户端的功能。
  • 统一界面。他有四个指导原则:资源识别、通过表征操作资源、自我描述信的信息、作为应用状态的超媒体(HATEOS)——据说这个原则能够实现的也不是很多,因为太理想化了。

Resource Design

在设计RESTful API时,我们最好遵循以下原则:

  • API端点的资源应该始终是复数名词,如果要用ID精确查找,那么就在URL里传入。例如GET /accountsGET /accounts/1DELETE /accounts/2都是合法的。
  • 如果是嵌套资源,应该通过以下方式访问:GET /accounts/1/payments/56
  • 使用HTTP方法来指定对资源的操作,例如GET、POST等进行增删改查。可以总结成下面的表格
资源路径 GET POST PUT DELETE
/accounts 获取所有账户 创建新账户 批量更新所有账户 删除所有账户
/accounts/1 获取 ID为1的账户 错误 更新ID为1的账户 删除 ID为1的账户
/accounts/1/payments 获取ID为1的账户的所有支付记录 为ID为1的账户创建新支付记录 批量更新ID为1的账户的所有支付记录 删除ID为1的账户的所有支付记录

HTTP Methods

这一部分他准备介绍主要的HTTP方法,让我们能够设计RESTful API。

  • GET,可以从服务器中获取资源,它是只读的,这意味着它不会影响资源的状态。
  • POST,用于向服务器发送数据以创建新资源,数据包含在请求体中
  • PUT,用于更新现有资源,若资源不存在则创建,数据包含在请求体中。
  • DELETE,用于删除资源。
  • PATCH,用于对资源进行部分修改,和PUT不同,后者更适合全面更新,而前者适合部分更新。
  • OPTIONS,这个方法用于检查Web服务器的功能,服务器会返回一个响应头,你可以用来检查是否支持跨域等。
  • HEAD,这个方法和GET类似,但是只返回响应头。适合在下载之前检查资源是否存在。

Response Status Codes

在这之后视频又讲了HTTP状态码。它是三位数代码,作为HTTP响应的一部分返回,表示请求的结果。

  • 1XX,信息性响应
  • 2XX,成功,例如200 OK成功的标准响应、201 CREATED请求成功了并创建了新的资源、204 NO CONTENT服务器成功处理了请求且没有额外内容,一般是对删除请求的响应,或者不愿意返回任何信息时。
  • 3XX,重定向,例如304 NOT MODIFIED是一种用于缓存目的的特殊响应类型,当客户端向服务器发送请求时,如果资源自从给定日期以来未被修改就会返回这个状态码,这样你就可以节省带宽。
  • 4XX,客户端错误,例如404 BAD REQUEST服务器因语法错误而无法理解请求(格式错误或参数无效)401 UNAUTHORIZED用户没有足够的特权,或者用户未认证、403 FORBIDDEN客户端没有请求所需的权限(服务器不愿意说具体理由的时候也可以用这个)
  • 5XX,服务端错误,例如500 INTERNAL SERVER ERROR当出现问题时给出的通用错误信息,意味着服务器本身出现了问题但是不会告诉你具体错误、503 SERVICE UNAVALIABLE表示服务当前不可用,只是暂时的,通常会包括一个时间告诉你需要等多久才能发起新的请求。

Example

下面的代码是一个PaymentRestController类的代码,它展示了SpringBoot中是如何实现RESTful API的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class PaymentRestController {
@PostMapping(value = "/payments")
public ResponseEntity<PaymentInformation> initiatePayment(
@RequestBody PaymentRequest paymentRequest
) {
// 业务代码
URI resultLocation = UriComponentsBuilder
.fromPath("/payments/{id}")
.buildAndExpand(confirmation.getId())
.toUri();
return ResponseEntity.created(resultLocation).body(confirmation);
}
}

这个类中有一个方法叫做initiatePayment,返回的是一个ResponseEntity的泛型类,也就是响应实体。方法上的@PostMapping注解表示要调用这个方法需要用POST方法访问/payments地址,然后请求的参数作为请求体传给了方法的实参。在业务代码结束后,如果我们要返回结果的地址的话,我们就需要创建一个URI,利用ReponseEntity.created()方法返回的是201状态码,然后返回一个响应体。

如果我们要指定Controller方法的响应状态,就可以用@ResponseStatus注解,这样当HTTP请求执行成功时,Spring就会让服务器返回对应注解中的状态。

In Action

那么介绍部分结束了之后,我们就可以开始写代码了。我们只需要保留SpringPracticeApplication.java这一个类即可,只保留初始的SpringBoot启动代码。

1
2
3
4
5
6
7
// SpringPracticeApplication.java
@SpringBootApplication
public class SpringPracticeApplication {
public static void main(String[] args) {
SpringApplication.run(SpringPracticeApplication.class, args);
}
}

接下来,我们直接在SpringPracticeApplication.java所在目录中创建一个Controller类,命名为FirstController。同时添加@RestController注解(要确保你的POM文件中有)spring-boot-starter-web依赖。我们首先尝试写一个GET方法,只需要在方法前面写上GetMapping注释。

1
2
3
4
5
6
7
8
// FirstController.java
@RestController
public class FirstController {
@GetMapping
public String sayHello() {
return "hello from my first controller";
}
}

直接在浏览器中输入localhost:8080,就可以看到返回的结果了。在老版本的SpringBoot中,如果你不写的话它是访问不到的。为了保险期间还是应该给@GetMapping注解加上参数最好了。

1
2
3
4
5
6
7
8
// FirstController.java
@RestController
public class FirstController {
@GetMapping("/hello")
public String sayHello() {
return "hello from my first controller";
}
}

现在如果你再直接输入localhost:8080的话,那就会显示Whitelabel Error Page。在正确的Hello页面里,我们通过浏览器调试工具,找到对应的网络请求,可以看到有很多的属性。

有很多不同的HTTP请求属性

我们看看@ResponseStatus的源代码。对于源代码的讲解会用//注释来编写,同时源代码中的注释也已经全部翻译为中文了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ResponseStatus.java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatus {

/**
* {@link #code} 的别名。
*/
@AliasFor("code")
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
// 这里是传递参数的部分,类型为HTTP对象

/**
* 用于响应的状态码。
* <p>默认值为{@link HttpStatus#INTERNAL_SERVER_ERROR},
* 通常应修改为更为合适的状态码。
* @since 4.2
* @see jakarta.servlet.http.HttpServletResponse#setStatus(int)
* @see jakarta.servlet.http.HttpServletResponse#sendError(int)
*/
@AliasFor("value")
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;

/**
* 用于响应的<em>错误原因</em>。
* <p>默认为空字符串,此时该属性会被忽略。
* 将错误原因设置为非空值,即可用于发送 Servlet 容器的错误页面。
* 在这种情况下,处理器方法的返回值将被忽略。
* @see jakarta.servlet.http.HttpServletResponse#sendError(int, String)
*/
String reason() default "";

}

HttpStatus是一个枚举类,例如201就是HttpStatus.CREATED。那么假如我们还有一个方法映射到/hello-2,我希望他正常访问时返回的是202,那么只需要在@ResponseStatus注解中传入即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// FirstController.java

@RestController
public class FirstController {
@GetMapping("/hello")
public String sayHello() {
return "hello from my first controller";
}
@GetMapping("/hello-2")
@ResponseStatus(HttpStatus.ACCEPTED)
public String sayHello2() {
return "hello 2 from my first controller";
}
}

重新运行应用,观察变化,我们会发现响应的状态代码确实变成202了。

我也是第一次见到这个状态码

接下来我们尝试创建一个POST endpoint。写好对应的注释,然后编写方法传参,接收一个message作为参数,然后直接将其显示出来。

1
2
3
4
5
6
7
8
9
10
11
// FirstController.java

@RestController
public class FirstController {
@PostMapping("/post")
public String post(
String message
){
return "Request accepted and message is "+ message;
}
}

直接在浏览器中输入/post显然是不行的,因为通过地址栏访问的全都是GET方法,此时对于/post返回的状态码是405。这个时候作者开始教我们如何使用Postman了。由于我们之前有过使用APIFox的经历,因此这边直接给他卸载了,装上Postman了。装完之后,我们直接发送一个空的Post请求到/post,会发现输出的结果是Request accepted and message is null,这是因为我们根本没有传入message变量。我们需要在URL的Body里面添加上对应的message

Body不仅可以传入text,还能传入json或者XML等

不对啊,怎么还是null?说明这个参数根本没传进来,我们需要告诉编译器,这个参数是在Request body位置的,因此我们需要给参数添加对应的注释@RequestBody,这意味着这个参数是在我们的请求体中。

1
2
3
4
5
6
7
8
9
10
11
// FirstController.java

@RestController
public class FirstController {
@PostMapping("/post")
public String post(
@RequestBody String message
){
return "Request accepted and message is "+ message;
}
}

现在我们再次尝试就发现正常了。当我们将HTTP请求发送给Spring时,它会将请求报文转换成指定的JAVA对象。当然请求体中改用JSON也是可以的。

他会将对应的键放到对应的形参的位置吗?

我们会发现输出的结果是

Request accepted and message is {

“message”: “baobao”

}

它只是把这个JSON对象直接转成String了。如果我们需要接收很复杂的对象的话,我们应该首先创建一个类来承接这个对象。在main函数所在目录下新建一个类Order,同时定义基本的成员变量。

1
2
3
4
5
6
// Order.java
public class Order {
private String customerName;
private String productName;
private int quantity;
}

对应的Controller代码也需要进行更改来承接这个类。

1
2
3
4
5
6
7
8
9
10
11
// FirstController.java

@RestController
public class FirstController {
@PostMapping("/post-order")
public String postOrder(
@RequestBody Order order
){
return "Request accepted and order is "+ order.toString();
}
}

同时对应的Postman请求也要做修改。

直接用类的字段名当做JSON的键名就好了

但是我们添加断点调试会发现,为什么所有的成员变量都是null呢?这是因为我们传入的是一个JSON字符串,Spring没有将它反序列化为一个对象,因此才没有传入任何属性。

这是怎么回事

实际上,Spring底层的Jackson库是通过反射来读取属性的,当反序列化时,Jackson会默认寻找public的setter方法或者public的构造器或者字段,而在序列化时,Jackson会默认去找pubilc的getter方法或者public字段。不过除此之外,你还可以通过@JsonProperty注解来让它打破这些规则,例如

1
2
3
4
5
public class User {
@JsonProperty("user_name") // 告诉 Jackson:这个私有字段叫 user_name
private String name;
// 不需要 getter/setter
}

不过我们还是更推荐走这种默认的反序列化方式。跟着视频中修改这个类,添加getter和setter,顺便重写一下toString方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Order.java
public class Order {
private String customerName;
private String productName;
private int quantity;

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

public String getProductName() {
return productName;
}

public void setProductName(String productName) {
this.productName = productName;
}

public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}

@Override
public String toString() {
return "Order{" +
"customerName='" + customerName + '\'' +
", productName='" + productName + '\'' +
", quantity=" + quantity +
'}';
}
}

现在就正确了,可以看到输出的是

Request accepted and order is Order{customerName=’baobao’, productName=’apple’, quantity=10}

假如说前端传入的JSON键名想要和后端接收到的不同呢?就可以用到我们上文中提到的@JsonProperty注解了。在这个注解的源码中告诉了我们这个注解的功能和用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// JsonProperty.java

/**
* 标记注解,可用于将**非静态方法**定义为逻辑属性的「setter」或「getter」
* (具体取决于方法签名);
* 也可用于将**非静态对象字段**标记为逻辑属性,参与序列化与反序列化
* (用于赋值或取值)
*<p>
* 注解值("")表示直接使用字段名(或由存取器方法/setter/getter推导的名称)
* 作为属性名,不做任何修改;若传入非空字符串,则可自定义属性名。
* 属性名指的是对外暴露的名称,即 JSON 对象中的属性名
* (区别于 Java 对象内部的字段名)。
*<p>
* 注意:**禁止**在「单行声明多个 Java 字段」的场景下使用非空注解值,示例:
*<pre>
* public class POJO {
* \@JsonProperty("a")
* public int a, b, c;
*</pre>
* 该写法会将同一个注解绑定到所有字段,导致属性名冲突。
*<p>
* 从 Jackson 2.6 版本开始,该注解也可用于修改枚举({@code Enum})的序列化行为,示例:
*<pre>
public enum MyEnum {
{@literal @JsonProperty}("theFirstValue") THE_FIRST_VALUE,
{@literal @JsonProperty}("another_value") ANOTHER_VALUE;
}
</pre>
* 可作为 {@link JsonValue} 注解的替代方案。
*<br>
* 注意:对于枚举({@code Enum})类型,空字符串是合法的注解值
* (未指定 value 属性时默认视为空字符串),这与普通属性不同,**不代表**「使用枚举默认名称」。
* (该处理逻辑在 Jackson 2.19 版本中修复)
*<p>
* 从 Jackson 2.12 版本开始,支持指定属性的 {@code namespace(命名空间)}:
* 该属性仅适用于特定格式的后端处理器(最典型的是 XML 格式)。
*<p>
* 注意:若同时存在 {@link JsonIgnore} 等排除类注解,
* 该属性存取器会被忽略;**忽略注解的优先级高于包含注解**。
*/

添加了注解的类就像这样:

1
2
3
4
5
6
7
8
9
10
11
// Order.java

public class Order {
@JsonProperty("c-name")
private String customerName;
@JsonProperty("p-name")
private String productName;
@JsonProperty("q")
private int quantity;
// ...
}

这时候如果你在JSON中用原先的键名,那么就和之前一样都是null了。

在Java14引入了“记录(Record)”特性,它是一种不可变的、透明的、用于存储数据的载体,核心目的是为了解决编写数据类时的大量模板代码,可以说算是一种语法糖了。但是它也有一些限制。

  • 不能继承其他类,可以实现接口。
  • 所有字段都是final,没有setter也无法修改。
  • 只有一个默认的全参构造器,如果要校验的话在大括号中要写一个紧凑构造器(没有参数列表)
  • 所有字段不能是transient。

新建java文件的时候,在idea中,我们可以直接选择新建一个记录,然后直接在括号里给出字段就行了。同时也给controller添加一个访问的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// OrderRecord.java
public record OrderRecord(
String customerName,
String productName,
int quantity
) {
}

//FirstController.java

@RestController
public class FirstController {
@PostMapping("/post-order-record")
public String postOrderRecord(
@RequestBody OrderRecord order
){
return "Request accepted and order is "+ order.toString();
}
}

也能得到正确的结果。

接下来我们看如何使用路径来传参,例如我想访问http://localhost:8080/hello/baobao如何让controller中的方法来处理这个请求,只需要在Mapping的链接里加上大括号然后写上变量名就行了,同时给参数加上一个@PathVariable注解。注意保持一致。

1
2
3
4
5
6
7
8
9
10
11
// FirstController.java

@RestController
public class FirstController {
@GetMapping("/hello/{userName}")
public String pathVar(
@PathVariable String userName
) {
return "my value = " + userName;
}
}

但是通常情况下,URL一半是不用驼峰命名的,最好还是叫/hello/{user-name},给注解直接传参就行了,即

1
2
3
4
5
6
7
8
9
10
11
// FirstController.java

@RestController
public class FirstController {
@GetMapping("/hello/{user-name}")
public String pathVar(
@PathVariable("user-name") String userName
) {
return "my value = " + userName;
}
}

都会正常输出my value = baobao

接下来我们再看如何将参数作为请求参数传递,即?xxx=xxx&yyy=yyy这类的在请求链接里的参数。只需要在方法的形参中加入@RequestParam注解即可。注解的参数是你想要的变量名称,如果不给的话就和形参名保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
// FirstController.java

@RestController
public class FirstController {
@GetMapping("/hello")
public String paramVar(
@RequestParam("user-name") String userName,
@RequestParam("last-name") String lastName
) {
return "my value = " + userName + " " + lastName;
}
}

现在就可以访问localhost:8080/hello?user-name=baobao&last-name=b了。原视频作者在这里运行出现了错误,这是因为他之前的getMapping还有一个指向hello的无参的方法,这就重了所以才会报错。同时作者这里讲了路径变量和请求参数的用法:一般来讲路径变量代表着某种资源的值,而请求参数用于传输上下文信息或者查询参数,和路径剥离。

接下来视频开始给我们讲原理了。下面就是Mapping的原理图。

直接从他视频中截出来了

  1. 发送请求,第一个接受请求的对象是Dispatchaer Servelet。
  2. dispatcher Servelet会转发请求到HandlerMapping类,映射Handler mapping对象。
  3. Handler Mapping 会尝试寻找映射的Controller,然后参考Mapping Registry。
  4. 如果找到的话会把方法返回给Handler Mapping。
  5. 他会将请求转发给所需的Controller。
  6. 执行业务代码。
  7. 业务代码的响应会回到Controller。
  8. Controller会把响应反馈给Dispatcher Servelet。
  9. Servelet会把结果返回给用户。

Mapping Registry要求路径或HTTP方法必须唯一,如果路径和方法均相同的话就会有冲突,它无法确定要用哪一个,抛出BeanCreationException告诉你Cannnot map ‘xxx’ method ‘yyy’。虽然说两个方法名称不同,但是他们的路径和方法都相同,但是Spring也会将其视作冲突并无法构建。

总结

这一部分主要讲述了如何编写和构建高效的RESTful API,可以说是SpringBoot的核心中的核心,毕竟这就是后端主要的功能。接下来的部分应该讲的是JPA等数据库操作,这个就要等到下次再说了。

参考文献

  • 标题: Spring Boot & Spring Data JPA – Complete Course(2) - NoTe0x004
  • 作者: Baobao0824
  • 创建于 : 2026-05-20 14:22:32
  • 更新于 : 2026-05-20 14:22:32
  • 链接: https://blog.baobao0824.top/学习笔记NoTe/NT-0x004/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Spring Boot & Spring Data JPA – Complete Course(2) - NoTe0x004