四.Springboot篇

一句话定性:最简单的方式,快速整合所有技术栈

img

1.快速入门

1.SpringBoot特性

  • SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用;
  • 大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术
  • 特性:
    • 快速创建独立 Spring 应用
    • 直接嵌入Tomcat、Jetty or Undertow
    • 提供可选的 starter,简化应用整合
    • 按需自动配置 Spring 以及 第三方库
    • 提供生产级特性:如 监控指标、健康检查、外部化配置等
    • 无代码生成、无xml; 都是基于自动配置技术
  • 总结:
    • 简化开发,简化配置,简化整合,简化部署,简化监控,简化运维

2.SpringBoot特性 - 快速部署

1
2
3
4
5
6
7
8
9
<!--    SpringBoot应用打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

打包:mvn clean package

运行:java -jar demo.jar

3.Springboot-依赖管理

详细见JavaWeb+Ai(下)

依赖管理的底层就是maven的依赖传递

场景启动器

img

img

4.自动配置

img

完整流程-源码:导入场景会导入一堆自动配置类,这些配置类导入基于条件注解一堆组件

img

完整流程-源码:自动配置类给容器中放组件、组件属性来自于属性类、属性类绑定配置文件

img

  • 核心流程总结:
    • 1: 导入 starter,就会导入autoconfigure 包。
    • 2: autoconfigure 包里面 有一个文件 META-INF**/spring/org.springframework.boot.autoconfigure.AutoConfiguration****.imports**,里面指定的所有启动要加载的自动配置类
    • 3: @EnableAutoConfiguration 会自动的把上面文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration 是有条件注解进行按需加载
    • 4: xxxAutoConfiguration 给容器中导入一堆组件,组件都是从 xxxProperties 中提取属性值
    • 5: xxxProperties 又是和配置文件进行了绑定
  • 效果:导入starter、修改配置文件,就能修改底层行为。

img

2基础使用

1.@ConfigurationProperties属性绑定

  • 将容器中任意组件的属性值和配置文件的配置项的值进行绑定
    • 1、给容器中注册组件(@Component、@Bean)
    • 2、使用 @ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定

对比@ConfigurationProperties和@Value

@ConfigurationProperties

  • 作用: @ConfigurationProperties 用于将配置文件中的属性(如application.properties或application.yml中的属性)绑定到一个POJO(Plain Old Java Object)上。通过这个注解,Spring会自动将配置文件中的属性映射到POJO类的字段上。
  • 特点: 属性的值是在启动时从配置文件中读取的,这些属性值不会经过SpEL(Spring Expression Language)表达式的计算。它们是直接从配置文件中读取并注入到POJO中的。

@Value

  • 作用: @Value 注解用于将配置属性注入到Spring Bean中的字段、方法或构造函数参数。它支持使用SpEL表达式,这意味着可以在注解的值中使用Spring表达式语言来动态计算属性值。
  • 特点: @Value 可以用于直接注入配置值,也可以利用SpEL表达式来处理更复杂的动态计算或逻辑。

参考文档:

@ConfigurationProperties & @EnableConfigurationProperties 注解-CSDN博客

2.YAML

  • 大小写敏感
  • 键值对写法 k: v,使用空格分割k,v
  • 使用缩进表示层级关系
    • 缩进时不允许使用Tab键,只允许使用空格。换行
    • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • # 表示注释,从这个字符一直到行尾,都会被解析器忽略。
  • Value支持的写法
    • 对象:键值对的集合,如:映射(map)/ 哈希(hash) / 字典(dictionary)
    • 数组:一组按次序排列的值,如:序列(sequence) / 列表(list)
    • 字面量:单个的、不可再分的值,如:字符串、数字、bool、日期

img

3.启动Spring应用的其他方式

  • 自定义 banner
  • 自定义 SpringApplication
    • new SpringApplication
    • new SpringApplicationBuilder
1
2
3
4
5
6
7
8
9
        SpringApplicationBuilder builder = new SpringApplicationBuilder();

//链式调用
builder
.sources(Springboot01DemoApplication.class)
.bannerMode(Banner.Mode.CONSOLE)
.environment(null)
// .listeners(null)
.run(args);

3.日志

1.简介

•规范:项目开发不要写System.out.println(),用日志记录信息

•SpringBoot 默认使用 slf4j + logback

img

