原链接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" ?> <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 > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 4.0.6</version > <relativePath /> </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 > <java.version > 21</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-webmvc</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-webmvc-test</artifactId > <scope > test</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了。这个方法有四个重载。
按照Bean名称获取,直接传Sting name,返回一个Object,需要强制类型转换,不推荐直接使用。
按照Bean名称加上类型获取,传入String name, Class<T> requiredType,类型安全,自动做类型检查,最常用。
按照类型获取,传入Class<T> requiredType,但是要保证容器中只能有一个该类型的Bean,否则就会抛出异常。
最后就是按照类型+构造参数获取,传入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 @Service public class MyFirstClass { public String sayHello () { return "Hello from the MyFirstClass" ; } } @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 { @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(); } } @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.sayHello的NullPointerException,这是因为我们没有在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" ); } } @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 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类。
Spring initializr提供了一个快速构建基本项目的工具,这个在之前已经说过了就不再赘述了。
总结 这些内容是视频的前两个小时,讲了SpringBoot项目的创建、基本工作流程以及Bean的使用。原视频都是先讲全部的知识点,然后来全部的实践操作,这个非常的不适合学习,所以我把顺序调整了很多。如果还要做这个视频的话那后续应该还是这么搞了。