Spring Boot & Spring Data JPA – Complete Course(1) - NoTe0x003

Baobao0824

原链接
https://www.youtube.com/watch?v=5rNk7m_zlAg

写在前面

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

很久没有进行更新了。最近一段时间事情比较多,然后好不容易积累起来的SpringBoot基础已经消耗殆尽了。正好找到了一个Youtube上的SpringBoot视频教程,我们直接跟着他的节奏来吧。由于整个视频非常的长,一共有12小时,因此我们把他拆开来看。首先先是Spring的基础安装以及Bean的使用。

What is Spring Boot?

SpringBoot是一个开发基于Spring应用的方法,它需要非常少或者直接0配置。它提供了成套的起始文件,例如POM文件(他说的那个Gradle是什么我不知道,毕竟我没用过Gradle),同时可以自动配置。

那么为什么要用SpringBoot呢?首先它可以作为一个独立的app进行加载;其次它内置了服务器,你不用再自己来配置Tomcat或者Jetty了;同时它提供了给starter的很多现成的文件;以及不需要其他的XML配置了。

我们现在使用Spring initializr创建了一个新的SpringBoot应用springPractice,JAVA版本为21,SpringBoot版本为4,同时添加了Spring Web包。我们使用idea打开这个项目。我们会发现项目结构如下所示,我们把一些重要文件的作用都标记在了上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
springPractice/
├── .idea/ # idea自动生成的目录
├── .mvn/ # Maven Wrapper 配置目录,这样就可以不用手动下载maven了(或许以后可以在老国重上改造一下)
├── src/
│ ├── main/
│ │ ├── java/ # 程序中的所有类和对象都在这个文件夹中
│ │ │ └── com/baobao/springPractice/
│ │ │ └── SpringPracticeApplication.java # 主启动类
│ │ └── resources/
│ │ ├── static/ # 放置静态资源,例如HTML、RestfulAPI的UI
│ │ ├── templates/ # 模板文件目录 (HTML)
│ │ └── application.properties # 应用配置文件
│ └── test/ # 所有的测试代码
│ └── java/
│ └── com/baobao/springPractice/
│ └── SpringPracticeApplicationTests.java # 测试类
├── .gitattributes # Git 属性配置文件
├── .gitignore # Git 忽略文件配置
├── HELP.md # 帮助文档
├── mvnw # Maven Wrapper 脚本 (Linux/Mac)
├── mvnw.cmd # Maven Wrapper 脚本 (Windows)
└── pom.xml # Maven 项目核心配置文件

接下来我们从头到尾看一下pom.xml文件的结构,每一部分的作用就写在注释里面了(可以看到这个注释格式和HTML是一样的)

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
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="UTF-8"?>
<!-- 上面一行声明了XML及其版本,下面一个标签声明了项目和XML命名空间(maven4.0.0)以及模式位置等-->
<!-- xml命名空间的作用是为了防止标签冲突,让maven识别出<build>等标签是maven的 -->
<!-- xmlns:xsi是XSI(XML Schema Instance)的命名空间,定义了xsi:schemaLocation这个属性的用法 -->
<!-- xsi:schemaLocation是模式定位,两个网址分贝时命名空间和规则文件地址,方便给idea等IDE进行智能提示 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 项目的model version -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--这个项目是基于spring boot starter parent的,下面都是本项目中的各种信息 -->
<groupId>com.baobao</groupId>
<artifactId>springPractice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name/>
<description/>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<!-- properties标签都是项目中实质有影响的属性 -->
<properties>
<java.version>21</java.version>
</properties>
<!-- 项目需要用到的所有依赖关系 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<!-- 使用springboot的时候不用指定版本,因为它会直接继承parent项目的版本 -->
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
<!-- 加上了<scope>标签就说明这个依赖只有在测试的时候才会提供 -->
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

然后我们直接启动这个应用,看看输出的内容有什么,里面各部分的解释已经放在注释里面了

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
/Users/baohan/Library/Java/JavaVirtualMachines/ms-21.0.11/Contents/Home/bin/java ...

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/

:: Spring Boot :: (v4.0.6)
# 这就是Spring的标记

