百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

精通Spring Boot 3 : 3. Spring Boot 网络开发 (2)

ztj100 2025-01-02 20:33 12 浏览 0 评论

Spring Web 注解控制器

Spring MVC 提供了基于注解的编程方式,并包含两个实用的注解:@Controller 和 @RestController。这些注解用于处理请求映射、请求输入、异常处理等多种功能。

@Controller 注解与 Model、ModelAndView 类以及 View 接口一起使用时,您可以访问 Session 属性和可在 HTML 页面中使用的对象。使用此功能时,您需要返回视图的名称,Spring Web 将负责解析和渲染 HTML 页面。请参见以下代码片段:

// more ...
@Controller
public class MyRetroBoardController {
    private RetroBoardService retroBoardService;
    @GetMapping("/retros")
    public String handle(Model model) {
        model.addAttribute("retros", retroBoardService.findAll());
        return "listRetros";
    }
    // more ...
}

前面的代码片段将 MyRetroBoardController 标记为一个网络控制器(使用 @Controller 注解)。当我们访问 "/retros" 这个端点时,它会创建一个必要的 Model 类,我们可以在其中添加一个属性(在这里是 retros 属性,值为所有的 retros),然后返回 HTML 页面的名称(listRetros,该页面位于 src/main/resources/templates/listRetros.html),这个页面将负责渲染并在页面中使用 retros 数据。Spring Web 会知道如何解析页面的位置以及如何通过模板引擎进行渲染。 如果您需要以特定值(与视图不同)进行响应,则必须在返回类型(方法中声明)上添加 @ResponseBody 注解,以便 Spring Web 能够使用 HTTP 消息转换器进行正确的响应。

@RestController 注解是一个类标记(它是 @Controller 的组合注解),可以直接写入响应体(因此不需要 @ResponseBody 注解)。我们将在整本书中广泛使用这个注解,因为我们将在后端更多地使用 Spring Boot。

Spring Web 技术还包括 @RequestMapping 注解,它可以用作类或方法的标记,因为它在将请求映射到控制器时非常有用。@RequestMapping 的一个优点是可以访问请求参数、头信息和媒体类型。虽然 @RequestMapping 可以用于每个方法,但有时可以使用一些快捷方式,例如 @GetMapping、@PostMapping、@PutMapping、@DeleteMapping 等等。这些快捷方式在列表 3-12 中有详细描述。

现在是时候编写我们的网络控制器了。请创建一个名为 web 的包和一个名为 RetroBoardController 的类,如清单 3-12 所示。

package com.apress.myretro.web;
import com.apress.myretro.board.Card;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.service.RetroBoardService;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@AllArgsConstructor
@RestController
@RequestMapping("/retros")
public class RetroBoardController {
    private RetroBoardService retroBoardService;
    @GetMapping
    public ResponseEntity<Iterable<RetroBoard>> getAllRetroBoards(){
        return ResponseEntity.ok(retroBoardService.findAll());
    }
    @PostMapping
    public ResponseEntity<RetroBoard> saveRetroBoard(@Valid @RequestBody RetroBoard retroBoard){
        RetroBoard result = retroBoardService.save(retroBoard);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{uuid}")
                .buildAndExpand(result.getId().toString())
                .toUri();
        return ResponseEntity.created(location).body(result);
    }
    @GetMapping("/{uuid}")
    public ResponseEntity<RetroBoard> findRetroBoardById(@PathVariable UUID uuid){
        return ResponseEntity.ok(retroBoardService.findById(uuid));
    }
    @GetMapping("/{uuid}/cards")
    public ResponseEntity<Iterable<Card>> getAllCardsFromBoard(@PathVariable UUID uuid){
        return ResponseEntity.ok(retroBoardService.findAllCardsFromRetroBoard(uuid));
    }
    @PutMapping("/{uuid}/cards")
    public ResponseEntity<Card> addCardToRetroBoard(@PathVariable UUID uuid,@Valid @RequestBody Card card){
        Card result = retroBoardService.addCardToRetroBoard(uuid,card);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{uuid}/cards/{uuidCard}")
                .buildAndExpand(uuid.toString(),result.getId().toString())
                .toUri();
        return ResponseEntity.created(location).body(result);
    }
    @GetMapping("/{uuid}/cards/{uuidCard}")
    public ResponseEntity<Card> getCardFromRetroBoard(@PathVariable UUID uuid,@PathVariable UUID uuidCard){
        return ResponseEntity.ok(retroBoardService.findCardByUUIDFromRetroBoard(uuid,uuidCard));
    }
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @DeleteMapping("/{uuid}/cards/{uuidCard}")
    public void deleteCardFromRetroBoard(@PathVariable UUID uuid,@PathVariable UUID uuidCard){
        retroBoardService.removeCardFromRetroBoard(uuid,uuidCard);
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, Object> response = new HashMap<>();
        response.put("msg","There is an error");
        response.put("code",HttpStatus.BAD_REQUEST.value());
        response.put("time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        response.put("errors",errors);
        return response;
    }
}

列表 3-12 源代码:src/main/java/com/apress/myretro/web/RetroBoardController.java

让我们来分析一下 RetroBoardController 类

