SpringBoot原理篇-课堂笔记
一、SpringBoot IoC
SpringBoot和Spring的关系:
Spring:是一个针对于JavaSE和JavaEE提供更简单使用,主要提供了IoC(控制反转,用于解耦)和AOP(面向切面编程,用于增强)两大核心思想,可以简化有关一切Java应用的开发与维护工具,是一个全栈式(服务端全栈涵盖了Web层、Service层、Dao层)的框架
SpringBoot:引导开发人员使用Spring的一个脚手架,它是对Spring框架做了再封装,目的不是为了取代Spring,而是为了让开发人员更方便的使用Spring框架
IoC:控制反转。反转的是创建对象的控制权
- 原本:需要什么对象,就要自己主动new一个对象。如果被new的类不存在,就没法继续写代码。存在:编译期耦合
- 反转:由Spring框架创建对象放到IoC容器里。需要什么对象,就从容器里取出一个对象。没有:编译期耦合,存在运行期耦合
- 把创建对象的控制权交给了框架。需要一个对象,由原来的主动创建,变成了现在的被动接收
如果要使用IoC:IoC是帮我们管理“对象”,而不是类
- 告诉Spring,哪些类需要你帮我创建对象:注册bean。 告诉Spring,Spring就会给我们指定的类创建对象,放到IoC容器里管理
- 告诉Spring,如何管理、配置创建好的对象:配置bean。比如让Spring以单例模式或者多例模式,来管理bean对象;
- 告诉Spring,要帮我把bean对象的依赖注入进去,最终给我的bean对象需要是完整的。
1. 注册bean的方式
1.1 @Component及其衍生注解与组件扫描
注册bean的注解
注解:就是一个标记、一个记号。它本身没有任何功能,需要其它代码提供功能
在某一个类上,添加以下任何一个注解,就意味着:告诉Spring,这个类你要帮我创建对象,放到IoC容器里去
注解 | 说明 |
---|---|
@Component | 注册bean对象的根本注解 |
@Controller | 是@Component的衍生注解,提供了语义化,通常在web层的类上加这个注解 |
@Service | 是@Component的衍生注解,提供了语义化,通常在Service层的类上加这个注解 |
@Repository | 是@Component的衍生注解,提供了语义化,通常在Dao层的类上加这个注解 |
@Configuration | 是@Component的衍生注解,提供了语义化,加这个注解的类要做为一个配置类,Spring底层会有不同的处理 |
实际使用中:
- web层的类:加@Controller
- service层的类:加@Service
- dao层的类:加@Repository
- 不在这三层的类:加@Component
- 一个类要做为配置类:加@Configuration
组件扫描@ComponentScan
如果我们在某个类上加了上边的@Component或衍生注解,还需要由Spring扫描我们项目里所有的类:
- 当Spring发现某个类上有@Component或它的衍生注解,就会创建对象,并把对象放到IoC容器里
如果没有使用SpringBoot框架,而是使用原始的Spring:必须有一个配置类,类上加@ComponentScan("要扫描的包")
- 加了这个注解之后,Spring就会扫描我们指定的这个包里所有的类
如果使用了SpringBoot框架:它在引导类上的那个@SpringBootApplication
,其实已经包含了@ComponentScan
-
我们就不需要自己再加
@ComponentScan
了 -
但是没有指定扫描的包,所以会扫描:添加了
@ComponentScan
注解的类 所在的包。最终是:只会扫描引导类所在的包
如果一个类加了
@Component
或衍生注解,但是不在扫描范围内的话,Spring容器里也不会有这个对象
1.2 @Configuration与@Bean
适合于第三方jar包里的类:
- 我们不能修改类,不可能在类上加@Component
- 如果想要让Spring创建对象放到容器里,就要使用@Configuration和@Bean
用法:
- 创建一个类,类上加@Configuration,就成为一个配置类
- 在配置类里增加一个方法,方法上加@Bean:Spring将会调用这个方法,把方法的返回值放到容器里
示例:要把SAXReader对象放到容器里
- 在pom.xml里添加dom4j的依赖坐标
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.4</version>
</dependency>
- 创建一个配置类(配置类必须在扫描范围内),类里的方法上使用@Bean注解
@Configuration
public class DemoConfig {
/**
* 把SAXReader对象放到Spring容器里进行管理
*/
@Bean
public SAXReader saxReader(){
return new SAXReader();
}
}
- 测试:能够从容器里获取到SAXReader对象,说明@Bean注册bean成功了
@SpringBootTest
public class Demo01IocTest {
@Autowired
private SAXReader reader;
@Test
public void test(){
System.out.println("reader = " + reader);
}
}
1.3 @Import【了解】
适合于不在扫描范围内的类,要把对象交给Spring管理。有以下三个类,不在扫描范围内
1.3.1 @Import直接导入类
在引导类或者在配置类上加注解:@Import({类名1.class, 类名2.class, 类名3.class, ....})
例如:
- 引导类上使用@Import把这些类交给Spring管理
@SpringBootApplication
@Import({Demo01Bean.class, Demo02Bean.class, Demo03Bean.class})
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
}
- 测试效果:从容器里可以得到这三个bean对象,说明这三个类对象已经交给Spring管理了
@SpringBootTest
public class Demo01IocTest {
/**
* 注入Spring容器对象
*/
@Autowired
public ApplicationContext app;
@Test
public void testImport(){
//从Spring容器里分别获取bean对象并打印
System.out.println(app.getBean(Demo01Bean.class));
System.out.println(app.getBean(Demo02Bean.class));
System.out.println(app.getBean(Demo02Bean.class));
}
}
1.3.2 @Import导入ImportSelector类
在引导类或配置类上加注解@Import(ImportSelector接口的实现类.class)
。接口实现类里可以选择一批类名,Spring将会把这些类创建对象放到容器里
- 准备一个ImportSelector的实现类。
package com.itheima.importer;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
* @author liuyp
* @since 2023/08/27
*/
public class ItcastImporter implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"cn.itcast.bean.Demo01Bean", "cn.itcast.bean.Demo02Bean", "cn.itcast.bean.Demo03Bean"};
}
}
- 在引导类或者配置类上注解
package com.itheima;
import cn.itcast.bean.Demo01Bean;
import cn.itcast.bean.Demo02Bean;
import cn.itcast.bean.Demo03Bean;
import com.itheima.importer.ItcastImporter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
/**
* @author liuyp
* @since 2023/08/26
*/
@SpringBootApplication
// 添加@Import注解,导入ImportSelector接口的实现类:ItcastImport
@Import(ItcastImporter.class)
public class Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Demo1Application.class, args);
}
}
小结
如果有一些类,需要把类的对象交给Spring进行管理,那么:
情况1:是自己写的类
给类上加@Component或衍生注解
把类写到扫描范围内(引导类所在的包下边)
情况2:是第三方的类(jar包里的类)
创建一个配置类,类上加@Configuration
类里加方法,方法上加@Bean:Spring会调用这个方法,把方法的返回值放到容器里
情况3:在扫描范围之外的类
在引导类或者在配置类上加@Import({类名.class, 类名.class, 类名.class, ...})
在引导类或者在配置类上加@Import(ImportSelector接口的实现类.class)
我们需要提前准备一个Java类,实现ImportSelector接口
重写接口的selectImports方法:方法里返回一批类名的数组
Spring将会调用这个方法,把这些类名对应的类,创建对象放到容器里
2. 配置bean的注解
2.1 注解介绍
注解 | 作用 |
---|---|
@Scope | 用在bean对象上,设置bean对象的作用域。告诉Spring是以单例模式还是多例模式,或者其它模式来维护这个bean对象 |
@PostConstruct | 用在bean对象里的方法上,这个方法将会在Spring创建bean对象之后,执行一次。 这个方法通常被称为bean的初始化方法 |
@PreDestroy | 用在bean对象里的方法上,这个方法将会在Spring销毁bean对象之前,执行一次。这个方法通常被称为bean的销毁方法 |
@Scope:加在bean对象上,用于设置bean对象的作用域
-
如果一个bean对象上没有加此注解:Spring默认以单例模式维护bean对象
从容器里获取这个bean对象,无论获取几次,得到的都是同一个对象
-
如果想要修改bean的作用域,可以给bean对象加注解
@Scope(“singleton”):单例的。默认就是单例的
@Scope(“prototype”):多例的。从容器里每次获取这个bean对象,Spring都将会创建一个新对象给我们
其它取值:
- request:一次请求内是同一个对象,不同请求会有新的
- session:一次会话内是同一个对象,不同会话会有新的
- application:服务端只要不关闭重启,就是同一个对象
-
实际开发中:绝大多数情况下,都使用默认的单例
2.2 效果演示
@Scope效果
bean对象上加@Scope设置作用域
@Scope("singleton")
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
测试:从容器里多次获取相同的bean,如果是单例,获取的是同一个
@Test
public void testScope(){
//第1次从容器里获取DemoController对象
DemoController demo01 = app.getBean(DemoController.class);
System.out.println("demo01 = " + demo01);
//第2次从容器里获取DemoController对象
DemoController demo02 = app.getBean(DemoController.class);
System.out.println("demo02 = " + demo02);
//如果bean对象上有@Scope("singleton")或没有此注解的默认情况下,结果是true,Spring是以单例模式维护bean对象的
//如果bean对象上有@Scope("prototype"),结果是false,Spring将会以多例形式维护bean对象,每次获取时Spring都会创建新的对象
System.out.println(demo01 == demo02);
}
@PostConstruct和@PreDestroy
作用:
- @PostConstruct:加在方法上,方法就是在bean对象被创建成功之后执行的方法
- @PreDestroy:加在方法上,方法就是在bean对象销毁之前执行的方法
注意:
-
应用于非Lazy的单例bean对象上。
如果单例bean上
@Lazy
:- 表示让Spring不要一启动就创建此单例bean对象,而是晚一点,在第一次使用这bean时再创建
- 创建之后,仍然以单例模式维护这个bean对象
使用场景:
-
如果服务器一启动,就要做某些事情:在bean里增加一个方法,加
@PostConstruct
当服务器启动时,Springboot将会自动扫描所有的类,找到所有单例bean,马上创建对象放到容器里==>服务器一启动,就创建单例bean对象
-
如果在服务器关闭时,有一些收尾的工作:在bean里增加一个方法,加
@PreDestroy
当服务器关闭时,SpringBoot将会先销毁容器里所有的单例bean对象。在销毁之前,会先调用每个bean对象的销毁方法
@Scope("singleton")
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PostConstruct
public void init(){
System.out.println("init,@PostConstructor,方法将会在当前bean对象创建成功之后自动执行");
}
@PreDestroy
public void destroy(){
System.out.println("destroy, @PreDestroy,方法将在bean对象销毁之前先执行一次");
}
}
小结
如果一个类已经让Spring帮我们创建完bean对象,我们可以告诉Spring如何管理这个bean对象:
如果想要单例bean,每次获取这个bean都是同一个对象:可以什么都不加,或者在bean对象上加@Scope("singleton")
如果想要多例bean,每次获取都要新的bean对象:要在bean上加@Scope("prototype")
如果想让Spring在创建bean对象之后,立即调用某个方法:就在方法上加@PostConstruct
如果想让Spring在销毁bean对象之前,先执行一次某方法:就在方法上加@PreDestroy
3. 依赖注入的注解
注解 | 说明 |
---|---|
@Autowired | byType注入。根据依赖项的类型,从容器里查找此类型的bean对象,注入进来 |
@Qualifier | 需要在byType基础上使用。表示根据指定的名称,从容器里查找bean对象,注入进来 |
@Resource | byName注入。根据依赖项的名称,从容器里查找此名称的bean对象,注入进来 |
@Value | 通常用于注入简单值(基本数据类型及其包装类,和String) |
二、SpringBoot配置参数
项目里边配置参数
有三种方式:
application.yaml
application.yml
application.properties
三种方式的使用:
- 实际开发的时候,选中其中的一种格式使用。不要混用
- 三种混用时的优先级:
application.yaml
<application.yml
<application.properties
项目外修改参数
有两种方式
- Java属性参数VM Options:在命令行里执行
java
命令时添加的参数。java -D参数名=值 -jar jar包名
- 程序参数Programs arguments:在命令行里执行命令时,给引导类设置的参数。
java -jar jar包名 --参数名=值
两种方式的使用:
- VM Options:
java -Dserver.port=8084 -jar jar包名称
- Program Arguments:
java -jar jar包名称 --server.port=8085
- 两种都用的优先级是:VM Options < Program Arguments
在idea里启动时,也能够配置这两个参数:
- 配置VM Options
- 配置Program Arguments
三、SpringBoot原理【面试】
Spring存在的问题:
- 依赖冲突问题:依赖比较多的话,各个依赖的版本号容易出现冲突
- 要导入的依赖太多
- 配置太麻烦:很多功能都必须要自己提供配置参数,没有默认值
- 整合其它框架太麻烦:每整合一个框架,就需要做专门的配置,甚至需要编写整合的代码
SpringBoot有什么好处:
-
提供了依赖版本锁定:只要项目里导入了父工程坐标,父工程会帮我们锁定一些常用依赖的版本号。依赖冲突的情况会减少
-
提供了起步依赖:一个起步依赖,实际是一批功能相关的依赖的集合体
-
提供了大量的默认配置:很多功能不需要加配置,使用默认值就可以正常运行。下面这个jar包里的这个文件,提供了默认值
-
提供了自动装配:整合其它框架变得非常轻松。很多时候,只要导入第三方框架的起步依赖,就自动整合好了
-
内置Tomcat
1. SpringBoot父工程坐标
SpringBoot父工程坐标:里边已经提前帮我们锁定了大量常用依赖的版本号。有效减少了依赖冲突的机率
<!--SpringBoot的父工程坐标-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>
我们在导入其它依赖的时候,如果这个依赖版本已经被SpringBoot提前锁定好了,就不需要我们再给设置版本号了
2. SpringBoot起步依赖
我们在开发中,可能需要导入数十甚至数百个依赖,pom.xml文件可以要写数百行甚至数千行。
SpringBoot提供了起步依赖:一个起步依赖,就是一批相关功能的依赖集合体。
即:导入一个起步依赖,实际上导入了一批依赖包
- SpringBoot官方提供的起步依赖,名称通常是:spring-boot-starter-xxx
- 第三方技术自己提供的起步依赖,名称通常是:xxx-spring-boot-starter
3. SpringBoot自动装配
3.1 什么是自动装配
SpringBoot在整合其它框架时,自动装配功能可以有效的减少整合的难度和复杂度。让很多框架与SpringBoot的整合变得极其简单
整合后的效果:
- 只要导入某框架的起步依赖,就可以直接使用这个框架了
- 只要导入某框架的起步依赖,就可以直接从IoC容器里获取框架相关的bean对象了
3.2 自动装配的效果演示
以druid起步依赖为例:
-
只要pom.xml里添加了druid的起步依赖,SpringBoot就会自动创建druid的连接池
DruidDataSource
对象放到IoC容器里。 -
我们可以直接注入使用druid连接池对象,而不用自己创建对象了
直接注入DruidDataSource
对象的示例代码:
package com.itheima;
import com.alibaba.druid.pool.DruidDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author liuyp
* @since 2023/08/26
*/
@SpringBootTest
public class DemoDruidStarterTest {
/**注入Druid连接池对象*/
@Autowired
private DruidDataSource dataSource;
@Test
public void test(){
//打印druid连接池对象。如果打印出来不是null,就说明IoC容器里已经有了Druid连接池对象。
//但是这个对象并不是我们创建然后放到容器里的,那么 谁帮我们创建了Druid连接池对象,并放到IoC容器里的呢?SpringBoot
System.out.println("dataSource = " + dataSource);
}
}
3.3 自动装配的原理
面试常问:SpringBoot工程的启动过程,和自动装配原理
启动过程:
- 先运行引导类的main方法,main方法里执行的是:
SpringApplication.run(引导类.class, args)
SpringApplication.run(引导类.class, args)
:- 把引导类传递给SpringApplication类
- SpringApplication创建了IoC容器对象
- 开始解析引导类上注解
@SpringBootApplication
,注解开始生效
@SpringBootApplication
注解的作用:@ComponentScan
:引导类会自动扫描组件。默认扫描的是“引导类所在的包”@SpringBootConfiguration
:是个组合注解,实际上引用的是@Configuration
,说明引导类也是个配置类@EnableAutoConfigurutaion
:要开启自动装配功能
自动装配原理
-
引导类上
@SpringBootApplication
,实际上引用了@EnableAutoConfiguration
-
@EnableAutoConfiguration
使用的是@Import(AutoConfigurationImportSelector.class)
-
AutoConfigurationImportSelector
是ImportSelector接口的实现类:会重写接口的
selectImports
方法在
selectImports
方法里:- 会扫描所有类路径里的
META-INF/spring.factories
文件 - 读取文件里
...EnableAutoConfiguration=类名1.class,类名2.class,类名3.class,...
- Spring然后找到这些类名对应的类
- 如果这些类上的
@Conditionalxxxx
注解满足条件,Spring就会创建对象放到IoC容器里
- 会扫描所有类路径里的
-
我们的代码里,就可以直接使用
@Autowired
注入需要用的bean对象了
四、案例-SpringBoot自定义启动器
需求
把阿里云的OSS工具,抽取到一个公用的模块里,把这个模块制作成一个启动器starter
在测试项目里引入starter,测试能否直接注入OSS工具对象,实现上传文件
分析
功能分析
准备资料
阿里云启动器starter模块的pom.xml里需要添加的依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
阿里云的配置参数(使用时改成自己的)
endpoint: https://oss-cn-xxxx.xxxxxxx.com
accessKeyId: xxxxxxxxxxxxxxxxxxxxxxxx
accessKeySecret: xxxxxxxxxxxxxxxxxxxxxx
bucketName:xxxxxxxx
实现
自定义启动器模块
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>
<dependencies>
<!--web层起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--阿里云OSS的依赖包-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--可选的包,用于@ConfigurationProperties不爆红-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
AliOSSUtils
package com.aliyun;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSUtils {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 把上传到oss,并返回url路径
return upload(inputStream, originalFilename);
}
/**
* 上传文件到阿里云OSS
* @param inputStream 文件的输入流,用于读取要上传的文件数据
* @param filename 文件原始名称
* @return 文件的url路径
*/
public String upload(InputStream inputStream, String filename) throws IOException {
//重命名文件,避免文件覆盖
filename = UUID.randomUUID() + filename.substring(filename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, filename, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + filename;
//关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.aliyun.AliOSSUtils
测试工程引用启动器
pom.xml
修改pom.xml,添加启动器的依赖坐标
<!--把aliyun启动器导进来了-->
<dependency>
<groupId>org.example</groupId>
<artifactId>day14-aliyun-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
application.yaml配置阿里云OSS参数
aliyun:
oss:
endpoint: https://oss-cn-xxxx.xxxxx.com
accessKeyId: xxxxxxxxxxxxxxxx
accessKeySecret: xxxxxxxxxxxxxxx
bucketName: xxxxxx
使用AliOSSUtils上传文件
package com.itheima;
import com.aliyun.AliOSSUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import java.io.FileInputStream;
/**
* @author liuyp
* @since 2023/08/27
*/
@SpringBootTest
public class Demo02AliyunTest {
@Autowired
private AliOSSUtils ossUtils;
@Test
public void test() throws Exception{
File file = new File("C:\\Users\\liuyp\\Desktop\\img\\10.jpg");
FileInputStream is = new FileInputStream(file);
//上传文件到阿里云OSS,得到文件的url
String url = ossUtils.upload(is, file.getName());
is.close();
System.out.println("url = " + url);
}
}
测试
itheima-liuyp
#### 使用AliOSSUtils上传文件
```java
package com.itheima;
import com.aliyun.AliOSSUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import java.io.FileInputStream;
/**
* @author liuyp
* @since 2023/08/27
*/
@SpringBootTest
public class Demo02AliyunTest {
@Autowired
private AliOSSUtils ossUtils;
@Test
public void test() throws Exception{
File file = new File("C:\\Users\\liuyp\\Desktop\\img\\10.jpg");
FileInputStream is = new FileInputStream(file);
//上传文件到阿里云OSS,得到文件的url
String url = ossUtils.upload(is, file.getName());
is.close();
System.out.println("url = " + url);
}
}