使用Spring Boot构建Web服务层

11 服务发布:如何构建一个 RESTful 风格的 Web 服务?

通过前面课程的学习,我们已经掌握了构建一个 Spring Boot 应用程序的数据访问层组件实现方法。接下来的几讲,我们将讨论另一层组件,即 Web 服务层的构建方式。

服务与服务之间的交互是系统设计和发展的必然需求,其涉及 Web 服务的发布及消费,今天我们先讨论如何在 Spring Boot 应用程序中发布 Web 服务。

SpringCSS 系统中的服务交互

在具体的技术体系介绍之前,我们先来梳理 SpringCSS 案例中服务交互之间的应用场景。

对于客服系统而言,其核心业务流程是生成客服工单,而工单的生成通常需要使用用户账户信息和所关联的订单信息。

在 SpringCSS 案例中,前面几讲我们已经构建了一个用于管理订单的 order-service,接下来我们将分别构建管理用户账户的 account-service 及核心的客服服务 customer-service。

关于三个服务之间的交互方式,我们先通过一张图了解下,如下图所示:

SpringCSS 案例系统中三个服务的交互方式图

实际上,通过上图我们已经可以梳理工单生成 generateCustomerTicket 核心方法的执行流程,这里我们先给出代码的框架,如下代码所示:

java 复制代码
public CustomerTicket generateCustomerTicket(Long accountId, String orderNumber) {
	// 创建客服工单对象
    CustomerTicket customerTicket = new CustomerTicket();

    // 从远程 account-service 中获取 Account 对象
    Account account = getRemoteAccountById(accountId);

    // 从远程 order-service 中获取 Order 读写
    Order order = getRemoteOrderByOrderNumber(orderNumber);

    // 设置 CustomerTicket 对象并保存
    customerTicket.setAccountId(accountId);
    customerTicket.setOrderNumber(order.getOrderNumber());
    customerTicketRepository.save(customerTicket);
    
    return customerTicket;
}

因 getRemoteAccountById 与 getRemoteOrderByOrderNumber 方法都涉及远程 Web 服务的调用,因此首先我们需要创建 Web 服务。

而 Spring Boot 为我们创建 Web 服务提供了非常强大的组件化支持,简单而方便,我们一起来看一下。

创建 RESTful 服务

在当下的分布式系统及微服务架构中,RESTful 风格是一种主流的 Web 服务表现方式。

在接下来的内容中,我们将演示如何使用 Spring Boot 创建 RESTful 服务。但在此之前,我们先来理解什么是 RESTful 服务。

理解 RESTful 架构风格

你可能听说过 REST 这个名称,但并不清楚它的含义。

REST(Representational State Transfer,表述性状态转移)本质上只是一种架构风格而不是一种规范,这种架构风格把位于服务器端的访问入口看作一个资源,每个资源都使用 URI(Universal Resource Identifier,统一资源标识符) 得到一个唯一的地址,且在传输协议上使用标准的 HTTP 方法,比如最常见的 GET、PUT、POST 和 DELETE。

下表展示了 RESTful 风格的一些具体示例:

RESTful 风格示例

另一方面,客户端与服务器端的数据交互涉及序列化问题。关于序列化完成业务对象在网络环境上的传输的实现方式有很多,常见的有文本和二进制两大类。

目前 JSON 是一种被广泛采用的序列化方式,本课程中所有的代码实例我们都将 JSON 作为默认的序列化方式。

使用基础注解

在原有 Spring Boot 应用程序的基础上,我们可以通过构建一系列的 Controller 类暴露 RESTful 风格的 HTTP 端点。这里的 Controller 与 Spring MVC 中的 Controller 概念上一致,最简单的 Controller 类如下代码所示:

java 复制代码
@RestController
public class HelloController {

    @GetMapping("/")
    public String index() {
        return "Hello World!";
    }
}

从以上代码中可以看到,这里包含了 @RestController 和 @GetMapping 这两个注解。

其中,@RestController 注解继承自 Spring MVC 中的 @Controller 注解,顾名思义就是一个基于 RESTful 风格的 HTTP 端点,并且会自动使用 JSON 实现 HTTP 请求和响应的序列化/反序列化方式。

通过这个特性,在构建 RESTful 服务时,我们可以使用 @RestController 注解代替 @Controller 注解以简化开发。

另外一个 @GetMapping 注解也与 Spring MVC 中的 @RequestMapping 注解类似。我们先来看看 @RequestMapping 注解的定义,该注解所提供的属性都比较容易理解,如下代码所示:

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};
    
    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

