SpringBoot学习笔记(1)- NoTe0x002

Baobao0824

原链接

  1. https://www.cnblogs.com/best/p/16098630.html
  2. https://www.cnblogs.com/best/p/16104989.html

写在前面

这一系列是我在学习他人的优质课程(博客文章、视频课程等)时所做的学习笔记,根据我自身的水平来进行学习,同时会进行思维的发散,补充原文中没有提到或者有错误的地方。同时也会进行经常性的更新和整理,让它和我的当下的状态更加契合。

本期选取的依旧是一个系列的博客。相比于上期的langchain,springboot显得就系统规范多了,同时也因为时间久,所以资料也很多,希望不会走上一期的老路。同时因为我在学习后端,正好可以先从springboot下手,搞清楚他到底完成了什么,然后可以自顶向下看看它下面的jdbc之类是如何运行的。

(一)SpringBoot概要与快速入门

简介&第一个SpringBoot应用

SpringBoot是Spring光甲的脚手架,因此使用了它之后,就能够快速创建独立的、生产级别的Spring应用。它基于“约定优于配置”的思想,即:框架提供合理的默认约定,只有偏离约定时才需要手动配置。可以让开发人员全身心投入到业务逻辑的代码编写中。

Spring Boot给了我们一个项目模板生成网站,在这个网站里可以选择包管理器、java版本、项目基本信息等,还可以添加所需要的依赖,然后就可以直接下载生成好的项目代码、或者直接在在线网站中查看。

项目结构

点击generate直接生成代码,或者点击explore查看项目结构和代码。当然由于我之前已经创建了一个Java24的项目,我这里就不用他的了。

一个SpringBoot项目都是由若干个controller组成的,每个controller类都可以包括若干的方法,使其对应任意一个接口。例如我们规定接口/hi,让其返回问候语。

1
2
3
4
5
6
7
8
9
10
11
12
package org.baobao.hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController //RestController告诉Spring将结果字符串直接呈现给调用者
public class HelloController {
@GetMapping("/hi") //访问的就是localhost:8080/hi
public String hello(){
return "Hello Spring Boot!";
}
}

而SpringBoot的程序入口是在Application类中的main方法里。因此我们也把入口文件写好。

1
2
3
4
5
6
7
8
9
10
11
package org.baobao.hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // 注解,让SpringBoot和idea都能够识别
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class,args);
}
}

在SpringBoot中,注解用到的地方是很多的。不像@Override这种只给编辑器看的检查型注解,在SpringBoot项目中的注解全都是功能型注解。如果不写SpringBootApplication的话,那么我们直接运行就会出现错误。run()函数中的第一个参数是HelloApplication类,传入这个参数的作用是为了让SpringApplication能够根据这个类扫描到对应的包名和注解,同时递归遍历当前目录和子目录下的所有Controller,如果有这个注解,那么就进行注册。

@RestController是构造型注释,其相当于ControllerResponseBody的组合体,其中@Controller可以将一个类标记为Web请求处理程序,通常与@RequestMapping一起使用。而@ResponseBody注解通常用于处理返回非HTML内容的请求,如JSON或XML。这里的“Rest”指的是Restful,是一种设计风格。

@GetMapping()注解属于@RequestMapping()注解的一种,能够提供路由信息,告诉Spring,任何对应路径的HTTP请求都应该映射到home方法。不同的HTTP请求类型都有对应的注解,所以目前很少直接使用@RequestMapping()了。

启动应用之后在浏览器中输入localhost:8080/hi就可以看到输出的内容了。

Maven打包&项目组成

虽然Maven叫做Maven,但是实际上他的命令用的是mvn。常用的mvn命令如下。

  • clean,清除之前的构建产物即target目录
  • compile,编译主代码即src/main/javatarget/classes
  • package,编译测试并打包,生成jar文件或者war文件
  • install,打包,并将自己的包安装到本地仓库即~/.m2/repository

npm或者pip等包管理器不同,maven的第三方库不需要手动下载,当我们在执行任意mvn命令的时候,maven就会先监测POM文件中所需要的依赖是否在本地,如果不在的话他就会自动下载。因此主要的工作集中在cleanpackage两步。

package命令最后会生成jar文件或者war文件。JAR文件即Java Archive,要么使用java -jar来运行,或者作为依赖(类似于Java版Minecraft中的模组安装)。而WAR文件是Web Application Archive,里面包括类文件、HTML、CSS、JS以及Web配置等,会按强制标准结构目录WEB-INF/存储,必须部署到Servlet容器,同时也没有主类和main函数,由对应的容器来管理生命周期。

