REST 服务可用于实现两个应用之间的通讯,包括 Web 应用中的客户端和服务器之间,移动应用与后端服务之间,或两个后端服务之间。
10.1 使用 REST 服务在应用之间交换数据
REST端点是应用程序通过 Web 协议公开服务的方式,因此也称为 Web 服务。
在 Spring 中,REST 端点仍然是映射到 HTTP 方法和路径的控制器操作。但对于 REST 服务, Spring MVC 调度器 servlet 不会查找视图,服务器在 HTTP 响应中直接向客户端返回控制器操作的返回内容。
REST 端点需注意以下问题:
- 如果控制器的操作需要很长时间才能完成,对端点的 HTTP 调用可能会超时并中断通信。
- 不建议在一次调用中发送大量数据(例如几兆字节),可能会导致调用超时并中断通信。
- 端点上过多的并发调用可能导致应用失败。
- REST 端点调用可能因为网络原因而失败。
总之,要考虑对失效情况的处理。作者推荐了J. J. Geewax的API Design
Patterns (Manning, 2021)一书。
10.2 实现 REST 端点
Spring 在 REST 端点后面使用相同的 Spring MVC 机制。
看一下示例sq-ch10-ex1
。本例非常简单,没有HTML文件,只有一个控制类HelloController :
@Controller
public class HelloController {@GetMapping("/hello")@ResponseBodypublic String hello() {return "Hello!";}@GetMapping("/ciao")@ResponseBodypublic String ciao() {return "Ciao!";}
}
注解@Controller和@GetMapping上一章都讲过了,唯一新的注解是@ResponseBody。它的作用是告知调度 servlet,控制器的操作不会返回视图名称,而是直接在 HTTP 响应中发送数据。
本例和后续示例需要安装postman:
或者也可以命令行工具cURL(Ciao是意大利语的Hello):
$ curl http://localhost:8080/hello
Hello!
$ curl -X GET http://localhost:8080/ciao
Ciao!
示例sq-ch10-ex2
和上例的效果完全一样,只不过使用注解@RestController,他是@Controller 和 @ResponseBody 的组合。
@RestController
public class HelloController {@GetMapping("/hello")public String hello() {return "Hello!";}@GetMapping("/ciao")public String ciao() {return "Ciao!";}
}
10.3 管理 HTTP 响应
HTTP 响应是指后端应用根据客户端请求将数据返回给客户端的方式,包含以下数据:
- 响应头:响应中的短数据片段(通常不超过几个字)
- 响应正文:返回的大量数据
- 响应状态
此处建议阅读附录C:HTTP简介。
10.3.1 将对象作为响应主体发送
示例sq-ch10-ex3
和上例非常类似,只不过返回值从字符串变为对象(此对象也称为DTO,即data transfer object)。
看一下控制类:
@RestController
public class CountryController {@GetMapping("/france")public Country france() {Country c = Country.of("France", 67);return c;}@GetMapping("/all")public List<Country> countries() {Country c1 = Country.of("France", 67);Country c2 = Country.of("Spain", 47);return List.of(c1,c2);}
}
应用运行如下:
$ curl http://localhost:8080/all
[{"name":"France","population":67},{"name":"Spain","population":47}]$ curl http://localhost:8080/france
{"name":"France","population":67}
返回的对象为JSON格式。使用 REST 端点时,JSON 是最常见的对象表示方式(当然你也可以用XML或YAML)。此处建议阅读附录D:使用 JSON 格式。
10.3.2 设置响应状态和标头
在某些情况下,会要求自定义 HTTP 响应状态,其最简单、最常用的方法是使用 ResponseEntity 类。Spring 提供的这个类允许您指定 HTTP 响应的主体、状态和标头。详见示例sq-ch10-ex4
。
model类和上例一样,控制类变为如下:
@RestController
public class CountryController {@GetMapping("/france")public ResponseEntity<Country> france() {Country c = Country.of("France", 67);return ResponseEntity.status(HttpStatus.ACCEPTED).header("continent", "Europe").header("capital", "Paris").header("favorite_food", "cheese and wine").body(c);}
}
程序运行如下:
$ curl -v http://localhost:8080/france
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
* using HTTP/1.x
> GET /france HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.12.1
> Accept: */*
>
< HTTP/1.1 202
< continent: Europe
< capital: Paris
< favorite_food: cheese and wine
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 03 Aug 2025 13:02:47 GMT
<
{"name":"France","population":67}* Connection #0 to host localhost left intact
其中202对应HttpStatus.ACCEPTED。
postman运行输出如下:
10.3.3 在端点级别管理异常
在很多情况下,我们会使用异常来指示特定情况,其中一些与业务逻辑相关。在这种情况下,您可能需要在 HTTP 响应中设置一些详细信息,以告知客户端发生的具体情况。管理异常的方法之一是在控制器的操作中捕获异常,并使用ResponseEntity 类,在发生异常时发送不同的响应配置。
参见示例sq-ch10-ex5
。来看一下控制类的主体部分:
@PostMapping("/payment")public ResponseEntity<?> makePayment() {try {PaymentDetails paymentDetails = paymentService.processPayment();return ResponseEntity.status(HttpStatus.ACCEPTED).body(paymentDetails);} catch (NotEnoughMoneyException e) {ErrorDetails errorDetails = new ErrorDetails();errorDetails.setMessage("Not enough money to make the payment.");return ResponseEntity.badRequest().body(errorDetails);}}
运行输出如下:
$ curl -X POST http://localhost:8080/payment
{"message":"Not enough money to make the payment."}
上述错误处理方法虽然可行,但在更复杂的应用程序中,将异常管理分离处理会更方便。这样可以减少重复代码,因为有时同一个异常可能被多个端点复用。其次,当你需要理解特定情况的工作原理时,知道在一个地方找到所有异常逻辑会更方便。因此,推荐使用 REST 控制器建议,这是一个可以拦截控制器操作抛出的异常并根据拦截到的异常应用自定义逻辑的切面。
在示例sq-ch10-ex6
中,控制类PaymentController不再进行异常处理,因此代码大大简化:
@RestController
public class PaymentController {private final PaymentService paymentService;public PaymentController(PaymentService paymentService) {this.paymentService = paymentService;}@PostMapping("/payment")public ResponseEntity<PaymentDetails> makePayment() {PaymentDetails paymentDetails = paymentService.processPayment();return ResponseEntity.status(HttpStatus.ACCEPTED).body(paymentDetails);}
}
错误处理的工作由advice类ExceptionControllerAdvice 专门负责:
@RestControllerAdvice
public class ExceptionControllerAdvice {@ExceptionHandler(NotEnoughMoneyException.class)public ResponseEntity<ErrorDetails> exceptionNotEnoughMoneyHandler() {ErrorDetails errorDetails = new ErrorDetails();errorDetails.setMessage("Not enough money to make the payment.");return ResponseEntity.badRequest().body(errorDetails);}
}
输出和上例一样。
10.4 使用请求主体从客户端获取数据
前面已经使用HTTP 请求参数从客户端到服务器传输少量数据,传输大量数据则可以用HTTP 请求正文(request body)。
要使用请求正文,只需使用 @RequestBody 注解控制器操作的参数即可。默认情况下,Spring 会假定您使用 JSON 来表示注解的参数,并尝试将 JSON 字符串解码为参数类型的实例。如果无法将 JSON 格式的字符串解码为该类型,应用将返回状态为“400 Bad Request”的响应。
参见示例sq-ch10-ex7
。核心代码为控制类:
@RestController
public class PaymentController {private static Logger logger =Logger.getLogger(PaymentController.class.getName());@PostMapping("/payment")public ResponseEntity<PaymentDetails> makePayment(@RequestBody PaymentDetails paymentDetails) {logger.info("Received payment " + paymentDetails.getAmount());return ResponseEntity.status(HttpStatus.ACCEPTED).body(paymentDetails);}
}
程序输出如下:
$ curl -X POST http://127.0.0.1:8080/payment -d '{"amount": 1000}' -H "Content-Type: application/json"
{"amount":1000.0}
HTTP GET也支持请求正文,详见RFC 7231。
总结
- 表述性状态转移 (REST) Web 服务是在两个应用程序之间建立通信的一种简单方法。
- 在 Spring 应用中,Spring MVC 机制支持 REST 端点的实现。您需要使用 @ResponseBody 注解来指定方法直接返回响应主体,或者将 @Controller 注解替换为 @RestController 来实现 REST 端点。如果您不使用上述任何一种注解,调度器 Servlet 将假定控制器的方法返回的是视图名称,并尝试查找该视图。
- 您可以让控制器的操作直接返回 HTTP 响应主体,并依赖 Spring 默认的 HTTP 状态行为。
- 您可以通过让控制器的操作返回 ResponseEntity 实例来管理 HTTP 状态和标头。
- 管理异常的一种方法是直接在控制器的操作级别处理它们。这种方法将处理异常的逻辑与特定的控制器操作耦合在一起。有时,使用这种方法会导致代码重复,最好避免这种情况。
- 您可以直接在控制器的操作中管理异常,或者使用 REST 控制器建议类来分离控制器的操作抛出异常时执行的逻辑。
- 端点可以通过 HTTP 请求中的请求参数、路径变量或 HTTP 请求正文从客户端获取数据。