而 @GetMapping 的注解的定义与 @RequestMapping 非常类似,只是默认使用了 RequestMethod.GET 指定 HTTP 方法,如下代码所示:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {

Spring Boot 2 中引入的一批新注解中,除了 @GetMapping ,还有 @PutMapping、@PostMapping、@DeleteMapping 等注解,这些注解极大方便了开发人员显式指定 HTTP 的请求方法。当然,你也可以继续使用原先的 @RequestMapping 实现同样的效果。

我们再看一个更加具体的示例,以下代码展示了 account-service 中的 AccountController。

java 复制代码
@RestController
@RequestMapping(value = "accounts")
public class AccountController {
    @GetMapping(value = "/{accountId}")
    public Account getAccountById(@PathVariable("accountId") Long accountId) {
        Account account = new Account();
        account.setId(1L);
        account.setAccountCode("DemoCode");
        account.setAccountName("DemoName");
        return account;
    }
}

在该 Controller 中,通过静态的业务代码我们完成了根据账号编号(accountId)获取用户账户信息的业务流程。

这里用到了两层 Mapping,第一层的 @RequestMapping 注解在服务层级定义了服务的根路径"/accounts",第二层的 @GetMapping 注解则在操作级别定义了 HTTP 请求方法的具体路径及参数信息。

到这里,一个典型的 RESTful 服务已经开发完成了,现在我们可以通过 java --jar 命令直接运行 Spring Boot 应用程序了。

在启动日志中,我们发现了以下输出内容(为了显示效果,部分内容做了调整),可以看到自定义的这个 AccountController 已经成功启动并准备接收响应。

java 复制代码
RequestMappingHandlerMapping : Mapped "{[/accounts/{accountId}], methods=[GET]}" onto public com.springcss.account.domain.Account com.springcss.account.controller.AccountController.getAccountById (java.lang.Long)

在本课程中,我们将引入 Postman 来演示如何通过 HTTP 协议暴露的端点进行远程服务访问。

Postman 为我们完成 HTTP 请求和响应过程提供了可视化界面,你可以尝试编写一个 AccountController,并通过 Postman 访问"http://localhost:8082/accounts/1"端点以得到响应结果。

在前面的 AccountController 中,我们还看到了一个新的注解 @PathVariable,该注解作用于输入的参数,下面我们就来看看如何通过这些注解控制请求的输入。

控制请求输入和输出

Spring Boot 提供了一系列简单有用的注解来简化对请求输入的控制过程,常用的包括 @PathVariable、@RequestParam 和 @RequestBody。

其中 @PathVariable 注解用于获取路径参数,即从类似 url/{id} 这种形式的路径中获取 {id} 参数的值。该注解的定义如下代码所示:

java 复制代码
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
    boolean required() default true;
}

通常,使用 @PathVariable 注解时,我们只需要指定一个参数的名称即可。我们可以再看一个示例,如下代码所示:

java 复制代码
@GetMapping(value = "/{accountName}")
public Account getAccountByAccountName(@PathVariable("accountName") String accountName) {
    Account account = accountService.getAccountByAccountName(accountName);
    return account;
}

@RequestParam 注解的作用与 @PathVariable 注解类似,也是用于获取请求中的参数,但是它面向类似 url?id=XXX 这种路径形式。

该注解的定义如下代码所示,相较 @PathVariable 注解,它只是多了一个设置默认值的 defaultValue 属性。

java 复制代码
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
    
    String defaultValue() default ValueConstants.DEFAULT_NONE;
}

在 HTTP 协议中,content-type 属性用来指定所传输的内容类型,我们可以通过 @RequestMapping 注解中的 produces 属性来设置这个属性。

在设置这个属性时,我们通常会将其设置为"application/json",如下代码所示:

java 复制代码
@RestController
@RequestMapping(value = "accounts", produces="application/json")
public class AccountController {

@RequestBody 注解用来处理 content-type 为 application/json 类型时的编码内容,通过 @RequestBody 注解可以将请求体中的 JSON 字符串绑定到相应的 JavaBean 上。

如下代码所示就是一个使用 @RequestBody 注解来控制输入的场景。

java 复制代码
@PutMapping(value = "/")
public void updateAccount(@RequestBody Account account) {

如果使用 @RequestBody 注解,我们可以在 Postman 中输入一个 JSON 字符串来构建输入对象,如下代码所示:

使用 Postman 输入 JSON 字符串发起 HTTP 请求示例图

通过以上内容的讲解,我们发现使用注解的操作很简单,接下来我们有必要探讨下控制请求输入的规则。

关于控制请求输入的规则,关键在于按照 RESTful 风格的设计原则设计 HTTP 端点,对于这点业界也存在一些约定。

  • 以 Account 这个领域实体为例,如果我们把它视为一种资源,那么 HTTP 端点的根节点命名上通常采用复数形式,即"/accounts",正如前面的示例代码所示。
  • 在设计 RESTful API 时,我们需要基于 HTTP 语义设计对外暴露的端点的详细路径。针对常见的 CRUD 操作,我们展示了 RESTful API 与非 RESTful API 的一些区别。

RESTful 风格对比示例

基于以上介绍的控制请求输入的实现方法,我们可以给出 account-service 中 AccountController 类的完整实现过程,如下代码所示:

java 复制代码
@RestController
@RequestMapping(value = "accounts", produces="application/json")
public class AccountController {

    @Autowired
    private AccountService accountService;

    @GetMapping(value = "/{accountId}")
    public Account getAccountById(@PathVariable("accountId") Long accountId) {
        Account account = accountService.getAccountById(accountId);
        return account;
    }

    @GetMapping(value = "accountname/{accountName}")
    public Account getAccountByAccountName(@PathVariable("accountName") String accountName) {
        Account account = accountService.getAccountByAccountName(accountName);
        return account;
    }

    @PostMapping(value = "/")
    public void addAccount(@RequestBody Account account) {
        accountService.addAccount(account);
    }

    @PutMapping(value = "/")
    public void updateAccount(@RequestBody Account account) {
        accountService.updateAccount(account);
    }

    @DeleteMapping(value = "/")
    public void deleteAccount(@RequestBody Account account) {
        accountService.deleteAccount(account);
    }
}

介绍完对请求输入的控制,我们再来讨论如何控制请求的输出。

相较输入控制,输出控制就要简单很多,因为 Spring Boot 所提供的 @RestController 注解已经屏蔽了底层实现的复杂性,我们只需要返回一个普通的业务对象即可。@RestController 注解相当于是 Spring MVC 中 @Controller 和 @ResponseBody 这两个注解的组合,它们会自动返回 JSON 数据。

这里我们也给出了 order-service 中的 OrderController 实现过程,如下代码所示:

java 复制代码
@RestController
@RequestMapping(value="orders/jpa")
public class JpaOrderController {
    @Autowired
    JpaOrderService jpaOrderService;
    @GetMapping(value = "/{orderId}")
    public JpaOrder getOrderById(@PathVariable Long orderId) {
        JpaOrder order = jpaOrderService.getOrderById(orderId);
     	return order;
    }

    @GetMapping(value = "orderNumber/{orderNumber}")
    public JpaOrder getOrderByOrderNumber(@PathVariable String orderNumber) {
        JpaOrder order = jpaOrderService.getOrderByOrderNumber(orderNumber);
//      JpaOrder order = jpaOrderService.getOrderByOrderNumberByExample(orderNumber);
//      JpaOrder order = jpaOrderService.getOrderByOrderNumberBySpecification(orderNumber);
     	return order;
    }

    @PostMapping(value = "")
    public JpaOrder addOrder(@RequestBody JpaOrder order) {
        JpaOrder result = jpaOrderService.addOrder(order);
     	return result;
    }
}

从上面示例可以看到,我们使用了 09 讲中介绍的 Spring Data JPA 完成实体对象及数据访问功能。

小结与预告

构建 Web 服务是开发 Web 应用程序的基本需求,而设计并实现 RESTful 风格的 Web 服务是开发人员必须具备的开发技能。

基于 Spring Boot 框架,开发人员只需要使用几个注解就能实现复杂的 HTTP 端点,并暴露给其他服务进行使用,工作都变得非常简单。

12 服务调用:如何使用 RestTemplate 消费 RESTful 服务?

11 讲我们介绍了如何使用 Spring Boot 构建 RESTful 风格 Web 服务的实现方法,而 SpringCSS 案例系统的演进过程也从单个服务上升到多个服务之间的交互。

完成 Web 服务的构建后,我们需要做的事情就是如何对服务进行消费,这也是 12讲我们介绍的要点,接下来我们将基于 RestTemplate 模板工具类来完成这一目标。

使用 RestTemplate 访问 HTTP 端点

RestTemplate 是 Spring 提供的用于访问 RESTful 服务的客户端的模板工具类,它位于 org.springframework.web.client 包下。

在设计上,RestTemplate 完全可以满足 11 讲中介绍的 RESTful 架构风格的设计原则。相较传统 Apache 中的 HttpClient 客户端工具类,RestTemplate 在编码的简便性以及异常的处理等方面都做了很多改进。

接下来,我们先来看一下如何创建一个 RestTemplate 对象,并通过该对象所提供的大量工具方法实现对远程 HTTP 端点的高效访问。

创建 RestTemplate

如果我们想创建一个 RestTemplate 对象,最简单且最常见的方法是直接 new 一个该类的实例,如下代码所示:

java 复制代码
@Bean
public RestTemplate restTemplate(){
    return new RestTemplate();
}

这里我们创建了一个 RestTemplate 实例,并通过 @Bean 注解将其注入 Spring 容器中。

通常,我们会把上述代码放在 Spring Boot 的 Bootstrap 类中,使得我们在代码工程的其他地方也可以引用这个实例。

下面我们查看下 RestTemplate 的无参构造函数,看看创建它的实例时,RestTemplate 都做了哪些事情,如下代码所示:

java 复制代码
public RestTemplate() {
	this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(new StringHttpMessageConverter());
    this.messageConverters.add(new ResourceHttpMessageConverter(false));
    this.messageConverters.add(new SourceHttpMessageConverter<>());
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    //省略其他添加 HttpMessageConverter 的代码
}

可以看到 RestTemplate 的无参构造函数只做了一件事情,添加了一批用于实现消息转换的 HttpMessageConverter 对象。

我们知道通过 RestTemplate 发送的请求和获取的响应都是以 JSON 作为序列化方式,而我们调用后续将要介绍的 getForObject、exchange 等方法时所传入的参数及获取的结果都是普通的 Java 对象,我们就是通过使用 RestTemplate 中的 HttpMessageConverter 自动帮我们做了这一层转换操作。

这里请注意,其实 RestTemplate 还有另外一个更强大的有参构造函数,如下代码所示:

java 复制代码
public RestTemplate(ClientHttpRequestFactory requestFactory) {
	this();
    setRequestFactory(requestFactory);
}

从以上代码中,我们可以看到这个构造函数一方面调用了前面的无参构造函数,另一方面可以设置一个 ClientHttpRequestFactory 接口。而基于这个 ClientHttpRequestFactory 接口的各种实现类,我们可以对 RestTemplate 中的行为进行精细化控制。

这方面典型的应用场景是设置 HTTP 请求的超时时间等属性,如下代码所示:

java 复制代码
@Bean
public RestTemplate customRestTemplate(){
     HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
     httpRequestFactory.setConnectionRequestTimeout(3000);
     httpRequestFactory.setConnectTimeout(3000);
     httpRequestFactory.setReadTimeout(3000);

     return new RestTemplate(httpRequestFactory);
}

这里我们创建了一个 HttpComponentsClientHttpRequestFactory 工厂类,它是 ClientHttpRequestFactory 接口的一个实现类。通过设置连接请求超时时间 ConnectionRequestTimeout、连接超时时间 ConnectTimeout 等属性,我们对 RestTemplate 的默认行为进行了定制化处理。

使用 RestTemplate 访问 Web 服务

在远程服务访问上,RestTemplate 内置了一批常用的工具方法,我们可以根据 HTTP 的语义以及 RESTful 的设计原则对这些工具方法进行分类,如下表所示。

RestTemplate 中的方法分类表

接下来,我们将基于该表对 RestTemplate 中的工具方法进行详细介绍并给出相关示例。不过在此之前,我们想先来讨论一下请求的 URL。

在一个 Web 请求中,我们可以通过请求路径携带参数。在使用 RestTemplate 时,我们也可以在它的 URL 中嵌入路径变量,示例代码如下所示:

复制代码
("http://localhost:8082/account/{id}", 1)

这里我们对 account-service 提供的一个端点进行了参数设置:我们定义了一个拥有路径变量名为 id 的 URL,实际访问时,我们将该变量值设置为 1。其实,在URL 中也可以包含多个路径变量,因为 Java 支持不定长参数语法,多个路径变量的赋值可以按照参数依次设置。

如下所示的代码中,我们在 URL 中使用了 pageSize 和 pageIndex 这两个路径变量进行分页操作,实际访问时它们将被替换为 20 和 2。

复制代码
("http://localhost:8082/account/{pageSize}/{pageIndex}", 20, 2)

而路径变量也可以通过 Map 进行赋值。如下所示的代码同样定义了拥有路径变量 pageSize 和 pageIndex 的 URL,但实际访问时,我们会从 uriVariables 这个 Map 对象中获取值进行替换,从而得到最终的请求路径为http://localhost:8082/account/20/2。

java 复制代码
Map<String, Object> uriVariables = new HashMap<>();
uriVariables.put("pageSize", 20);
uriVariables.put("pageIndex", 2);
webClient.getForObject() ("http://localhost:8082/account/{pageSize}/{pageIndex}", Account.class, uriVariables);

请求 URL 一旦准备好了,我们就可以使用 RestTemplates 所提供的一系列工具方法完成远程服务的访问。

我们先来介绍 get 方法组,它包括 getForObject 和 getForEntity 这两组方法,每组各有三个方法。

例如,getForObject 方法组中的三个方法如下代码所示:

java 复制代码
public <T> T getForObject(URI url, Class<T> responseType)
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables){}
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)