刚创建好的SpringBoot项目结构非常简单,只包含下面三个文件夹:

  • src/main/java,放置程序开发代码
  • src/main/resources,放置配置文件
  • src/test/java,放置测试代码

而在下面又包含以下的主要文件

  • xxxApplication.java,应用的启动类,包含main方法
  • application.properties,配置文件
  • xxxApplicationTests.java,单元测试类

其他注解与代码

除了我们在上面讲的几个注解之外,还有@EnableAutoConfiguration注解也很重要。这个注解是直接包含在@SpringBootApplication注解里面的,他的作用是告诉SpringBoot根据项目类路径中的依赖自动配置Spring应用。例如类路径中有H2,则会自动配置内存数据库等。@SpringBootApplication注解是三个注解的组合:@Configuration@EnableAutoConfigurationComponentScan

在程序入口的main方法中,我们的主要方法是通过调用run来运行SpringBoot的SpringApplication类。在传入的时候我们要把本体所在的Application类传递给run方法,来确定哪个是主要的Spring组件,同时也要传递main函数附带的命令行参数。

SpringBoot起步依赖原理分析

每一个Maven项目都会有对应的POM文件,规定了该项目继承自哪个父项目,使用什么样的依赖,同时预定义了常用依赖的版本号,子项目就只需声明依赖而无需指定版本(当然你也可以显式指定版本)。而所有的SpringBoot应用都需要以spring-boot-starter-parent为父工程,在POM文件中这样写

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version>
</parent>

spring-boot-starter-parent中并不包含对应的第三方库,因此要在<dependency>中规定必需的依赖库。

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

实际上如果我们点开spring-boot-starter-web会找到它的POM文件,依赖管理和插件管理等已经定义好,包括了spring-bootspring-boot-autoconfigure等所以不需要开发人员自己去进行版本控制。

Vue+Axios+Spring Boot用户管理

博客原文给我们提供了一个Vue购物车示例,我们可以直接拿来用,为了是让后端的代码可以很好的演示。

然后我们创建一个User类,有两个属性usernameage。同时提供了一个静态变量users作为列表,以及重写的toString()方法等。同时User类实现了Serializable接口,这个接口不包含任何方法,作用是告诉JVM该类的对象可以被序列化和反序列化。

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
package org.baobao.user;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// 用户实体类,实现序列化接口
public class User implements Serializable {
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

// 私有属性,用户名和年龄
private String username;
private int age;
// 用户列表静态属性
public static List<User> users = new ArrayList<>();
// 上来先初始化几个静态变量拿来用
static {
users.add(new User("张三",18));
users.add(new User("李四",19));
users.add(new User("王五",91));
}

public User(){

}
public User(String username, int age){
this.username = username;
this.age = age;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}

同时创建一个用户服务类UserService。只包括一个方法 getAllUser。这里面我们用到了@Service注解。这个注解表明当前的类是在Service层的,是作为业务规则、流程编排等业务逻辑的位置,本质上是@Component的特化。而@Controller则是在Service的上层,进行接受请求和返回响应,

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.baobao.user;

import org.springframework.stereotype.Service;

import java.util.List;

// 用户服务
@Service
public class UserService {
public List<User> getAllUser(){
return User.users;
}
}

同时可以创建UserController,可以向前端提供服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.baobao.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/users")
public List<User> users(){
return userService.getAllUser();
}
}

通常情况下,我们必须要提前声明一个new UserService类的对象来用,Spring容器启动时,容器就会调用UserService类的构造函数,然后通过@Autowired注解注入进来。这样的话所有的Controller都可以共享同样的UserService对象,同时也可以将其纳入容器管理中。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">
<h2>用户管理</h2>
<table width="100%" border="1">
<tr>
<th>编号</th>
<th>姓名</th>
<th>年龄</th>
</tr>
<tr v-for="(user,index) in users">
<td>{{index+1}}</td>
<td>{{user.username}}</td>
<td>{{user.age}}</td>
</tr>
</table>
</div>
<script src="js/vue.min.js"></script>
<script src="js/axios.min.js"></script>
<script>
var app = new Vue({
el: "#app",
data: {
users: []
},
created() {
axios.get('http://localhost:8080/users', {
}).then(function (response) {
app.users=response.data;
})
.catch(function (error) {
console.log(error);
})
.then(function () {
});
}
});
</script>
</body>
</html>

这就是对应的前端页面。如果你的浏览器不能正常显示,那么可能就是出现CORS问题了。那么可以在users()前面直接加上这个注解。