2026-05-14T14:54:54.795+08:00 INFO 31395 --- [springPractice] [ main] c.b.s.SpringPracticeApplication : Starting SpringPracticeApplication using Java 21.0.11 with PID 31395 (/Users/baohan/Documents/springPractice/target/classes started by baohan in /Users/baohan/Documents/springPractice)
# java版本、进程id等
2026-05-14T14:54:54.797+08:00 INFO 31395 --- [springPractice] [ main] c.b.s.SpringPracticeApplication : No active profile set, falling back to 1 default profile: "default"
# 没有活跃的配置文件集,退回到默认配置
2026-05-14T14:54:55.102+08:00 INFO 31395 --- [springPractice] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
# Tomcat初始化为8080端口
2026-05-14T14:54:55.109+08:00 INFO 31395 --- [springPractice] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-05-14T14:54:55.109+08:00 INFO 31395 --- [springPractice] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.21]
2026-05-14T14:54:55.126+08:00 INFO 31395 --- [springPractice] [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 290 ms
2026-05-14T14:54:55.243+08:00 INFO 31395 --- [springPractice] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
# Tomcat开始运行,上下文路径为'/'
2026-05-14T14:54:55.245+08:00 INFO 31395 --- [springPractice] [ main] c.b.s.SpringPracticeApplication : Started SpringPracticeApplication in 0.626 seconds (process running for 0.845)
# 已经启动应用

首先他打算教我们一个最炫酷的技巧,就是如何更改Spring的横幅来发布我们自己的应用。首先我们应该去Text Banner Generator,输入你喜欢的内容并选择字体,然后直接复制。在src/main/resources文件夹中新建一个banner.txt文件,把你复制的字符串粘贴进去就可以了,或者你也可以在里面放上任何你想要的字符串都是可以的。例如

1
2
3
4
5
6
7
8
9
10
11
/Users/baohan/Library/Java/JavaVirtualMachines/ms-21.0.11/Contents/Home/bin/java ...
░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓███████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓██████▓▒░
SpringBoot 4.0.6
2026-05-14T15:08:59.614+08:00 INFO 35815 --- [springPractice] [ main] c.b.s.SpringPracticeApplication : Starting SpringPracticeApplication using Java 21.0.11 with PID 35815 (/Users/baohan/Documents/springPractice/target/classes started by baohan in /Users/baohan/Documents/springPractice)
# ...

接下来就可以创建自己的类了,在SpringPracticeApplication同级的目录中创建一个新的类,直接叫MyFirstClass,提供一个方法SayHello

1
2
3
4
5
6
7
package com.baobao.springPractice;

public class MyFirstClass {
public String sayHello(){
return "Hello from the MyFirstClass";
}
}

然后直接在main函数中输出这个结果

1
2
3
4
5
public static void main(String[] args) {
SpringApplication.run(SpringPracticeApplication.class, args);
MyFirstClass myFirstClass = new MyFirstClass();
System.out.println(myFirstClass.sayHello());
}

你会发现Spirng应用在启动之后输出了返回的结果。但是这并不是Spring官方推荐你使用的方式,因为你没有用到依赖注入等Spring的核心特性。

Spring Beans

到底什么是Spring Bean?之前我也没太搞清楚。其实说白了就是在Java应用中,由Spring框架管理的对象,目的是为了简化Java应用的开发,帮助开发者省去大量的手动操作。它可以通过XML、Java注解和Java代码来进行配置。它同样拥有自己的生命周期,由Spring容器来进行管理。当我们启动Spring应用的时候,第一个Spring容器就会被启动,然后创建对应的bean;当容器关闭的时候,bean就会被销毁。

我们可以使用@Configuration注解来声明一个类进行完整的配置,这个类必须是public并且没有final修饰;同时使用@Bean注解在一个配置类中,这个类必须是非final和非private的。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class AppConfig {
@Bean
public PaymentService paymentService(AccountRepository accountRepository){
return new PaymentServiceImpl(accountRepository);
}
@Bean
public AccountRepository accountRepository(){
return new JdbcAccountRepository(dataSource());
}
@Bean("ds")
public DataSource dataSource(){
return (...)
}
}

我们可以看到,最后一个方法的注解中传入了一个字符串ds,那么spring框架在运行的时候,就会给Bean的名字设为ds,上面两个不提供名称的,Spring框架就会将方法名作为Bean的名称。

回到我们的项目中,如果我们在SpringPracticeApplication类中创建一个带有@Bean注解的myFirstClass方法,同时获取应用本身(是一个ConfigurableApplicationContext类型的对象),然后用getBean方法来获取这个对象。我们就不用再预先创建对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class SpringPracticeApplication {

public static void main(String[] args) {
var ctx = SpringApplication.run(SpringPracticeApplication.class, args);
MyFirstClass myFirstClass = ctx.getBean(MyFirstClass.class);
System.out.println(myFirstClass.sayHello());
}
@Bean
public MyFirstClass myFirstClass() {
return new MyFirstClass();
}
}

对于这段代码,我们先讲讲var,它是java10引入的一个局部变量类型推断关键字,让编译器自动推断变量的类型,从而减少样板代码,让代码更简洁。但是它不会影响java的强类型特性,变量该是什么类型还是什么类型,只是为了节省代码编写时的冗余。

然后我们再看,SpringApplication.run方法返回的是一个ConfigurableApplicationContext类型的对象,使用getBean方法就可以获取到对应的Bean了。这个方法有四个重载。

  1. 按照Bean名称获取,直接传Sting name,返回一个Object,需要强制类型转换,不推荐直接使用。
  2. 按照Bean名称加上类型获取,传入String name, Class<T> requiredType,类型安全,自动做类型检查,最常用。
  3. 按照类型获取,传入Class<T> requiredType,但是要保证容器中只能有一个该类型的Bean,否则就会抛出异常。
  4. 最后就是按照类型+构造参数获取,传入Class<T> requiredType, Object... args,用于原型Bean或者需要运行时传参的工厂Bean。

那么在调用getBean的时候,Spring内部做了什么呢?首先会根据名称或者类型查找容器中的Bean定义,然后判断Bean的作用域,如果是singleton那么直接返回已存在的实例,如果是prototype那么就会返回新实例。然后会递归创建依赖的Bean,然后初始化回调,最后返回Bean实例。如果我们没有添加@Bean注解,那么就会报错NoSuchBeanDefinitionException

Spring Component

一个Spring Component(应该是叫组件)包括了一个类级别的注解@Component,能够将这个类标记为Spring Component。通过使用@Autowired注释,就能自动完成构造器依赖注入,不过当只有一个构造器的时候它就是可选的。例如

1
2
3
4
5
6
7
8
@Component
public class PaymentServiceImpl {
private final AccountRepository accountRepository;
@Autowired
public PaymentServiceImpl(AccountRepository accountRepository){
this.accountRepository = accountRepository;
}
}

组件也有很多的类型,而Component是通用型的组件,其他的组件类型,例如@Repository@Service@Controller等,不过你也可以自己自定义组件原型。

在我们的项目中,我们也可以直接给MyFirstClass类加上@Component(或者它的细化例如@Service)注解,那么Spring应用在启动之前进行扫描的时候,就会把整个类看做一个Bean。完整的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// MyFirstClass.java
@Service
public class MyFirstClass {
public String sayHello(){
return "Hello from the MyFirstClass";
}
}
// SpringPracticeApplication.java
@SpringBootApplication
public class SpringPracticeApplication {
public static void main(String[] args) {
var ctx = SpringApplication.run(SpringPracticeApplication.class, args);
MyFirstClass myFirstClass = ctx.getBean(MyFirstClass.class);
System.out.println(myFirstClass.sayHello());
}
}

我们深入@Service的源码来看看。

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
/**
* Alias for {@link Component#value}.
*/
@AliasFor(annotation = Component.class)
String value() default "";
}

这是一个public的接口,已经用@Component注释了。同时这里可以补充一个java小知识,就是注解使用@interface声明,实际上就是一种特殊的接口,例如这里有个注解声明的编译前后的代码。

1
2
3
4
5
6
7
8
9
10
// 编译前
public @interface MyAnnotation {
String value();
int count() default 1;
}
// 编译后
public interface MyAnnotation extends java.lang.annotation.Annotation {
String value();
int count() default 1;
}

但是注解本身是“标记”或者“携带数据”的作用,并不提供行为,所以注解本身不能被实现,也不能有逻辑,只能有声明,返回值的类型也有限制,也只能继承java.lang.annotation.Annotation

接下来,我们先把MyFirstClass中的注解去掉,在应用所在类的目录下创建一个ApplicationConfig类,把之前在应用类中声明的myFirstClass方法移动过来,同时给这个类加上@Configuration注解。

1
2
3
4
5
6
7
@Configuration
public class ApplicationConfig {
@Bean
public MyFirstClass myFirstClass() {
return new MyFirstClass();
}
}

我们会发现依旧能够正常启动。在上文中我们也说过了,一个Bean的名称,如果我们没有在注解中指定的话,那么就是方法名。因此我们可以直接把getBean中的实参改成"myFirstClass", MyFirstClass.class

接下来我们会探索关于Bean的更多功能。我们在MyFirstClass类中声明一个私有字段String myVar,同时生成对应的构造函数。整个类看起来就像这样(别忘了给Bean中的用法加上实参):

1
2
3
4
5
6
7
8
9
public class MyFirstClass {
private String myVar;
public MyFirstClass(String myVar) {
this.myVar = myVar;
}
public String sayHello(){
return "Hello from the MyFirstClass ===> myVar = "+ myVar;
}
}

可以看到输出也是能正常的进行显示的。

接下来我们要展示Bean的其他用法。首先我们创建一个新类,叫做MyFirstService,里面的内容如下(我就不解释了都能看得懂)同时也对应用类进行更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class MyFirstService {
private MyFirstClass myFirstClass;
public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

// SpringPracticeApplication.java
@SpringBootApplication
public class SpringPracticeApplication {
public static void main(String[] args) {
var ctx = SpringApplication.run(SpringPracticeApplication.class, args);
MyFirstService myFirstService = ctx.getBean(MyFirstService.class);
System.out.println(myFirstService.tellAStory());
}
}

我们直接运行,会弹出MyFirstClass.sayHelloNullPointerException,这是因为我们没有在MyFirstService中告诉Spring如何注入这个类。我们只需要给这个类一个构造函数,同时idea推荐你给这个字段加上final关键字。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyFirstService {
private final MyFirstClass myFirstClass;
@Autowired
public MyFirstService(MyFirstClass myFirstClass) {
this.myFirstClass = myFirstClass;
}
public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

那么在运行的过程中,MyFirstService的构造函数在执行时,Spring会去找一个匹配myFirstClass的Bean,在ApplicationConfig中获取了返回值匹配的方法Bean,然后分配给当前类中的字段。实际上,在SpringBoot4.3更新后,如果只有一个构造函数的话,那么@Autowired注解就可以不写了,Spring框架会自动将当前函数作为bean的注入对象。

我们在ApplicationConfig类中再创建一个Bean,我们会发现无法正常启动,因为发现了两个MyFirsrClass的Bean,框架不知道要用哪个。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ApplicationConfig {
@Bean
public MyFirstClass myFirstBean() {
return new MyFirstClass("first bean");
}
@Bean
public MyFirstClass mySecondBean() {
return new MyFirstClass("second bean");
}
}

Spring框架给了你解决方法,要么设置主要的bean,要么给bean带上@Qualifier,注意这个注解不会影响Bean本身的名字,只是作为一个限定的额外信息。

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

例如

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
@Configuration
public class ApplicationConfig {
@Bean
@Qualifier("bean1")
public MyFirstClass myFirstBean() {
return new MyFirstClass("first bean");
}
@Bean
@Qualifier("bean2")
public MyFirstClass mySecondBean() {
return new MyFirstClass("second bean");
}
}

// MyFirstService.java
@Service
public class MyFirstService {
private final MyFirstClass myFirstClass;
public MyFirstService( @Qualifier("bean2") MyFirstClass myFirstClass) {
this.myFirstClass = myFirstClass;
}
public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

这就是参数级注解或者标记注解。如果我们不像在传参的时候指定的话,那么也可以在bean方法前面添加@Primary注释,能够让Spring框架进行选择,这里就不再展示了。

Dependency Injection

依赖注入,缩写为DI,就是在Bean的作用域中注入各种不同的变量,不用在函数中手动传参了。大概分为这四种:构造器注入、字段注入、配置方法注入和setter方法注入。

例如

1
2
3
4
5
6
7
@Service
public class DefaultPaymentService {
private final AccountRepository accountRepository;
public DefaultPaymentService(AccountRepository accountRepository){
this.accountRepository = accountRepository;
}
}

就是一个典型的构造器注入,这也是Spring官方最推荐的注入方式。同时,如果我们有多个不同的Bean,你也可以指定注入的时候使用哪个Bean的数据来进行注入。例如有下面这样一个配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ApplicationConfig{
@Bean
@Qualifier("primary")
public AccountRepository primary(){
return new JdbcAccountRepository(...);
}
@Bean
@Qualifier("secoundary")
public AccountRepository secoundary(){
return new JdbcAccountRepository(...);
}
}

那么在对应组件使用构造器注入的时候,就可以指定选择哪一个构造器了。

1
2
3
4
5
6
7
8
9
@Service
public class DefaultPaymentService{
@Autowired
public DefaultPaymentService(
@Qualifier("primary") AccountRepository accountRepository
){
this.accountRepository = accountRepository;
}
}

在多个Bean的情况下,如果我们想给一个Bean设为默认,那么可以用@Primary注释,例如

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ApplicationConfig{
@Bean
@Primary
public AccountRepository primary(){
return new JdbcAccountRepository(...);
}
@Bean
public AccountRepository secoundary(){
return new JdbcAccountRepository(...);
}
}

在这种情况下,除非你用@Qualifier注解去指定,否则的话默认用的就是这个primary

说完了构造器注入,我们再来看看字段注入。我们可以把数据直接注入到类的字段中,而不需要什么构造器或者方法声明,但是官方不鼓励使用这种方式,因为这会使得在隔离环境下测试组件变得更加复杂,因此只能用在测试类中使用。例如:

1
2
3
4
5
@Service
public class DefaultPaymentService {
@Autowired
private AccountRepository accountRepository;
}

还有方法注入,它允许通过一个方法来注入一个或者多个依赖,或者当接收依赖的时候允许初始化工作。例如

1
2
3
4
5
6
7
@Service
public class DefaultPaymentService{
@Autowired
public void configureClass(AccountRepository accountRepository, FeeCaclulator feeCaclulator){
// ...
}
}

其实看着和构造器注入似乎没有什么区别,因为构造函数本身就是一种方法。但是方法注入这一条路线并不是Spring官方所推荐的。它在Bean创建后才进行注入,对象状态可变,因此可能会掩盖很多问题。

最后我们再来看setter函数注入。我说实话,这和方法注入和构造器注入几乎完全一样,只不过@Autowired等注解的位置放在了setter函数之前。

1
2
3
4
5
6
7
@Service
public class DefaultPaymentService {
@Autowired
public void setAccountRepository(AccountReposiotry accountRepository){
//...
}
}

Spring官方推荐的做法是:对必须得依赖项使用构造器注入,而对可选的依赖项使用Setter或者配置方法(在setter方法上使用@Required注解也可以用来将属性标记为必须依赖项),最好还是用带有参数程序化验证的构造器注入。

Spring官方最提倡使用构造器注入,因为它让你能够将应用程序组件实现为不可变对象,并且确保所需的依赖项不为null。此外,构造器注入的组件总是以完全初始化的状态返回给客户端调用。当你的构造器参数太多时,应该进行重构。

setter注入应该主要用于那些可以在类内部赋予合理默认值的可选依赖项,否则你必须在代码使用该依赖的每个地方都进行not-null检查,setter方法使得该类的对象易于在以后进行重新配置或者重新注入,你也可以通过JMX MBeans管理setter注入。

刚才我们在项目中用到的都是构造器注入,接下来我们来展示字段注入。移除MyFirstService的构造函数,直接在字段中进行注入,不过Spring官方并不推荐这种做法,我们还是看看就好了,可以看到最终输出的结果还是一样的。

1
2
3
4
5
6
7
8
9
@Service
public class MyFirstService {
@Autowired
@Qualifier("mySecondBean")
private MyFirstClass myFirstClass;
public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

如果Bean本体都没有@Qualifier进行修饰,那么可以进行选择吗,答案是可以的。只需要在使用的时候用@Qualifier注解传入bean名称就可以了(默认是方法名,当然你也可以在@Bean中进行起名)

之后我们展示方法注入。在MyFirstService类中创建对应的方法,给类的字段赋值即可。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MyFirstService {
private MyFirstClass myFirstClass;
@Autowired
public void injectDependencies(@Qualifier("myFirstBean") MyFirstClass myFirstClass){
this.myFirstClass = myFirstClass;
}

public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

这里就有一个问题了。这是一个类里面的方法,它没有任何的用法,又是什么时候调用这个方法的呢?实际上是Spring容器在创建MyFirstService这个Bean的过程中创建的。

最后我们来看setter函数注入。给MyFirstService类中的myFirstClass成员变量创建setter函数就可以了。甚至结构一点都不用改,把方法名换成setMyFirstClass就可以了。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyFirstService {
private MyFirstClass myFirstClass;
@Autowired
public void setMyFirstClass(@Qualifier("myFirstBean") MyFirstClass myFirstClass){
this.myFirstClass = myFirstClass;
}
public String tellAStory(){
return "the dependency is saying : "+ myFirstClass.sayHello();
}
}

Bean Scope

Bean Scope,即Bean的作用域,定义了Spring容器中Bean的生命周期和可见范围,即一个Bean创建几次、存活多久、对谁可见。默认的作用域是单例模式(Singleton),整个容器中只有一个实例,这意味着每个应用上下文实例都会被其他组件访问,因此你必须保证容器的线程安全,适合不需要保持状态或者需要所有用户共享相同状态的地方。Spring提供的其他作用域模式如下:

  • prototype,每次获取都创建一个新实例,适合携带特定用户或者线程的状态,因此无法被共享。
  • request,一次HTTP请求一个实例
  • session,一次HTTP Session一个实例
  • application,整个ServletContext一个实例
  • websocket,一次WebSocket会话一个实例

那么我们如何定义一个Bean的作用域呢?也需要使用特定的注解来。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyConfiguration{
@Bean
@Scope("portotype")
public Bean1 beand(){
// ...
}
@Bean
@SessionScope
public Bean2 bean2(){
// ...
}
}

直接看就能够看懂,我认为不用再解释了(我到现在才知道,居然可以给不同的Bean设置不同的作用域和周期)。

Special Spring Beans

除了自己定义Bean之外,Spring框架内部也有很多特殊的Bean。

首先是Environment,它是Spring容器在启动时自动创建并管理的一个特殊Bean,负责管理Profile切换环境、管理Properties配置读取。使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ApplicationConfig {
@Autowired Environment environment;
@Bean
public PaymentService paymentService(){
var profile = Profiles.of("cloud");
var isOkay = this.environment.acceptsProfiles(profile);
this.environment.getProperty("data.driver");
return ...
}
}

原视频中给environment变量添加上了final修饰符,这个理论上是不应该有的。因为final字段必须在构造时就赋值,而@Autowired注解是Spring容器后续注入的,会导致编译错误。

接下来就是Profile,我们可以直接用@Profile注解来获取这个Bean中的内容,用在组件、配置上。例如

1
2
3
@Service
@Profile("cloud")
public class DefaultPaymentService implements PaymentService{}

也可以直接将这个注解声明在某一个Bean中,那么可以做到仅在某一个环境下注入某一个Bean。例如

1
2
3
4
5
6
7
8
@Configuration
public class ApplicationConfig {
@Bean
@Profile("cloud")
public PaymentService paymentService(){
// ...
}
}

我们也可以通过代码程序化地将一个Bean设为是否激活。

1
2
3
4
5
6
7
8
9
public static void main(String[] args){
AnnotationConfigApplicationContext applicationContext;
applicationContext = new AnnotationConfigApplicationContext();
applicationContext.getEnvironment().setActiveProfiles("cloud");
applicationContext.scan("com.baobao.sample");
applicationContext.refresh();

PaymentService paymentService = applicationContext.getBean(PaymentService.class)
}

这段代码在手动创建一个Spring容器。首先他创建了一个AnnotationConfigApplicationContext,一个基于注解的Spring容器,然后在下一行激活了cloud环境;再下一行扫描了指定包及其子包,识别类似于@Component@Repository之类的注解;最后刷新容器,这一步做了所有核心工作,包括初始化Bean;最后一行就可以获取到Bean本体了。

除了上面的代码方法,我我们还可以通过修改application.yaml或者application.properties中的spring.profiles.active字段来进行修改。

在Spring框架中,@Value注解可以很好的把值注入到类中的变量,无论是他们什么类型,而这些值可以来自于属性文件、系统属性甚至是硬编码。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@PropertySource("classpath:database.properties")
public class ApplicationConfig{
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
return null;
}
}

假设我们关于数据库的属性值全部存在资源文件夹下的database.properties文件中,利用@PropertySource注解告诉Spring从哪里去找。@Value注解还可以用来从别的Bean里面获取值,例如

1
2
3
4
5
6
@Component
public class FeeCalculator{
private String defaultLocate;
@Value("#{systemProperties['user.region']}")
public void setDefaultLocate(String defaultLocate)
}

使用#开头的@Value注解,#{}为SpEL表达式,直接从内置对象systemProperties里面拿属性,然后获取key[user.region]

回到我们的项目,我们在MyFirstService类中添加一个字段environment,同时创建对应的getter和setter,为setEnvironment方法添加上@AutoWired。然后可以写一些返回环境信息的方法,例如java版本或者系统名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class MyFirstService {
private Environment environment;

public Environment getEnvironment() {
return environment;
}
@Autowired
public void setEnvironment(Environment environment) {
this.environment = environment;
}
public String getJavaVersion(){
return environment.getProperty("java.version");
}
public String getOsName(){
return environment.getProperty("os.name");
}
}

然后直接让Application的myFirstService调用这两个方法即可。除此之外,它还可以读取应用自定义属性,也就是application.properties(或者yaml)中的变量。例如现在已经定义了my.custom.property = Baobao,然后新增一个方法:

1
2
3
public String getPropertyName(){
return environment.getProperty("my.custom.property");
}

假如说我们现在直接这样写:

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyFirstService {
private String customProperty;
public String getCustomProperty() {
return customProperty;
}

public void setCustomProperty(String customProperty) {
this.customProperty = customProperty;
}
}

你会发现最后输出的是一个null,因为我们还没有给这个变量注入一个值。这个时候就需要用@Value注解了。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyFirstService {
@Value("${my.custom.property}")
private String customProperty;
public String getCustomProperty() {
return customProperty;
}
public void setCustomProperty(String customProperty) {
this.customProperty = customProperty;
}
}

实际上诸如int之类的其他类型也是可以的,Spring能够自动转换,如果转换失败就会报错,和其他的java代码一样。

接下来,假设我们还有一个属性文件,在resources文件夹下创建,名叫custom.properties,只有一行my.prop=Baobao。那么如何把这个文件中的变量注入到MyFirstService中呢?只需要用到@Value注解。

1
2
3
4
5
6
7
8
9
@Service
@PropertySource("classpath:custom.properties")
public class MyFirstService {
@Value("${my.prop}")
private String customPropertyFromAnotherFile;
public String getCustomPropertyFromAnotherFile() {
return customPropertyFromAnotherFile;
}
}

注意到,这里的@Value注解使用的是类似于js中的模板字面量,然后需要用@PropertySource注解来定义数据源,否则默认找的就是application文件。如果我们是有多个来源呢?那么可以使用PropertySources注解。

1
2
3
4
@PropertySources({
@PropertySource("classpath:custom.properties"),
@PropertySource("classpath:custom2.properties")
})

没想到吧,我第一次见注解的参数里面包括了注解的,这还是我第一次看。

最后一部分是关于@Profile注解的使用。如果我们想要创建在特定环境专用的配置文件,只需要加个-即可,例如application-dev.properties,同时更改一下该文件里面的变量值。那么如何在IDE里运行不同环境的APP呢?直接在idea的配置里面,填写“有效配置文件”那一栏为dev即可。

我都怀疑他是不是翻译出错了

现在输出的就是application-dev文件中的变量了。在日常开发中,最好把所有环境中都要用到的共通变量放到主文件里,不同环境中的特有变量或者需要覆盖的值才应该放在对应的文件中。或者可以直接在application文件中给spring.profiles.active变量赋值。运行之后我们可以看到控制台输出

The following 1 profile is active: “dev”

如果我们将该变量赋予多个值,例如dev,test,custom,再次运行程序,我们会发现即使没有对应的文件之类的,他也会显示有3个profile,即使找不到也不会抛出异常,毕竟约定大于配置。如果多个配置都存在的话,那么后一个配置的变量会覆盖前一个。

除了这些方法,我们还可以用代码来设置我们当前的配置环境。我们需要先获取SpringApplication,然后调用这个变量的setDefaultProperty方法,传入一个Map。例如

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class SpringPracticeApplication {
public static void main(String[] args) {
var app = new SpringApplication(SpringPracticeApplication.class);
app.setDefaultProperties(Collections.singletonMap("spring.profiles.active","dev"));
var ctx = app.run(args);
MyFirstService myFirstService = ctx.getBean(MyFirstService.class);
System.out.println(myFirstService.getCustomProperty());
}
}

Collections.singletonMap方法能够让你快速创建只有一个元素的Map,简化了大量代码,同时生成的Map不可变,天生线程安全。不过个人认为这种代码修改环境的方式还是太麻烦了,实际情况中应该很少用得到。

那么我们如何让一个Bean仅在特定环境中使用呢?很简单,只需要给对应的Bean加上@Profile注解就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class ApplicationConfig {
@Bean
@Profile("test")
public MyFirstClass myFirstBean() {
return new MyFirstClass("first bean");
}
@Bean
@Profile("dev")
public MyFirstClass mySecondBean() {
return new MyFirstClass("second bean");
}
@Bean
@Profile("test")
public MyFirstClass myThirdBean() {
return new MyFirstClass("third bean");
}
}

此时在dev环境里,你不指定bean的名称,那么也只有第二个bean可以用。那么假如我们在@Qualifier注解中要求的Bean名称和当前的环境不匹配呢?那么会直接构建失败。Spring会告诉你

The following candidates were found but could not be injected:

  • User-defined bean method ‘mySecondBean’ in ‘ApplicationConfig’

我们也可以设置整个类的@Profile,例如ApplicationConfig.java,那么你就可以把三个Bean方法中的@Profile全部移除掉了。毕竟它的上级已经要求了对应的环境。假设这时候环境不匹配,那么构建失败时显示的错误和之前就不一样了,你会发现Spring找不到任何的Bean,同时他给你的建议变成了

Consider defining a bean of type ‘com.baobao.springPractice.MyFirstClass’ in your configuration.

Best Practices

这一部分我们要讲一些在编写Spring应用时的最佳实践。

  1. 避免大型配置。你可以把配置类拆开,拆成多个类,而不是只用一个配置类,那样的话这个类就会变得非常的庞大。例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Configuration
    public class ServiceConfig {
    @Bean
    public PaymentService paymentService(){}
    }
    @Configuration
    public class RepositoryConfig {
    @Bean
    public AccountRepository accountRepository(){}
    }
    @Configuration
    @Import({ServiceConfig.class, RepositoryConfig.class})
    public class AppConfig{
    @Bean
    public DataSource datasource(){}
    }
    这段代码中,我们在AppConfig这个总类中,导入了ServiceConfig类和RepositoryConfig类。
  2. Spring initializr提供了一个快速构建基本项目的工具,这个在之前已经说过了就不再赘述了。

总结

这些内容是视频的前两个小时,讲了SpringBoot项目的创建、基本工作流程以及Bean的使用。原视频都是先讲全部的知识点,然后来全部的实践操作,这个非常的不适合学习,所以我把顺序调整了很多。如果还要做这个视频的话那后续应该还是这么搞了。

  • 标题: Spring Boot & Spring Data JPA – Complete Course(1) - NoTe0x003
  • 作者: Baobao0824
  • 创建于 : 2026-05-18 10:20:10
  • 更新于 : 2026-05-18 10:20:10
  • 链接: https://blog.baobao0824.top/学习笔记NoTe/NT-0x003/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论