从以上方法定义上,我们不难看出它们之间的区别只是传入参数的处理方式不同。

这里,我们注意到第一个 getForObject 方法只有两个参数(后面的两个 getForObject 方法分别支持不定参数以及一个 Map 对象),如果我们想在访问路径上添加一个参数,则需要我们构建一个独立的 URI 对象,示例如下代码所示:

java 复制代码
String url = "http://localhost:8080/hello?name=" + URLEncoder.encode(name, "UTF-8");
URI uri = URI.create(url);

我们先来回顾下 12 讲中我们介绍的 AccountController,如下代码所示:

java 复制代码
@RestController
@RequestMapping(value = "accounts")
public class AccountController {
    @GetMapping(value = "/{accountId}")
    public Account getAccountById(@PathVariable("accountId") Long accountId) {
		...
    }
}

对于上述端点,我们可以通过 getForObject 方法构建一个 HTTP 请求来获取目标 Account 对象,实现代码如下所示:

java 复制代码
Account result = restTemplate.getForObject("http://localhost:8082/accounts/{accountId}", Account.class, accountId);

当然,我们也可以使用 getForEntity 方法实现同样的效果,但在写法上会有所区别,如下代码所示:

java 复制代码
ResponseEntity<Account> result = restTemplate.getForEntity("http://localhost:8082/accounts/{accountId}", Account.class, accountId);
Account account = result.getBody();

在以上代码中,我们可以看到 getForEntity 方法的返回值是一个 ResponseEntity 对象,在这个对象中还包含了 HTTP 消息头等信息,而 getForObject 方法返回的只是业务对象本身。这是这两个方法组的主要区别,你可以根据个人需要自行选择。

与 GET 请求相比,RestTemplate 中的 POST 请求除提供了 postForObject 和 postForEntity 方法组以外,还额外提供了一组 postForLocation 方法。

假设我们有如下所示的 OrderController ,它暴露了一个用于添加 Order 的端点。

java 复制代码
@RestController
@RequestMapping(value="orders")
public class OrderController {
    @PostMapping(value = "")
    public Order addOrder(@RequestBody Order order) {
    	Order result = orderService.addOrder(order);
        return result;
    }
}

那么,通过 postForEntity 发送 POST 请求的示例如下代码所示:

java 复制代码
Order order = new Order();
order.setOrderNumber("Order0001");
order.setDeliveryAddress("DemoAddress");
ResponseEntity<Order> responseEntity = restTemplate.postForEntity("http://localhost:8082/orders", order, Order.class);
return responseEntity.getBody();

从以上代码中可以看到,这里我们构建了一个 Order 实体,通过 postForEntity 传递给了 OrderController 所暴露的端点,并获取了该端点的返回值。(特殊说明:postForObject 的操作方式也与此类似。)

掌握了 get 和 post 方法组后,理解 put 方法组和 delete 方法组就会非常容易了。其中 put 方法组与 post 方法组相比只是操作语义上的差别,而 delete 方法组的使用过程也和 get 方法组类似。这里我们就不再一一展开,你可以自己尝试做一些练习。

最后,我们还有必要介绍下 exchange 方法组。

对于 RestTemplate 而言,exchange 是一个通用且统一的方法,它既能发送 GET 和 POST 请求,也能用于发送其他各种类型的请求。

我们来看下 exchange 方法组中的其中一个方法签名,如下代码所示:

java 复制代码
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException

请注意,这里的 requestEntity 变量是一个 HttpEntity 对象,它封装了请求头和请求体,而 responseType 用于指定返回数据类型。 假如前面的 OrderController 中存在一个根据订单编号 OrderNumber 获取 Order 信息的端点,那么我们使用 exchange 方法发起请求的代码就变成这样了,如下代码所示。

复制代码
ResponseEntity<Order> result = restTemplate.exchange("http://localhost:8082/orders/{orderNumber}", HttpMethod.GET, null, Order.class, orderNumber);

而比较复杂的一种使用方式是分别设置 HTTP 请求头及访问参数,并发起远程调用,示例代码如下所示:

java 复制代码
//设置 HTTP Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);

//设置访问参数
HashMap<String, Object> params = new HashMap<>();
params.put("orderNumber", orderNumber);

//设置请求 Entity
HttpEntity entity = new HttpEntity<>(params, headers);
ResponseEntity<Order> result = restTemplate.exchange(url, HttpMethod.GET, entity, Order.class);
RestTemplate 其他使用技巧

除了实现常规的 HTTP 请求,RestTemplate 还有一些高级用法可供我们进行使用,如指定消息转换器、设置拦截器和处理异常等。

指定消息转换器

在 RestTemplate 中,实际上还存在第三个构造函数,如下代码所示:

java 复制代码
public RestTemplate(List<HttpMessageConverter<?>> messageConverters) {
	Assert.notEmpty(messageConverters, "At least one HttpMessageConverter required");
    this.messageConverters.addAll(messageConverters);
}

从以上代码中不难看出,我们可以通过传入一组 HttpMessageConverter 来初始化 RestTemplate,这也为消息转换器的定制提供了途径。

假如,我们希望把支持 Gson 的 GsonHttpMessageConverter 加载到 RestTemplate 中,就可以使用如下所示的代码。

java 复制代码
@Bean
public RestTemplate restTemplate() {
	List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
    messageConverters.add(new GsonHttpMessageConverter());
    RestTemplate restTemplate = new RestTemplate(messageConverters);
    return restTemplate;
}

原则上,我们可以根据需要实现各种自定义的 HttpMessageConverter ,并通过以上方法完成对 RestTemplate 的初始化。

设置拦截器

如果我们想对请求做一些通用拦截设置,那么我们可以使用拦截器。不过,使用拦截器之前,首先我们需要实现 ClientHttpRequestInterceptor 接口。

这方面最典型的应用场景是在 Spring Cloud 中通过 @LoadBalanced 注解为 RestTemplate 添加负载均衡机制。我们可以在 LoadBalanceAutoConfiguration 自动配置类中找到如下代码。

java 复制代码
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
     final LoadBalancerInterceptor loadBalancerInterceptor) {
        return restTemplate -> {
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
            list.add(loadBalancerInterceptor);
            restTemplate.setInterceptors(list);
     };
}

在上面代码中,我们可以看到这里出现了一个 LoadBalancerInterceptor 类,该类实现了 ClientHttpRequestInterceptor 接口,然后我们通过调用 setInterceptors 方法将这个自定义的 LoadBalancerInterceptor 注入 RestTemplate 的拦截器列表中。

处理异常

请求状态码不是返回 200 时,RestTemplate 在默认情况下会抛出异常,并中断接下来的操作,如果我们不想采用这个处理过程,那么就需要覆盖默认的 ResponseErrorHandler。示例代码结构如下所示:

java 复制代码
RestTemplate restTemplate = new RestTemplate();

ResponseErrorHandler responseErrorHandler = new ResponseErrorHandler() {
	@Override
    public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
    	return true;
    }

    @Override
    public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
    	//添加定制化的异常处理代码
    }
};

restTemplate.setErrorHandler(responseErrorHandler);

在上述的 handleError 方法中,我们可以实现任何自己想控制的异常处理代码。

实现 SpringCSS 案例中的服务交互

介绍完 RestTemplate 模板工具类的使用方式后,我们再回到 SpringCSS 案例。

11 讲中,我们已经给出了 customer-service 的 CustomerService 类中用于完成与 account-service 和 order-service 进行集成的 generateCustomerTicket 方法的代码结构,如下代码所示:

java 复制代码
public CustomerTicket generateCustomerTicket(Long accountId, String orderNumber) {
	// 创建客服工单对象
    CustomerTicket customerTicket = new CustomerTicket();

    // 从远程 account-service 中获取 Account 对象
    Account account = getRemoteAccountById(accountId);
 
    // 从远程 order-service 中获取 Order 读写
    Order order = getRemoteOrderByOrderNumber(orderNumber);

    // 设置 CustomerTicket 对象并保存
    customerTicket.setAccountId(accountId);
    customerTicket.setOrderNumber(order.getOrderNumber());
    customerTicketRepository.save(customerTicket);

    return customerTicket;
}

这里以 getRemoteOrderByOrderNumber 方法为例,我们来对它的实现过程进行展开,getRemoteOrderByOrderNumber 方法定义代码如下:

java 复制代码
@Autowired
private OrderClient orderClient;

private OrderMapper getRemoteOrderByOrderNumber(String orderNumber) {
	return orderClient.getOrderByOrderNumber(orderNumber);
}

getRemoteAccountById 方法的实现过程也类似。

接下来我们构建一个 OrderClient 类完成对 order-service 的远程访问,如下代码所示:

java 复制代码
@Component
public class OrderClient {
    private static final Logger logger = LoggerFactory.getLogger(OrderClient.class);

    @Autowired
    RestTemplate restTemplate;

    public OrderMapper getOrderByOrderNumber(String orderNumber) {
        logger.debug("Get order from remote: {}", orderNumber);
        ResponseEntity<OrderMapper> result = restTemplate.exchange(
                "http://localhost:8083/orders/{orderNumber}", HttpMethod.GET, null,
                OrderMapper.class, orderNumber);

        OrderMapper order= result.getBody();

        return order;
    }
}

注意:这里我们注入了一个 RestTemplate 对象,并通过它的 exchange 方法完成对远程 order-serivce 的请求过程。且这里的返回对象是一个 OrderMapper,而不是一个 Order 对象。最后,RestTemplate 内置的 HttpMessageConverter 完成 OrderMapper 与 Order 之间的自动映射。