1
2
3
4
5
@CrossOrigin(                                                                   
origins = "*",
allowedHeaders = "*", // 允许所有头
methods = {RequestMethod.GET, RequestMethod.OPTIONS} // 必须包含 OPTIONS
)

CORS的工作原理如下:前端在发送请求后,后端的响应报文中会包含一系列数据。例如Access-Control-Allow-Origin,允许访问的源,例如*或者localhost:3000`。那么浏览器就会检查发送端的源和返回的这个源是否匹配,如果匹配则放行,否则触发拦截。这样就能够防止恶意网站用自己的身份偷偷访问其他网站。那么我们直接让所有的源都可以来访问就行了。

同时我们可以创建一个DELETE请求的接口/delete,这个接口会接收一个int类型的参数id,然后直接返回id。@RequestParam注解用于从HTTP请求中读取参数值,调用的时候就可以发送例如/delete?id=123这样的。注意这个注解接收的值永远是字符串,和HTTP参数设计统一。

1
2
3
4
@DeleteMapping("/delete")
public int delete(@RequestParam(defaultValue = "0") int id){
return id;
}

(二)SpringBoot测试JUnit5、 SpringBoot 配置、Spring IoC与自动装配

SpringBoot测试与Junit5

SpringBoot包含了一套完整的单元测试流程,需要很多的依赖,包括Springtest和JUnit5等。我们可以在POM文件中直接添加spring-boot-starter-test,里面就包括了所有的常见包。整体上SpringBoot Test支持的测试种类大致分为如下三类。

  • 单元测试:一般面向方法,编写一般业务代码时的测试成本较大。
  • 切片测试:一般面向难于测试的边界功能,介于单元测试和功能测试之间。
  • 功能测试:一般面向某个完整的业务功能,同时也可以使用切片测试中的mock能力。

Mock是单元测试中的核心技术,其实就是造假,用一个假的对象代替真实的依赖,让测试更可控、更快、更独立。

测试类应该放到主启动类的同级包或者子包下面,因此我们在java.org.baobao.user下面写一个注解测试类,然后分析一下这种测试是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.baobao.user;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest // 为测试提供上下文环境
public class TestTest {
@Test // 声明一个测试函数
void testEqual(){
int actual =1;
Assertions.assertEquals(1,actual);
}
}

除了@SpringBootTest注解之外的测试相关的注解都是由JUnit5提供的。这段测试函数会断言真实值和设定值二者是否相等。是一个非常简单的测试类。如果我们有若干个测试函数,那么还可以给多个测试函数设置排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestTest {
@Test
@Order(2)
public void testA() {
System.out.println("1st");

}
@Test
@Order(1)
public void testB(){
System.out.println("2nd");
}
}

这里传入的MethodOrderer.OrderAnnotation.class能够将测试函数按照从小到大的顺序排序。其他常见的排序方式如下

  • MethodOrderer.MethodName.class,方法名字母顺序
  • MethodOrderer.DisplayName.class,@DisplayName字母顺序
  • MethodOrderer.Random.class,随机顺序(默认)

除此之外,对于测试函数我们还有一些其他的注解,用来规定不同的调用时机或者起到别的作用,例如

  • @BeforeEach,在每个单元测试方法执行前都执行一遍。
  • @BeforeAll,在所有单元测试方法执行前执行一遍,只执行一次,并且这个方法必须为静态方法。
  • @AfterEach,在每个单元测试方法执行后都执行一遍。
  • @AfterAll,在所有单元测试方法执行后执行一遍,只执行一次,也必须为静态方法。
  • @DisplayName('商品入库测试'),参数是指定单元测试的名称
  • @Disabled,设为无效,跳过该测试
  • @RepeatedTest(n),重复n次

假如说我们针对同一个函数传入不同的参数值,那么可以使用@ValueSource()注解来为参数化测试@ParameterizedTest提供数据。

1
2
3
4
5
6
7
8
@SpringBootTest
public class TestTest {
@ParameterizedTest
@ValueSource(strings = {"apple","banana","cherry"})
void isLengthGreaterThanThree(String fruit){
Assertions.assertTrue(fruit.length()>3);
}
}

同时他的问题也很明显:只能传简单类型、只能传一组同类型、不能动态生成数据,同时目标函数只能接收一个参数。

上文中我们用到了很多Assertions类的方法,它们都被称作断言方法。接下来我们就正式介绍一下它们。

  • assertTrue()里面的表达式值为true时通过,assertFalse()则反之。
  • assertNull()assertNotNull()判断条件是否为null
  • assertThrows(),他是用于断言代码是否抛出特定异常的方法。它接收两个参数是,第一个是异常类型,第二个是抛出异常的函数对象。
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class TestTest {
@Test
void exceptionTest(){
Assertions.assertThrows(ArithmeticException.class,()->{ // 判断是否是数学运算的异常
int i = 1/0; // 测试通过
int i = 1; // 测试不通过
// 注意函数必须符合Executable接口,即无参,返回值为void
});
}
}
  • assertTimeout()方法可以判断某个函数运行是否超时。
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class TestTest {
@Test
void isFinishedIn1Second(){
Assertions.assertTimeout(Duration.ofSeconds(1),()->{ // 传入的必须是一个Duration类的对象
for (int i=0;i<10000000;i++){
System.out.println(""); // 一秒钟肯定是运行不完的
}
});
}
}

这个方法会一直等到函数运行完毕之后才开始算时间,不会打断函数的运行。如果你希望函数运行超时之后直接终止线程,那么就可以使用assertTimeoutPreemptively()

  • AssertAll()是组合断言,当内部所有的断言通过之后他才会通过。比较好的实践方式是在参数中给出多个lambda表达式。
1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
public class TestTest {
@Test
void allCompleted(){
Assertions.assertAll(()->{
Assertions.assertTrue(1>0);
},()->{
Assertions.assertFalse(-1>0);
});
}
}

Mock测试

前面说过,Mock测试就是造假的艺术。我们在SpringBoot中的所有代码都由容器统一管理,由容器进行实例化,生命周期受控,使用依赖注入和配置驱动。例如我们测试类中需要一个Service返回的数据,Mock测试就不会真正去调用该Service,只需要模拟一个数据即可。

我们仿照原文中的接口/mock,该接口传入一个参数name,然后返回Hello, ${name}!。为了方便,我们直接在UserController中进行添加即可。

1
2
3
4
5
6
7
@RestController
public class UserController {
@GetMapping("/mock")
public String hello(@RequestParam(defaultValue = "") String name){
return "Hello, "+ name;
}
}

相对应的Mock测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
@AutoConfigureMockMvc // 启用MockMVC自动配置
public class TestTest {
@Autowired
MockMvc mockMvc;
@Test
void mvcTest() throws Exception{
// 模拟发送一个请求到/mock
mockMvc.perform(MockMvcRequestBuilders
.get("/mock") // 设置请求地址
.param("name","baobao"))// 请求参数
.andExpect(MockMvcResultMatchers.status().isOk()) // 期待状态为200
.andExpect(MockMvcResultMatchers.content().string("Hello, baobao")) // 期待请求返回的字符
.andDo(MockMvcResultHandlers.print()) // 结果输出到控制台
.andReturn();// 返回
}
}

在4.0版本之后的SpringBoot中,@AutoConfigureMockMvc注解被拆到了spring-boot-webmvc-test包中,所以不要忘记在pom文件中声明。文章中说SpringBoot自己使用的是Mockito框架,但是我们这个实例中并没有使用,它也和SpringBoot的各部件无关。MockMVC的基础是基于ResultActions的链式调用,那么首先我们就需要perform()方法来让其返回一个ResultActions对象,然后就可以进行链式调用了。

MockMvcResultMatchers是结果匹配器,用来匹配HTTP请求结果的各种状态,包括请求头、状态码、返回内容甚至Cookie等。后续的链式调用函数包括andExpect进行断言、andDo进行无副作用的操作以及andReturn返回。使用这些链式调用的目的是为了保证mockMvc的单例模式和线程安全能够正常运行。

SpringBoot配置

SpringBoot是基于约定优先的,这就意味着我们不用手动填写所有配置项,只有当我们覆盖了默认配置项之后我们才需要进行配置的编写。配置文件一般放在src/main/resources目录下,通常分为两种,一类是application.properties,另一类是application.yaml。这二者的作用和使用条件都是完全一样的,只在语法风格上有区别。yaml的语法我们在之前已经介绍过了,因此这里主要介绍application.properties的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 端口(默认8080)
server.port=8080
# 上下文路径
server.servlet.context-path=/api
# 绑定地址(默认所有网卡)
server.address=0.0.0.0
# 连接超时时间(毫秒)
server.connection-timeout=5000
# Tomcat 最大线程数
server.tomcat.max-threads=200
# 编码设置
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true

SpringBoot提供了三种方式来读取配置文件中的内容,分别是Environment类、@Value注解以及@ConfigurationProperties注解。在配置文件中我们定义以下两个变量,注意不用像Java语句那样给字符串加上双引号。

1
2
app.name=baobao
app.age=20

如果采用@Value注解的话,我们就需要在对应的类中提前声明对应的成员变量让容器来注入。

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class UserController {
@Value("${app.name}")
String name;
@Value("${app.age}")
int age;
@GetMapping("/conf-test")
public String hello(){
return "Hello, " + this.name + this.age+ " !";
}
}

Enviroment类可以配合@Autowired注解来使用,将其注册成一个成员变量,使用的时候直接通过getProperty方法来获取即可。

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class UserController {
@Autowired
Environment env;
@GetMapping("/conf-test")
public String hello(){
String name = env.getProperty("app.name");
String age = env.getProperty("app.age");
return "Hello, " + name + age+ " !";
}
}

最后是@ConfigurationProperties注解。这个注解比起@Value更适合复杂场景的配置使用。首先我们需要声明一个带有@Component和该注解的类,然后把需要用到的参数放在成员变量当中(注意得是private并且要配好Getter和Setter)。不过有一个问题就是这样做的话只能读取一层的property,想要深入一个层次还需要重新配置prefix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@ConfigurationProperties(prefix = "app") // 配置前缀为app
public class Person {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String name;
private int age;
}

然后就可以使用@AutoWired注解来进行绑定了。

1
2
3
4
5
6
7
8
9
@RestController
public class UserController {
@Autowired
Person p;
@GetMapping("/conf-test")
public String hello(){
return "Hello, " + p.getName() + p.getAge() + " !";
}
}

@ConfigurationProperties注解非常符合面向对象的设计原则,并且在管理的时候也很难混乱,因此SpringBoot底层采用的就是这个注解。

在实际的情况中,开发环境测试环境以及生产环境下都需要不同的配置,因此两种配置方式也给了我们配置动态切换的方法,可以通过加后缀的方式来更改。在application后面加上-devtest或者-pro来分别代表开发测试和生产环境。然后在总的application下面通过修改spring.profiles.active属性来切换所使用的环境。如果使用的是yaml,那么还可以通过---加上配置spring.config.activate.on-profile的方式来配置。例如

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
# 所有环境共享的公共配置
spring:
application:
name: user-service
# ...
# 默认激活环境
spring:
profiles:
active: dev
---
# dev
spring:
config:
activate:
on-profile: dev
# ...
---
# test
spring:
config:
activate:
on-profile: test
# ...
---
# prod
spring:
config:
activate:
on-profile: prod
# ...

SpringBoot程序的配置文件加载根据以下的目录顺序进行,在源代码中,classpath实际上是src/resources,而编译后就指向了target/classes

  1. ./config,当前项目的config目录下
  2. ./,项目根目录
  3. classpath/config,classpath的config目录下
  4. classpath/,classpath的根目录

IoC基础

IOC(Inversion of Control,控制反转),又被称为依赖注入,是 Spring 框架的核心设计原则,它改变了传统对象创建和管理的方式,让框架来掌控对象的生命周期。

使用传统方式,我们每一个对象都是自己创建的,在各种不同的Service类里面让每一个成员变量都等于一个new xxx()。假如说构造函数改变了,你需要换新的参数,那你就只能重新编码。而如果使用SpringBoot,那你直接在类里面给成员变量加上注解就可以,不用赋值,容器会自动控制什么时候来new。本质上是把对象的控制权从程序员给到了Spring容器。Spring容器创建的对象就叫做Bean。

本文中剩下的关于IOC容器的问题,目前看来不适合新手进行阅读,因为它是涉及到各种底层原理的东西,包括手动装配IOC容器之类的,那是Spring的底层原理,而SpringBoot把这一切都已经给你自动装配好了。因此需要等我们熟练使用SpringBoot之后,我们就可以进行自顶向下的原理挖掘了。

总结

这是SpringBoot的第一部分,最近事情有点多,而且说实话这个博客其实挺烂的,所以说要去粗取精,更符合当前的最佳实践的状态。下周可能是继续这个SpringBoot,或者弄一些别的东西,总之我本人也是很期待的哈哈哈。

参考文献

  • 标题: SpringBoot学习笔记(1)- NoTe0x002
  • 作者: Baobao0824
  • 创建于 : 2026-04-01 00:00:00
  • 更新于 : 2026-05-18 09:49:02
  • 链接: https://blog.baobao0824.top/学习笔记NoTe/NT-0x002/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论