2.日志入门

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
41
42
43
44
45
46
47
48
49
50
51
52
@Slf4j
@SpringBootTest
public class LogTest {
//1、获取一个日志记录器
// Logger logger = LoggerFactory.getLogger(LogTest.class);


@Test
void test02() throws InterruptedException {
int i = 0;
// while (true) {
// log.info("info日志.... 数字:【{}】,ok={}", i++, "哈哈");
// Thread.sleep(3);
// }
}

@Test
void test01(){


// System.out.println("djkaljdalkjdklaj");
//2、记录日志
//级别:由低到高:ALL -- TRACE -- DEBUG -- INFO -- WARN -- ERROR -- OFF
//越打印,越粗糙; 日志有一个默认级别(INFO);只会打印这个级别之上的所有信息;

log.trace("追踪日志......");
if("1".equals(log)){
log.debug("调试日志.......");
//业务流程

try {
//关键点
log.info("信息日志........");

//容易出问题点
// aa.bb(){
// log.warn("警告日志........");
// };

}catch (Exception e){
log.error("错误日志......"+e.getMessage());
}

}




//格式: 时间 级别 进程id --- 项目名 --- 线程名 --- 当前类名: 日志内容

}
}

日志格式

  • 默认输出格式:
    • 时间和日期:毫秒级精度
    • 日志级别:ERROR, WARN, INFO, DEBUG, or TRACE.
    • 进程 ID
    • —: 消息分割符
    • 线程名: 使用[]包含
    • Logger 名: 通常是产生日志的类名
    • 消息: 日志记录的内容 注意: logback 没有FATAL级别,对应的是ERROR
  • 注意: logback 没有FATAL级别,对应的是ERROR

日志级别

由低到高:ALL,TRACE, DEBUG, INFO, WARN, ERROR,FATAL,OFF;

只会打印指定级别及以上级别的日志

  • ALL:打印所有日志
  • TRACE:追踪框架详细流程日志,一般不使用
  • DEBUG:开发调试细节日志
  • INFO:关键、感兴趣信息日志
  • WARN:警告但不是错误的信息日志,比如:版本过时
  • ERROR:业务错误日志,比如出现各种异常
  • FATAL:致命错误日志,比如jvm系统崩溃
  • OFF:关闭所有日志记录

不指定级别的所有类,都使用 root 指定的级别作为默认级别

SpringBoot日志默认级别是 INFO

修改日志级别-在配置文件中修改

1
2
3
4
logging:
level:
com.example.mypackage: DEBUG
root: WARN

日志分组

将相关的logger分组在一起,统一配置。SpringBoot 支持分组统一配置

1
2
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace

SpringBoot 预定义两个组

组名 范围
web org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

日志-文件输出

SpringBoot 默认只把日志写在控制台,如果想额外记录到文件,可以在application.properties中添加 logging.file.name 或 logging.file.path 配置项。两个都配置以文件名为准

logging.file.name logging.file.path 示例 效果
未指定 未指定 仅控制台输出
指定 未指定 my.log 写入指定文件。可以加路径
未指定 指定 /var/log 写入指定目录,文件名为spring.log
指定 指定 以logging.file.name为准
1
2
3
4
5
# 两个都配置以文件名为准
logging.file.name=boot.log
#logging.file.path=D://aaa.log

#logging.logback.rollingpolicy.max-file-size=2MB

日志-文件归档与滚动切割

  • 归档:每天的日志单独存到一个文档中。
  • 切割:每个文件10MB,超过大小切割成另外一个文件。

默认滚动切割与归档规则如下:

配置项 描述
logging.logback.rollingpolicy.file-name-pattern 日志存档的文件名格式 默认值:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
logging.logback.rollingpolicy.clean-history-on-start 应用启动时是否清除以前存档;默认值:false
logging.logback.rollingpolicy.max-file-size 每个日志文件的最大大小;默认值:10MB
logging.logback.rollingpolicy.total-size-cap 日志文件被删除之前,可以容纳的最大大小(默认值:0B)。设置1GB则磁盘存储超过 1GB 日志后就会删除旧日志文件
logging.logback.rollingpolicy.max-history 日志文件保存的最大天数;默认值:7

日志-引入框架自己的日志配置文件

通常我们配置 application.properties 就够了。当然也可以自定义。比如:

日志系统 自定义
Logback logback-spring.xml / logback.xml
Log4j2 log4j2-spring.xml / log4j2.xml
JDK (Java Util Logging) logging.properties

日志-切换日志组合

img

3.日志系统 – 最佳实践

  • 1、导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志
  • 2、修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xml,log4j2-spring.xml
  • 3、如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
  • 4、业务中使用slf4j-api记录日志。不要再 sout 了

4.进阶使用

1.profiles 环境隔离-@Profile

  • 环境隔离能力;快速切换开发、测试、生产环境

  • 步骤:

      1. 标识环境:指定哪些组件、配置在哪个环境生效
      • @Profile 标记组件生效环境
      1. 切换环境:这个环境对应的所有组件和配置就应该生效
    • 激活环境:

      • 配置文件:spring.profiles.active=production,hsqldb
      • 命令行:java -jar demo.jar --spring.profiles.active=dev,hsqldb
    • 环境包含:

      • spring.profiles.include[0]=common
      • spring.profiles.include[1]=local
    • 生效的配置 = 默认环境配置 + 激活的环境 + 包含的环境配置

    • 注意:激活的配置优先级高于默认配置 profile高于application

  • 项目里面这么用

    • 基础的配置mybatis、log、xxx:写到包含环境中
    • 需要动态切换变化的 db、redis:写到激活的环境中

循环引用

img

Profiles环境隔离 - 分组

  • 创建 prod 组,指定包含 db 和 mq 配置
    • spring.profiles.group.prod[0]=db
    • spring.profiles.group.prod[1]=mq
  • 使用 --spring.profiles.active=prod ,激活prod,db,mq配置文件

Profiles环境隔离 - 配置文件

  • application-{profile}.properties 可以作为指定环境的配置文件
  • 激活这个环境,配置就会生效。最终生效的所有配置是
    • application.properties:主配置文件,任意时候都生效
    • application-{profile}.properties:指定环境配置文件,激活指定环境生效
  • profile优先级 > application

2.外部化配置

外部配置优先于内部配置

属性占位符:

app.name=MyApp app.description=${app.name} Hello

激活优先 外部优先

img

3.断言

测试注解

  • @Test :表示方法是测试方法。
  • @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest :表示方法可重复执行,下方会有详细介绍
  • @DisplayName :为测试类或者测试方法设置展示名称
  • @BeforeEach :表示在每个单元测试之前执行
  • @AfterEach :表示在每个单元测试之后执行
  • @BeforeAll :表示在所有单元测试之前执行
  • @AfterAll :表示在所有单元测试之后执行
  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith :为测试类或测试方法提供扩展类引用

断言机制

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
assertArrayEquals 数组断言
assertAll 组合断言
assertThrows 异常断言
assertTimeout 超时断言
fail 快速失败
1
2
3
4
5
6
7
8
9
10
11
12
13
 @Test
void test02(){
//1、业务规定,返回hello字符串才算成功,否则就是失败
String result = helloService.sayHello();
//2、断言:判断字符串是否等于hello
// Assertions.assertEquals("hello",result,"helloservice并没有返回hello");




Assertions.assertThrows(ArithmeticException.class, () -> {
helloService.hello();
});

4.可观测性

•可观测性(Observability)指应用的运行数据,可以被线上进行观测、监控、预警等

img

  • SpringBoot 提供了 actuator 模块,可以快速暴露应用的所有指标
  • 导入: spring-boot-starter-actuator
  • 访问 http://localhost:8080/actuator;
  • 展示出所有可以用的监控端点

可观测性 - Endpoints

端点名 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表
caches 暴露可用的缓存
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。需要一个或多个Flyway组件。
health 显示应用程序运行状况信息
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件
info 显示应用程序信息
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息
端点名 描述
mappings 显示所有@RequestMapping路径列表
scheduledtasks 显示应用程序中的计划任务
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序
shutdown 使应用程序正常关闭。默认禁用
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储
heapdump 返回hprof堆转储文件
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

5.SpringBoot核心原理

1.生命周期监听

img

监听器感知生命周期

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
41
/**
* 监听到SpringBoot启动的全生命周期
*/
@Slf4j
public class MyListener implements SpringApplicationRunListener {

@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MyListener...starting...");
}

@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
log.info("MyListener...started...");
}

@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
log.info("MyListener...ready...");
}

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
log.info("MyListener...failed...");
}

@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
log.info("MyListener...environmentPrepared...");
}

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
log.info("MyListener...contextLoaded...");
}

@Override
public void contextPrepared(ConfigurableApplicationContext context) {
log.info("MyListener...contextPrepared...");
}
}
监听器 感知阶段 配置方式
BootstrapRegistryInitializer 特定阶段:引导初始化 1、META-INF/spring.factories 2、application.addBootstrapRegistryInitializer()
ApplicationContextInitializer 特定阶段:ioc容器初始化 1、META-INF/spring.factories 2、application.addInitializers()
ApplicationListener 全阶段 1、META-INF/spring.factories 2、SpringApplication.addListeners(…) 3、@Bean 或 @EventListener
SpringApplicationRunListener 全阶段 META-INF/spring.factories
ApplicationRunner 特定阶段:感知应用就绪 @Bean
CommandLineRunner 特定阶段:感知应用就绪 @Bean

最佳实践:

1、应用启动后做事:ApplicationRunner、CommandLineRunner

2、事件驱动开发:ApplicationListener

2生命周期事件

img

img

3.事件驱动开发

  • 应用启动过程生命周期事件感知(9大事件)
  • 应用运行中事件感知(无数种)
  • 事件驱动开发
    • 定义事件:
      • 任意事件:任意类可以作为事件类,建议命名 xxxEvent
      • 系统事件:继承 ApplicationEvent
    • 事件发布:
      • 组件实现 ApplicationEventPublisherAware
      • 自动注入 ApplicationEventPublisher
    • 事件监听:
      • 组件 + 方法标注@EventListener

img

img

4.SpringBoot完整项目启动流程

img

5.自动配置原理

img

6.自定义Starter

1.场景设计

  • 场景:抽取聊天机器人场景,它可以打招呼。
  • 效果:任何项目导入此starter都具有打招呼功能,并且问候语中的人名需要可以在配置文件中修改

img

EnableRobot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.robot.starter.annotation;


import com.atguigu.robot.starter.RobotAutoConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {

}

RobotController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.robot.starter.controller;



import com.atguigu.robot.starter.service.RobotService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RobotController {



@Autowired
RobotService robotService;

@GetMapping("/robot/hello")
public String sayHello(){
String msg = robotService.sayHello();
return msg;
}
}

RobotProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.atguigu.robot.starter.properties;


import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


@Component
@ConfigurationProperties(prefix = "robot")
@Data
public class RobotProperties {

private String name;
private String model;
}

RobotService

1
2
3
4
5
6
package com.atguigu.robot.starter.service;

public interface RobotService {

public String sayHello();
}

RobotServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.robot.starter.service.impl;


import com.atguigu.robot.starter.properties.RobotProperties;
import com.atguigu.robot.starter.service.RobotService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class RobotServiceImpl implements RobotService {


@Autowired
RobotProperties robotProperties;

@Override
public String sayHello() {
return "我是机器人【"+robotProperties.getName()+"】,使用底层大模型:【"+robotProperties.getModel()+"】;我们开始聊天吧";
}
}

RobotAutoConfiguration

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
package com.atguigu.robot.starter;


import com.atguigu.robot.starter.controller.RobotController;
import com.atguigu.robot.starter.properties.RobotProperties;
import com.atguigu.robot.starter.service.RobotService;
import com.atguigu.robot.starter.service.impl.RobotServiceImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@EnableConfigurationProperties(RobotProperties.class)
@Configuration //把这个场景要用的所有组件导入到容器中
public class RobotAutoConfiguration {

@Bean
public RobotController robotController() {
return new RobotController();
}

@Bean
public RobotService robotService() {
return new RobotServiceImpl();
}

}

2.基础抽取

    1. 创建自定义starter项目,引入spring-boot-starter基础依赖
    1. 编写模块功能,引入模块所有需要的依赖。
    1. 编写xxxAutoConfiguration自动配置类,帮其他项目导入这个模块需要的所有组件
1
2
3
4
5
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>robot-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

在需要robot这个starter的项目中

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
package com.atguigu.boot;

import com.atguigu.robot.starter.RobotAutoConfiguration;
import com.atguigu.robot.starter.annotation.EnableRobot;
import com.atguigu.robot.starter.controller.RobotController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;


/**
* spring-boot-start-web
*
* 为什么导入 robot-spring-boot-starter ,访问 controller 是 404?
* 原因:主程序只会扫描到自己所在的包及其子包下的所有组件
*
* 自定义starter:
* 1、第一层抽取:编写一个自动配置类,别人导入我的starter,
* 无需关心需要给容器中导入哪些组件,只需要导入自动配置类,
* 自动配置类帮你给容器中导入所有这个场景要用的组件
* @Import(RobotAutoConfiguration.class)
* 2、第二层抽取:只需要标注功能开关注解。@EnableRobot
* 3、第三层抽取:只需要导入starter,所有功能就绪
*/
@EnableRobot
@SpringBootApplication
public class Springboot02DemoApplication {

public static void main(String[] args) {
SpringApplication.run(Springboot02DemoApplication.class, args);
}

}

在application.properties中

1
2
3
4
5
spring.application.name=springboot-02-demo

# 配置依然在别人这里
robot.name=小哈哈
robot.model=chatgpt666

捋一下流程:02-demo这个项目导入了robot的starter,项目一启动的时候还导入了robot项目的自动配置类(@EnableRobot中标了@Import(RobotAutoConfiguration.class)),这个自动配置类开启了配置文件绑定,配置文件正好配置类相应的信息,跟RobotProperties绑好了,然后在自动配置类中在容器中还放了组件controller和service

如何实现第三层抽取

img

在resource中创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里面写:

1
com.atguigu.robot.starter.RobotAutoConfiguration

测试:

img

7.SpringBoot总结

需要掌握的源码

1.SpringBoot 自动配置原理

2.SpringMVC DispatcherServlet 流程

3.Spring IOC容器(三级缓存机制)

4.Spring 事务原理(TransactionMapper、TransactionInterceptor)

需要掌握的开发技巧:RESTful、CRUD

SpringBoot篇完结