事实上,OrderMapper 与 Order 对象的内部字段一一对应,它们分别位于两个不同的代码工程中,为了以示区别我们才故意在命名上做了区分。

小结与预告

12 讲的内容,我们是在 11 讲的内容基础上引入了 RestTemplate 模板类来完成对远程 HTTP 端点的访问。RestTemplate 为开发人员提供了一大批有用的工具方法来实现 HTTP 请求的发送以及响应的获取。同时,该模板类还开发了一些定制化的入口供开发人员嵌入,用来实现对 HTTP 请求过程进行精细化管理的处理逻辑。和 JdbcTemplate 一样,RestTemplate 在设计和实现上也是一款非常有效的工具类。

13 服务调用:如何正确理解 RestTemplate 远程调用实现原理?

在 12 讲中,我们详细描述了如何使用 RestTemplate 访问 HTTP 端点的使用方法,它涉及 RestTemplate 初始化、发起请求及获取响应结果等核心环节。今天,我们将基于上一课时中的这些环节,从源码出发让你真正理解 RestTemplate 实现远程调用的底层原理。

初始化 RestTemplate 实例

12 讲中我们提到可以通过 RestTemplate 提供的几个构造函数对 RestTemplate 进行初始化。在分析这些构造函数之前,我们有必要先看一下 RestTemplate 类的定义,如下代码所示:

java 复制代码
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations

从上述代码中,我们可以看到 RestTemplate 扩展了 InterceptingHttpAccessor 抽象类,并实现了 RestOperations 接口。接下来我们围绕 RestTemplate 的方法定义进行设计思路的梳理。

首先,我们来看看 RestOperations 接口的定义,这里截取了部分核心方法,如下代码所示:

java 复制代码
public interface RestOperations {
    <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

	<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

    <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables) throws RestClientException;

	void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException;

	void delete(String url, Object... uriVariables) throws RestClientException;

	<T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,

    Class<T> responseType, Object... uriVariables) throws RestClientException;
	...
}

显然,RestOperations 接口定义了 12 讲中介绍到的 get/post/put/delete/exhange 等所有远程调用方法组,这些方法都遵循 RESTful 架构风格而设计。RestTemplate 为这些接口提供了实现机制,这是它的一条代码支线。

然后我们再看 InterceptingHttpAccessor,它是一个抽象类,包含的核心变量如下代码所示:

java 复制代码
public abstract class InterceptingHttpAccessor extends HttpAccessor {
    private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
    private volatile ClientHttpRequestFactory interceptingRequestFactory;
    ...
}

通过变量定义,我们明确了 InterceptingHttpAccessor 包含两部分处理功能,一部分负责设置和管理请求拦截器 ClientHttpRequestInterceptor,另一部分负责获取用于创建客户端 HTTP 请求的工厂类 ClientHttpRequestFactory。

同时,我们注意到 InterceptingHttpAccessor 同样存在一个父类 HttpAccessor,这个父类值真正实现了 ClientHttpRequestFactory 的创建及如何通过 ClientHttpRequestFactory 获取代表客户端请求的 ClientHttpRequest 对象。HttpAccessor 的核心变量如下代码所示:

java 复制代码
public abstract class HttpAccessor {
    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
   	...
}

从以上代码我们可以看到,HttpAccessor 中创建了 SimpleClientHttpRequestFactory 作为系统默认的 ClientHttpRequestFactory。关于 ClientHttpRequestFactory,我们会在本课时的后续内容中进行详细的讨论。

最后,针对这部分内容我们再来梳理下 RestTemplate 的类层结构,如下图所示:

RestTemplate 的类层结构

在 RestTemplate 的类层结构中,我们能快速理解它的设计思想。整个类层结构清晰地分成两条支线,左边支线用于完成与 HTTP 请求相关的实现机制,而右边支线提供了基于 RESTful 风格的操作入口,并使用了面向对象中的接口和抽象类完成这两部分功能的聚合。

RestTemplate 核心执行流程

介绍完 RestTemplate 的实例化过程,接下来我们来分析它的核心执行流程。

作为用于远程调用的模板工具类,我们可以从具备多种请求方式的 exchange 方法入手,该方法的定义如下代码所示:

java 复制代码
@Override
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
  @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
  throws RestClientException {
  	//构建请求回调
    RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);

    //构建响应体抽取器
    ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);

    //执行远程调用
    return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));
}

显然,我们应该进一步关注这里的 execute 方法。事实上,无论我们采用 get/put/post/delete 中的哪种方法发起请求,RestTemplate 负责执行远程调用时,使用的都是 execute 方法,该方法定义如下代码所示:

java 复制代码
@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
	URI expanded = getUriTemplateHandler().expand(url, uriVariables);
	return doExecute(expanded, method, requestCallback, responseExtractor);
}

从以上代码中,我们发现 execute 方法首先通过 UriTemplateHandler 构建了一个 URI,然后将请求过程委托给 doExecute 方法进行处理,该方法定义如下代码所示:

java 复制代码
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
            @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
	Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;

    try {
    	//创建请求对象
        ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null) {
        	//执行对请求的回调
            requestCallback.doWithRequest(request);
        }
        //获取调用结果
        response = request.execute();
        //处理调用结果
        handleResponse(url, method, response);
        //使用结果提取从结果中提取数据
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
    catch (IOException ex) {
    	String resource = url.toString();
        String query = url.getRawQuery();
        resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
        throw new ResourceAccessException("I/O error on " + method.name() +
                    " request for \"" + resource + "\": " + ex.getMessage(), ex);
    }
    finally {
    	if (response != null) {
        	response.close();
        }
   	}
}