  • @AllArgsConstructor:这个来自 Lombok 库的注解会使用字段作为参数来创建构造函数。在这种情况下,它将使用 RetroBoardService 作为参数。Spring 会通过这个构造函数注入 RetroBoardService bean,因此您可以在这个控制器类中访问它。
  • 我们通过@RestController 注解来标识这个类。该注解会直接使用所有声明的方法将内容写入响应体。
  • @RequestMapping:此注解将该类标记为响应任何请求的类,使用正确的 HTTP 方法,并将以/retros 端点作为其他路径的基础。
  • @GetMapping:该注解用于标记多个方法,这些方法将通过 HTTP GET 方法响应/retros 端点。这是@RequestMapping(method = RequestMethod.GET)的简化写法,后者提供了更多可用参数。如果你查看 findRetroBoardById 方法,会发现@GetMapping 使用了值"/{uuid}";这是一个映射到 URL 模式的路径变量。在这种情况下,使用了 PathPattern,允许使用如下匹配模式:
    • "/retros/docu?ent.doc": 仅在路径中匹配一个字符。
    • "/retros/*.jpg": 在路径中匹配任意数量的字符。
    • "/retros/**": 匹配多个路径部分。
    • "/retros/{project}/versions": 匹配路径段并将其作为变量捕获。
    • "/retros/{project:[a-z]+}/versions": 使用正则表达式匹配并捕获一个变量。
  • 你可以在方法中使用类似这样的代码:@GetMapping("/{product:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")。正如你所看到的,你可以选择如何访问你的接口,这同样适用于每个@RequestMapping 及其快捷方式。
  • ResponseEntity:这个类是从 HttpEntity(一个泛型类)扩展而来的,表示一个 HTTP 请求或响应实体,包含头部和主体。如果你查看代码,会发现类型各不相同。在 Spring Boot 或 Spring Web 应用程序中,使用 ResponseEntity 类是常见的做法之一。默认情况下,Spring Boot 会使用 HTTP JSON 消息转换器进行响应,因此你可以始终期待收到 JSON 格式的响应。当然,你也可以覆盖这个默认设置,以其他格式进行响应,例如 XML。
  • @PostMapping:这是一个用于响应 HTTP POST 请求的注解,它会在 HTTP 数据包中发送主体(数据)。这与@RequestMapping(method = RequestMethod.POST)是相同的。对于这种请求,通常需要发送数据,因此您可能需要使用@RequestBody 注解。
  • @RequestBody:这个注解会查看网络请求的主体,并尝试将其(通过 HttpMessageConverter)绑定到带有此注解的实例。在这个控制器中,我们有两个实例:一个用于 RetroBoard,另一个用于 Card 类。您可以使用 @Valid 注解来对数据进行验证。
  • @Valid:此注解用于标记一个参数,以便进行级联验证,检查该参数是否根据使用的约束(如@NotNull、@NotEmpty、@NotBlank 等)通过验证。所有这些注解都在 RetroBoard 和 Card 类中,并属于 Jakarta 库。在后台,当进行网络请求时,将执行验证,并抛出一个我们可以捕获的异常。
  • @PathVariable:这个注解用于将路径与在@RequestMapping 中声明的路径或使用的快捷方式进行绑定。例如,在 findRetroBoardById 方法中,路径/retros/{uuid}的值被绑定到 UUID 实例。因此,我们可以通过类似的方式来访问它:
  • 访问 http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2
  • 9dc9b71b-a07e-418b-b972-40225449aff2 将被设置为 UUID 实例变量。路径名称 ({uuid}) 必须与函数中声明的参数名称一致。
  • ServletUriComponentsBuilder:这是一个辅助类,用于创建 URI 并扩展到包含键和值的基本路径。在此情况下,该类在 saveRetroBoard 方法中使用,负责创建 Header 中所需的位置,并通过 ResponseEntity.create()方法调用将其设置为响应的一部分。
  • @PutMapping:该注解用于响应 HTTP PUT 方法请求,通常会将 HTTP Body 包含在请求中。在我们的例子中,它结合了特定路径(带有 URL 模式)和 HTTP Body(@RequestBody);请查看 addCardToRetroBoard 方法,它还包含了一些使用@Valid 注解的验证。
  • @ResponseStatus:该注解用于自定义在特定控制器方法或异常处理程序中返回的 HTTP 状态码。有时,无论操作是什么,我们都可以返回特定的 HTTP 状态码。在这种情况下,在 deleteCardFromRetroBoard 方法中,我们返回 HttpStatus.NO_CONTENT 状态(204 代码)。
  • @DeleteMapping:该注解用于处理 HTTP DELETE 请求方法。在我们的代码中,我们还通过@PathVariable 声明了一个 URL 路径。
  • @ExceptionHandler:该注解用于定义处理控制器方法执行过程中抛出的特定异常的方法(或在@ControllerAdvice 类中进行全局异常处理)。当发生与@ExceptionHandler 方法参数中声明的异常类型匹配的异常时,Spring MVC 会调用该方法来处理异常并生成相应的响应。有时,我们需要以特定的方式回应应用程序中发生的任何错误或异常。默认情况下,Spring 会返回由嵌入式 Tomcat 服务器抛出的“服务器内部错误”(或其他错误),并且不会提供太多关于发生了什么的详细信息。 为了避免这种情况,我们可以捕获这些类型的错误,例如当我们没有有效数据(来自 RetroBoard 或 Card)时发生的错误,并解释发生了什么。我们可以注释一个处理此问题的方法。在我们的代码中,有一个 handleValidationExceptions 方法。我们的@ExceptionHandler 将 MethodArgumentNotValidException 类声明为参数,这意味着只有在验证过程中抛出此错误时,它才会被触发。
  • 方法参数无效异常:当进行@Valid 级联验证时,会抛出此异常。handleValidationExceptions 方法将生成包含错误信息的响应,并返回一个将被转换为 JSON 格式的 Map,这样更容易理解发生了什么。

我们刚刚实现了一个基于 Spring Boot 的 Web 应用程序。在您继续进行测试之前,请再仔细查看一下 RetroBoardController 类,关注每一个细节。

测试我的怀旧应用

为了测试我们的应用程序,我们首先要测试 RetroBoardService 类,这是应用程序的核心。因此,在测试文件夹中,请用列表 3-13 中的代码替换 MyretroApplicationTests 的内容。

package com.apress.myretro;
import com.apress.myretro.board.Card;
import com.apress.myretro.board.CardType;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.exception.CardNotFoundException;
import com.apress.myretro.exception.RetroBoardNotFoundException;
import com.apress.myretro.service.RetroBoardService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Collection;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@SpringBootTest
class MyretroApplicationTests {
    @Autowired
    RetroBoardService service;
    UUID retroBoardUUID = UUID.fromString("9DC9B71B-A07E-418B-B972-40225449AFF2");
    UUID cardUUID = UUID.fromString("BB2A80A5-A0F5-4180-A6DC-80C84BC014C9");
    UUID mehCardUUID = UUID.fromString("775A3905-D6BE-49AB-A3C4-EBE287B51539");
    @Test
    void saveRetroBoardTest(){
        RetroBoard retroBoard = service.save(RetroBoard.builder().name("Gathering 2023").build());
        assertThat(retroBoard).isNotNull();
        assertThat(retroBoard.getId()).isNotNull();
    }
    @Test
    void findAllRetroBoardsTest(){
        Iterable<RetroBoard> retroBoards = service.findAll();
        assertThat(retroBoards).isNotNull();
        assertThat(retroBoards).isNotEmpty();
    }
    @Test
    void cardsRetroBoardNotFoundTest() {
        assertThatThrownBy(() -> {
            service.findAllCardsFromRetroBoard(UUID.randomUUID());
                }).isInstanceOf(RetroBoardNotFoundException.class);
    }
    @Test
    void  findRetroBoardTest(){
        RetroBoard retroBoard = service.findById(retroBoardUUID);
        assertThat(retroBoard).isNotNull();
        assertThat(retroBoard.getName()).isEqualTo("Spring Boot 3.0 Meeting");
        assertThat(retroBoard.getId()).isEqualTo(retroBoardUUID);
    }
    @Test
    void findCardsInRetroBoardTest(){
        RetroBoard retroBoard = service.findById(retroBoardUUID);
        assertThat(retroBoard).isNotNull();
        assertThat(retroBoard.getCards()).isNotEmpty();
    }
    @Test
    void addCardToRetroBoardTest(){
        Card card = service.addCardToRetroBoard(retroBoardUUID, Card.builder()
                        .comment("Amazing session")
                        .cardType(CardType.HAPPY)
                .build());
        assertThat(card).isNotNull();
        assertThat(card.getId()).isNotNull();
        RetroBoard retroBoard = service.findById(retroBoardUUID);
        assertThat(retroBoard).isNotNull();
        assertThat(retroBoard.getCards()).isNotEmpty();
    }
    @Test
    void findAllCardsFromRetroBoardTest() {
        Iterable<Card> cardList = service.findAllCardsFromRetroBoard(retroBoardUUID);
        assertThat(cardList).isNotNull();
        assertThat(((Collection) cardList).size()).isGreaterThan(3);
    }
    @Test
    void removeCardsFromRetroBoardTest(){
        service.removeCardFromRetroBoard(retroBoardUUID,cardUUID);
        RetroBoard retroBoard = service.findById(retroBoardUUID);
        assertThat(retroBoard).isNotNull();
        assertThat(retroBoard.getCards()).isNotEmpty();
        assertThat(retroBoard.getCards()).hasSizeLessThan(4);
    }
    @Test
    void findCardByIdInRetroBoardTesT(){
        Card card = service.findCardByUUIDFromRetroBoard(retroBoardUUID,mehCardUUID);
        assertThat(card).isNotNull();
        assertThat(card.getId()).isEqualTo(mehCardUUID);
    }
    @Test
    void notFoundCardInRetroBoardTest(){
        assertThatThrownBy(() -> {
            service.findCardByUUIDFromRetroBoard(retroBoardUUID,UUID.randomUUID());
        }).isInstanceOf(CardNotFoundException.class);
    }
}

列表 3-13 源代码:src/test/java/com/apress/myretro/MyRetroApplicationTests.java

正如你所看到的,MyretroApplicationTests 类仅测试服务。在这个测试中,我们使用了 AssertJ 库中的一些简单断言。为了验证 My Retro App 是否返回正确的异常,我们可以使用 assertThatThrownBy 方法。你可以通过你的 IDE 或运行以下命令来执行这些测试:

./gradlew clean test
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
> Task :compileJava
> Task :test
MyretroApplicationTests > saveRetroBoardTest() PASSED
MyretroApplicationTests > findAllRetroBoardsTest() PASSED
MyretroApplicationTests > findRetroBoardTest() PASSED
MyretroApplicationTests > removeCardsFromRetroBoardTest() PASSED
MyretroApplicationTests > cardsRetroBoardNotFoundTest() PASSED
MyretroApplicationTests > notFoundCardInRetroBoardTest() PASSED
MyretroApplicationTests > findCardsInRetroBoardTest() PASSED
MyretroApplicationTests > addCardToRetroBoardTest() PASSED
MyretroApplicationTests > findCardByIdInRetroBoardTesT() PASSED
MyretroApplicationTests > findAllCardsFromRetroBoardTest() PASSED
BUILD SUCCESSFUL in 10s
5 actionable tasks: 5 executed

现在,我们真正想做的是测试我们的网络 API,对吗?有许多工具可以帮助我们实现这一点,比如 PostMan (https://www.postman.com) 和 Insomnia (https://insomnia.rest)。但我想向你介绍一个名为 REST Client 的工具,它可以直接帮助我们进行 HTTP 请求。这个工具有开源版本和付费版本,风格一致。付费版本包含在 IntelliJ IDEA 企业版中,而如果你使用 VS Code,则需要从插件选项卡安装 REST Client (v0.25.x),这个插件的作者是 Huachao Mao(见图 3-3)。

我将演示如何使用 VS Code REST Client 插件来测试 My Retro App。请下载插件并创建 src/http 文件夹,然后添加一个名为 myretro.http 的文件,内容如列表 3-14 所示。

### Get All Retro Boards
GET http://localhost:8080/retros
Content-Type: application/json
### Get Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2
Content-Type: application/json
### Get All Cards from Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards
Content-Type: application/json
### Get Single Card from Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards/bb2a80a5-a0f5-4180-a6dc-80c84bc014c9
Content-Type: application/json
### Create a Retro Board
POST http://localhost:8080/retros
Content-Type: application/json
{
  "name": "Spring Boot Conference"
}
### Add Card to Retro
PUT http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards
Content-Type: application/json
{
  "comment": "We are back in business",
  "cardType": "HAPPY"
}
### Delete Card from Retro
DELETE http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards/bb2a80a5-a0f5-4180-a6dc-80c84bc014c9
Content-Type: application/json

文件 3-14src/http/myretro.http

列表 3-14 展示了实现我的复古应用所需的所有调用。在每个调用的顶部,您应该启用“发送请求”链接;点击后,您将在另一个窗口中查看响应。请尝试每个调用。

使用这种类型的客户端非常简单。这个插件的功能远超这段简要介绍。如果你想了解更多关于如何使用它的信息,请访问 https://github.com/Huachao/vscode-restclient。

相关推荐

Java项目宝塔搭建实战MES-Springboot开源MES智能制造系统源码

大家好啊,我是测评君,欢迎来到web测评。...

一个令人头秃的问题,Logback 日志级别设置竟然无效?

原文链接:https://mp.weixin.qq.com/s/EFvbFwetmXXA9ZGBGswUsQ原作者:小黑十一点半...

实战!SpringBoot + RabbitMQ死信队列实现超时关单

需求背景之为什么要有超时关单原因一:...

火了!阿里P8架构师编写堪称神级SpringBoot手册,GitHub星标99+

Springboot现在已成为企业面试中必备的知识点,以及企业应用的重要模块。今天小编给大家分享一份来着阿里P8架构师编写的...

Java本地搭建宝塔部署实战springboot仓库管理系统源码

大家好啊,我是测评君,欢迎来到web测评。...

工具尝鲜(1)-Fleet构建运行一个Springboot入门Web项目

Fleet是JetBrains公司推出的轻量级编辑器,对标VSCode。该款产品还在公测当中,具体下载链接如下JetBrainsFleet:由JetBrains打造的下一代IDE。想要尝试的...

SPRINGBOOT WEB 实现文件夹上传(保留目录结构)

网上搜到的SpringBoot的代码不多,完整的不多,能用的也不多,基本上大部分的文章只是提供了少量的代码,讲一下思路,或者实现方案。之前一般的做法都是使用HTML5来做的,大部都是传文件的,传文件夹...

Java项目本地部署宝塔搭建实战报修小程序springboot版系统源码

大家好啊,我是测评君,欢迎来到web测评。...

新年IT界大笑料“工行取得基于SpringBoot的web系统后端实现专利

先看看专利描述...

看完SpringBoot源码后,整个人都精神了

前言当读完SpringBoot源码后,被Spring的设计者们折服,Spring系列中没有几行代码是我们看不懂的,而是难在理解设计思路,阅读Spring、SpringMVC、SpringBoot需要花...

阿里大牛再爆神著:SpringBoot+Cloud微服务手册

今天给大家分享的这份“Springboot+Springcloud微服务开发实战手册”共有以下三大特点...

WebClient是什么?SpringBoot中如何使用WebClient?

WebClient是什么?WebClient是SpringFramework5引入的一个非阻塞、响应式的Web客户端库。它提供了一种简单而强大的方式来进行HTTP请求,并处理来自服务器的响应。与传...

SpringBoot系列——基于mui的H5套壳APP开发web框架

  前言  大致原理:创建一个main主页面,只有主页面有头部、尾部,中间内容嵌入iframe内容子页面,如果在当前页面进行跳转操作,也是在iframe中进行跳转,而如果点击尾部按钮切换模块、页面,那...

在Spring Boot中使用 jose4j 实现 JSON Web Token (JWT)

JSONWebToken或JWT作为服务之间安全通信的一种方式而闻名。...

Spring Boot使用AOP方式实现统一的Web请求日志记录?

AOP简介AOP(AspectOrientedProgramming),面相切面编程,是通过代码预编译与运行时动态代理的方式来实现程序的统一功能维护的方案。AOP作为Spring框架的核心内容,通...

取消回复欢迎 发表评论: