Skip to main content

SpringBoot 构建 RestAPI 应用

Spring 是 Java 应用框架,SpringMVC 只是其中一个能提供 web 服务的模块。

SpringBoot 是 Spring 的常用模块封装,类似项目的脚手架。

RestAPI 是内容传输格式,客户端和浏览器约定用 JSON 格式交互数据,适用于后台接口开发,前后端分离。

本实验用 SpringBoot 的 MVC 整合模块开发一个 RestAPI 应用,Gradle 作为依赖管理工具。

实验工具版本说明

工具 版本 说明
Gradle 7.2 依赖管理工具
springboot gradle plugin 2.4.8 SpringBoot Gradle 插件
springboot starter web 2.5.5 SpringBoot web 模块整合
Java 11 任意 Java 11 发行版,推荐 AdoptOpenJDK

用 Gradle 新建一个 Java 应用项目

项目根目录执行 gradle init,Gradle 询问一些东西,然后生成项目基本结构.

Gradle init 指引
> gradle init

# 询问项目类型: 实验选择 application
Select type of project to generate: 选择 application

# 询问开发语言: 实验选择 Java
Select implementation language: 

# 询问是否同时开发库: 实验选择单应用项目 (no - only one application project)
Split functionality across multiple subprojects?: no - only one application project

# 询问构建语言: 实验选择 Groovy
Select build script DSL:

# 询问测试工具: 默认即可
Select test framework:

# 项目名称: 默认即可
Project name:

# 应用包名: 实验为 org.luo3.springrestdemo
Source package:

init 完毕,项目根目录会有这些文件:

gradle init 后的项目结构

./gradlew tasks 可以查看所有可用的构建任务。

管理项目依赖

打开 app/build.gradle

添加 SpringBoot Gradle 插件

导入后就可以用 bootRun、bootJar 启动或打包应用了。

bootRun 和 run 的区别?

bootRun 是 SpringBoot Gradle 提供的,run 是 application 插件提供的。它们都可以读取 application { mainClass = XXX } 作为程序入口类。

bootXXX 只是方便 Spring 应用打包,运行的任务,用这些任务可以省去很多麻烦的配置步骤。

plugins 处增加 SpringBoot Gradle 插件。

plugins {
  // ...

  // [添加] SpringBoot Gradle 插件
  id "org.springframework.boot" version "2.4.8"
}

添加 SpringBoot Starter Web 依赖包

dependencies 处增加依赖。

dependencies {
    // [增加] SpringBoot Starter Web 模块整合包
    implementation 'org.springframework.boot:spring-boot-starter-web:2.5.5'

    // ...
}

现在已经可以在 java 代码引用 spring 的依赖了。

入口类启动 Spring

Gradle init application 会自动生成一个 App.java 作为程序入口。

将程序入口改成用 Spring 启动:

  • @SpringBootApplication 标记入口类
  • static void main 方法启动 Spring
@SpringBootApplication
public class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }
}

在项目根目录执行 ./gradlew bootRun 即可启动 web 服务器。

./gradlew bootRun

默认端口 :8080,因为没有 Controller,所以请求任意路径都会报 404。

> curl localhost:8080
{"timestamp":"2021-10-14T02:26:00.038+00:00","status":404,"error":"Not Found","path":"/"}

添加 MVC 控制器 Controller

App.java 同级目录新建控制器类 AppController.java

基本路由映射

// @RestController 标记为 RestAPI 控制器
@RestController
public class AppController {

  // 基本 HTTP GET
  @GetMapping("/hi")
  public Map<String, String> handleBaseHTTPGET () {
    return new HashMap<>(){{
      put("msg", "hi");
    }};
  }

}

运行服务器,访问 localhost:8080/hi

# 测试基本 HTTP GET
curl -XGET localhost:8080/hi
# 结果 {"msg":"hi"}
扩展: 处理其他 HTTP method
@RestController
public class AppController {

  // 处理 POST
  // curl -X POST localhost:8080/post_hi
  @PostMapping("/post_hi")
  public Map<String, String> handlePost () {
    return new HashMap<>(){{
      put("msg", "hi from POST");
    }};
  }

  // 接受多个 method,比如 GET, POST
  // curl -X POST localhost:8080/getpost_hi
  // curl -X GET localhost:8080/getpost_hi
  // curl localhost:8080/getpost_hi
  @RequestMapping(path = "/getpost_hi", method = {RequestMethod.GET, RequestMethod.POST})
  public Map<String, String> handleGETPOST () {
    return new HashMap<>(){{
      put("msg", "hi from GET | POST");
    }};
  }

}
扩展: 配置路由前缀
@RestController
@RequestMapping("/sub")
public class SubRouteController {
  
  // 子路由,实际路径 /sub/hi
  @GetMapping("/hi")
  public Map<String, String> handleSubRoute () {
    return new HashMap<>(){{
      put("msg", "hi, sub route");
    }};
  }

}

# 测试子路由
curl -XGET localhost:8080/sub/hi
# 结果 {"msg":"hi, sub route"}
@RestController
public class AppController {

  // URL Query
  @GetMapping("/hello")
  public Map<String, String> handleURLQuery (@RequestParam("name") String name) {
    return new HashMap<>(){{
      put("msg", "hi, " + name);
    }};
  }

}

运行效果

# 测试 URL Query
curl -XGET localhost:8080/hello?name=luo
# 返回 {"msg":"hi, luo"}

处理 JSON body

@RestController
public class AppController {

  // JSON body
  @PostMapping(
    path = "/hello",
    consumes = "application/json", 
    produces = "application/json"
  )
  public Map<String,Object> handleJSONBody (@RequestBody Map<String,Object> jsonObject) {
    String name = jsonObject.get("name").toString();
    return new HashMap<>(){{
      put("msg", "welcome back, " + name);
    }};
  }

}

运行效果

# 测试 JSON 内容
curl -XPOST \
-H 'Content-Type: application/json' \
-d '{"name": "luo"}' \
localhost:8080/hello
# 结果 {"msg": "welcome back, luo"}
将 body 绑定到 Java 对象
@RestController
public class AppController {

  public static class Person {
    // 私有成员
    private String name;
    private String nickname;
    // getter
    // 建议用 lombok 库,@Getter 自动生成成员 getter
    public String getName() {
      return name;
    }
    public String getNickname() {
      return nickname;
    }
    // setter
    // 建议用 lombok 库,@Setter 自动生成成员 setter
    public void setName(String name) {
      this.name = name;
    }
    public void setNickname(String nickname) {
      this.nickname = nickname;
    }
  }

  @PostMapping(
    path = "/post",
    consumes = "application/json", 
    produces = "application/json"
  )
  public Person post (@RequestBody Person personObject) {
    return personObject; // 将请求的 JSON 原样返回
  }

}

curl -X POST \
-H 'Content-Type: application/json' \
-d '{ "name": "guest" }' \
--url 'localhost:8080/post' 
# 返回 {"name":"guest","nickname":null}

路径变量 (占位符)

@RestController
public class AppController {

  // 占位符
  @GetMapping("/profile/{slug}")
  public Map<String, String> handlePathVariable (@PathVariable("slug") String slugValue) {
    return new HashMap<>(){{
      put("msg", "profile of user: " + slugValue);
    }};
  }

}

运行结果

# 测试占位符变量
curl -XGET localhost:8080/profile/luo
# 结果 {"msg": "profile of user: luo"}

直接拿整个请求

@RestController
public class AppController {

  @GetMapping("/get_scheme")
  public String rawRequest (HttpServletRequest request) {
    return request.getScheme();
  }

}

运行结果

curl 'http://localhost:8080/get_scheme'
// 返回 http

应用打包成 Jar

执行 bootJar 任务,会在 app/build/libs 目录生成 jar。

./gradlew bootJar

jar 可以独立执行

java -jar app.jar

附录与引用

Java 开发辅助库: lombok

getter,setter 在 SpringMVC 或其他数据交换情形经常需要用到,lombok 的一些库省去了一堆本来要手写的 getter, setter 代码。

用 lombok 前
class Bottle {
  private int capacity;
  private String name;
  private boolean isPlastic;
  // 手写 getter
  public int getCapacity () { ... }
  public int getName () { ... }
  public int getIsPlastic () { ... }
  // 手写 setter
  public void setCapacity (int cap) { ... }
  public void setName (String name) { ... }
  public void setIsPlastic (boolean flag) { ... }
  // 手写 toString
  public String toString() { return "" + "" + ""; }
}
用 lombok 后
@Getter // 自动生成 getter
@Setter // 自动生成 setter
@ToString // 还可以生成 toString,拼接成员变量
class LombokBottle {
  private int capacity; // 只要定义私有成员即可
  private String name;
  private boolean isPlastic;
}

但是 lombok 不用显式声明方法名称,Java 编译是肯定不给过的。像这样:

error: cannot find symbol

让 Java 无视这些 symbol 检查就可以了。

Gradle 示例
dependencies {
  // lombok 依赖
  implementation 'org.projectlombok:lombok:1.18.20'
  // lombok 声明检查,加上后,编译通过
  annotationProcessor 'org.projectlombok:lombok:1.18.20'
}

@ResponseBody 的作用 (StackOverflow)

看到很多项目的 RestAPI Controller 都能见到 @ResponseBody

public class Controller {
  @GetMapping("/")
  @ResponseBody
  public String hi () { ... }
}

@ResponseBody 标记是告诉 Spring MVC,这个方法的返回值,直接编码放在 HTTP body 里面。

本实验项目归档

下载 | Download