从上述方法中,我们发现使用 RestTemplate 进行远程调用时,主要涉及创建请求对象、执行远程调用及处理响应结果这三大步骤,下面我们分别展开说明下。

创建请求对象

创建请求对象的入口方法如下代码所示:

java 复制代码
ClientHttpRequest request = createRequest(url, method);

通过跟踪上面的 createRequest 方法,我们发现流程执行到了前面介绍的 HttpAccessor 类,如下代码所示:

java 复制代码
public abstract class HttpAccessor {
    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    ...
        
    protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
        ClientHttpRequest request = getRequestFactory().createRequest(url, method);
        if (logger.isDebugEnabled()) {
            logger.debug("Created " + method.name() + " request for \"" + url + "\"");
        }
        return request;
    }
}

创建 ClientHttpRequest 的过程是一种典型的工厂模式应用场景,这里我们直接创建了一个实现 ClientHttpRequestFactory 接口的 SimpleClientHttpRequestFactory 对象,然后再通过这个对象的 createRequest 方法创建了客户端请求对象 ClientHttpRequest 并返回给上层组件进行使用。ClientHttpRequestFactory 接口的定义如下代码所示:

java 复制代码
public interface ClientHttpRequestFactory {
    //创建客户端请求对象
	ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;
}

在 Spring 中,存在一批 ClientHttpRequestFactory 接口的实现类,而SimpleClientHttpRequestFactory 是它的默认实现,在实现自定义的 ClientHttpRequestFactory 时,开发人员也可以根据需要自行选择。

为简单起见,我们直接跟踪 SimpleClientHttpRequestFactory 的代码,来看它的 createRequest 方法,如下代码所示:

java 复制代码
private boolean bufferRequestBody = true;

@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
	HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
    	return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
    	return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

在上述 createRequest 中,首先我们通过传入的 URI 对象构建了一个 HttpURLConnection 对象,然后对该对象进行一些预处理,最后构造并返回一个 ClientHttpRequest 实例。

通过翻阅代码,我们发现上述的 openConnection 方法只是通过 URL 对象的 openConnection 方法返回了一个 UrlConnection,而 prepareConnection 方法也只是完成了对 HttpUrlConnection 超时时间、请求方法等常见属性的设置。

在这里,我们注意到 bufferRequestBody 参数的值为 true,因此通过 createRequest 方法最终返回的结果是一个 SimpleBufferingClientHttpRequest 对象。

执行远程调用

一旦获取了请求对象,我们就可以发起远程调用并获取响应了,RestTemplate 中的入口方法如下代码所示:

java 复制代码
response = request.execute();

这里的 request 就是前面创建的 SimpleBufferingClientHttpRequest 类,我们可以先来看一下该类的类层结构,如下图所示:

SimpleBufferingClientHttpRequest 类层结构图

在上图的 AbstractClientHttpRequest 中,定义了如下代码所示的 execute 方法。

java 复制代码
@Override
public final ClientHttpResponse execute() throws IOException {
	assertNotExecuted();
	ClientHttpResponse result = executeInternal(this.headers);
	this.executed = true;
	return result;
}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;

AbstractClientHttpRequest 类的作用是防止 HTTP 请求的 Header 和 Body 被多次写入,所以在 execute 方法返回之前,我们设置了一个 executed 标志位。同时,在 execute 方法中,我们最终调用了一个抽象方法 executeInternal,这个方法的实现在 AbstractClientHttpRequest 的子类 AbstractBufferingClientHttpRequest 中,如下代码所示:

java 复制代码
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
    byte[] bytes = this.bufferedOutput.toByteArray();

    if (headers.getContentLength() < 0) {
		headers.setContentLength(bytes.length);
	}
    
    ClientHttpResponse result = executeInternal(headers, bytes);

    this.bufferedOutput = new ByteArrayOutputStream(0);
	return result;
}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput)    throws IOException;

和 AbstractClientHttpRequest 类一样,我们进一步梳理了一个抽象方法 executeInternal,这个抽象方法通过最底层的 SimpleBufferingClientHttpRequest 类实现,如下代码所示:

java 复制代码
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
	addHeaders(this.connection, headers);

    // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
    if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {
    	this.connection.setDoOutput(false);
    }

    if (this.connection.getDoOutput() && this.outputStreaming) {
    	this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
    }

    this.connection.connect();
    if (this.connection.getDoOutput()) {
    	FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
    }
    else {
    	// Immediately trigger the request in a no-output scenario as well
        this.connection.getResponseCode();
    }
    
    return new SimpleClientHttpResponse(this.connection);
}

这里通过 FileCopyUtils.copy 工具方法,我们把结果写入输出流上了,executeInternal 方法最终返回的结果是一个包装了 Connection 对象的 SimpleClientHttpResponse。

处理响应结果

一个 HTTP 请求处理的最后一步是从 ClientHttpResponse 中读取输入流,然后格式化为一个响应体并将其转化为业务对象,入口代码如下所示:

java 复制代码
//处理调用结果
handleResponse(url, method, response);

//使用结果提取从结果中提取数据
return (responseExtractor != null ? responseExtractor.extractData(response) : null);

我们先来看这里的 handleResponse 方法,定义如下代码所示:

java 复制代码
protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
   	ResponseErrorHandler errorHandler = getErrorHandler();
   	boolean hasError = errorHandler.hasError(response);

   	if (logger.isDebugEnabled()) {
   		try {
    		logger.debug(method.name() + " request for \"" + url + "\" resulted in " +
          	response.getRawStatusCode() + " (" + response.getStatusText() + ")" +
           	(hasError ? "; invoking error handler" : ""));
   		}
   		catch (IOException ex) {
        	// ignore
   		}
    }

    if (hasError) {
    	errorHandler.handleError(url, method, response);
	}
}

以上代码中,通过 getErrorHandler 方法我们获取了一个 ResponseErrorHandler,如果响应的状态码错误,我们可以调用 handleError 来处理错误并抛出异常。在这里,我们发现这段代码实际上并没有真正处理返回的数据,而只是执行了错误处理。

而获取响应数据并完成转化的工作是在 ResponseExtractor 中,该接口定义如下代码所示:

java 复制代码
public interface ResponseExtractor<T> {
    @Nullable
    T extractData(ClientHttpResponse response) throws IOException;
}

在 RestTemplate 类中,我们定义了一个 ResponseEntityResponseExtractor 内部类实现了ResponseExtractor 接口,如下代码所示:

java 复制代码
private class ResponseEntityResponseExtractor <T> implements ResponseExtractor<ResponseEntity<T>> {
	@Nullable
    private final HttpMessageConverterExtractor<T> delegate;
 
    public ResponseEntityResponseExtractor(@Nullable Type responseType) {
    	if (responseType != null && Void.class != responseType) {
        	this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
        }
        else {
        	this.delegate = null;
        }
   }

   @Override
   public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {
   		if (this.delegate != null) {
    		T body = this.delegate.extractData(response);
        	return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);
    	}
    	else {
    		return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();
    	}
	}
}

在上述代码中,ResponseEntityResponseExtractor 中的 extractData 方法本质上是将数据提取部分的工作委托给了一个代理对象 delegate,而这个 delegate 的类型就是 HttpMessageConverterExtractor。

从命名上看,我们不难看出 HttpMessageConverterExtractor 类的内部使用了 12 讲介绍的 HttpMessageConverter 实现消息的转换,如下代码所示(代码做了裁剪):

java 复制代码
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
	private final List<HttpMessageConverter<?>> messageConverters;

	@Override
    @SuppressWarnings({"unchecked", "rawtypes", "resource"})
    public T extractData(ClientHttpResponse response) throws IOException {
		MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
		if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
			return null;
		}
        
        MediaType contentType = getContentType(responseWrapper);
		
        try {
            for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
                if (messageConverter instanceof GenericHttpMessageConverter) {
                    GenericHttpMessageConverter<?> genericMessageConverter =
                            (GenericHttpMessageConverter<?>) messageConverter;

                    if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
                        return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
                    }
                }

                if (this.responseClass != null) {
                    if (messageConverter.canRead(this.responseClass, contentType)) {
                        return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
                }
            }
        }
    }
	...
}

上述方法看上去有点复杂,但核心逻辑很简单,首先遍历 HttpMessageConveter 列表,然后判断其是否能够读取数据,如果能就调用 read 方法读取数据。

最后,我们讨论下 HttpMessageConveter 中如何实现 read 方法。

先来看 HttpMessageConveter 接口的抽象实现类 AbstractHttpMessageConverter,在它的 read 方法中我们同样定义了一个抽象方法 readInternal,如下代码所示:

java 复制代码
@Override
public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    return readInternal(clazz, inputMessage);
}

protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

在 12 讲中,我们提到 Spring 提供了一系列的 HttpMessageConveter 实现消息的转换,而最简单的实现方式是 StringHttpMessageConverter,该类的 read 方法如下代码所示:

java 复制代码
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
    Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
    return StreamUtils.copyToString(inputMessage.getBody(), charset);
}

StringHttpMessageConverter 的实现过程:首先从输入消息 HttpInputMessage 中通过 getBody 方法获取消息体,也就是一个 ClientHttpResponse 对象,再通过 copyToString 方法从该对象中读取数据,并返回字符串结果。

至此,通过 RestTemplate 发起、执行及响应整个 HTTP 请求的完整流程就介绍完毕了。

从源码解析到日常开发

本节课涉及了大量关于如果处理 HTTP 请求的实现细节,而这些实现细节对开发人员理解 HTTP 协议、掌握 HTTP 协议及远程调用很大帮助,后期,你可以根据实际需要针对某些细节进一步深入分析。

同时,通过对 RestTemplate 本身及围绕它的多个工具类的设计和实现过程进行梳理,也可以加深我们对抽象类与接口的标准设计理念的理解,并将这些设计理念付诸日常开发过程中。

小结与预告

我们要想深入理解和掌握一个 HTTP 请求的处理过程,剖析 RestTemplate 工具类的实现很有必要。

RestTemplate 中提供了创建请求对象、执行远程调用及处理响应结果这三大步骤的完整实现思路。本节课中我们对这些步骤进行了详细说明,并分析了其中包含的设计理念及实现技巧。

相关推荐
摇滚侠3 小时前
Spring Boot 3零基础教程,Spring Boot 特性介绍,笔记02
java·spring boot·笔记
ahauedu5 小时前
Spring Boot 2.7+ 中 RedisConnectionFactory Autowire 警告的深度解析
java·spring boot·后端
摇滚侠6 小时前
Spring Boot 3零基础教程,深度理解 Spring Boot 自动配置原理,笔记11
spring boot·笔记·后端
爆更小哇6 小时前
统一功能处理
java·spring boot
hweiyu0010 小时前
Spring Boot 项目集成 Gradle:构建、测试、打包全流程教程
java·spring boot·后端·gradle
一勺菠萝丶10 小时前
Spring Boot 项目启动报错:`Could not resolve type id ... no such class found` 终极解决方案!
java·spring boot·后端
摇滚侠14 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯16 小时前
Spring Boot测试框架详解
java·spring boot·后端
程序员小凯18 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存