JavaWeb学习笔记(Day14)

一、配置优先级

在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。通过以上的测试,我们也得出了优先级**(从低到高):**

  • application.yaml(忽略)

  • application.yml

  • application.properties

  • java系统属性(-Dxxx=xxx)

  • 命令行参数(--xxx=xxx)

二、Bean管理

1. 获取Bean
(1) 常用方法

① 根据name获取bean

java 复制代码
Object getBean(String name)

② 根据类型获取bean

java 复制代码
<T> T getBean(Class<T> requiredType)

③ 根据name获取bean(带类型转换)

java 复制代码
<T> T getBean(String name, Class<T> requiredType)
(2) 代码实现

测试类:

java 复制代码
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {

    @Autowired
    private ApplicationContext applicationContext; //IOC容器对象

    //获取bean对象
    @Test
    public void testGetBean(){
        //根据bean的名称获取
        DeptController bean1 = (DeptController) applicationContext.getBean("deptController");
        System.out.println(bean1);

        //根据bean的类型获取
        DeptController bean2 = applicationContext.getBean(DeptController.class);
        System.out.println(bean2);

        //根据bean的名称 及 类型获取
        DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
        System.out.println(bean3);
    }
}

核心逻辑:

① ApplicationContext 是怎么来的?

ApplicationContextSpring 框架中 IOC 容器的核心接口,你可以把它理解成 Spring 项目里管理所有 Bean 的 "大管家"

它的核心作用是:

  • 管理 Bean 的生命周期 :项目启动时,Spring 会把配置好的 Bean(比如你代码里的DeptController)都创建好,存到ApplicationContext这个容器里;
  • 提供 Bean 的获取方式 :就是你代码里用的getBean()方法 ------ 不管按名称、按类型,都能从容器里拿到对应的 Bean 对象;
2. Bean作用域
(1) 在Spring中支持五种作用域,后三种在web环境才生效:
作用域 说明
singleton 容器内同名称的bean只有一个实例(单例)(默认)
prototype 每次使用该bean时会创建新的实例(非单例)
request 每个请求范围内会创建新的实例(web环境中,了解)
session 每个会话范围内会创建新的实例(web环境中,了解)
application 每个应用范围内会创建新的实例(web环境中,了解)

知道了bean的5种作用域了,我们要怎么去设置一个bean的作用域呢?

  • 可以借助Spring中的@Scope注解来进行配置作用域
(2) 代码示例

① 测试一

  • 控制器
java 复制代码
//默认bean的作用域为:singleton (单例)
@Lazy //延迟加载(第一次使用bean对象时,才会创建bean对象并交给ioc容器管理)
@RestController
@RequestMapping("/depts")
public class DeptController {

    @Autowired
    private DeptService deptService;

    public DeptController(){
        System.out.println("DeptController constructor ....");
    }

    //省略其他代码...
}
  • 测试类
java 复制代码
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {

    @Autowired
    private ApplicationContext applicationContext; //IOC容器对象

    //bean的作用域
    @Test
    public void testScope(){
        for (int i = 0; i < 10; i++) {
            DeptController deptController = applicationContext.getBean(DeptController.class);
            System.out.println(deptController);
        }
    }
}

② 测试二

修改控制器DeptController代码:

java 复制代码
@Scope("prototype") //bean作用域为非单例
@Lazy //延迟加载
@RestController
@RequestMapping("/depts")
public class DeptController {

    @Autowired
    private DeptService deptService;

    public DeptController(){
        System.out.println("DeptController constructor ....");
    }

    //省略其他代码...
}
3. 第三方Bean
(1) 存在的问题

如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。

(2) 解决方案
方案一:在启动类上启动@Bean标识的方法(不建议)
java 复制代码
@SpringBootApplication
public class SpringbootWebConfig2Application {

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

    //声明第三方bean
    @Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
    public SAXReader saxReader(){
        return new SAXReader();
    }
}

xml文件:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<emp>
    <name>Tom</name>
    <age>18</age>
</emp>

测试类:

java 复制代码
@SpringBootTest
class SpringbootWebConfig2ApplicationTests {

    @Autowired
    private SAXReader saxReader;

    //第三方bean的管理
    @Test
    public void testThirdBean() throws Exception {
        Document document = saxReader.read(this.getClass().getClassLoader().getResource("1.xml"));
        Element rootElement = document.getRootElement();
        String name = rootElement.element("name").getText();
        String age = rootElement.element("age").getText();

        System.out.println(name + " : " + age);
    }

    //省略其他代码...
}

说明:以上在启动类中声明第三方Bean的作法,不建议使用(项目中要保证启动类的纯粹性)

解决方案2:在配置类中定义@Bean标识的方法
  • 如果需要定义第三方Bean时, 通常会单独定义一个配置类
java 复制代码
@Configuration //配置类  (在配置类当中对第三方bean进行集中的配置管理)
public class CommonConfig {

    //声明第三方bean
    @Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
          //通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名
    public SAXReader reader(DeptService deptService){
        System.out.println(deptService);
        return new SAXReader();
    }

}

核心逻辑:@Bean 方法的参数会自动从容器中找匹配的 Bean 注入

你写的reader(DeptService deptService)这个方法,Spring 会帮你做这几件事:

  • 执行reader()方法创建 SAXReader 之前,Spring 先看方法有没有参数(这里参数是 DeptService);
  • Spring 会去自己的 IOC 容器里,找类型为 DeptService的 Bean(就是上面 @Service 注解生成的那个);
  • 把找到的 DeptService Bean 自动 "传" 到方法参数里,所以你在方法里能直接用deptService(比如打印它);
  • 方法执行完返回 SAXReader 对象,Spring 再把这个 SAXReader 注册成容器里的 Bean(默认名称是方法名reader)。

三、SpringBoot 原理

1. 自动配置
(1) 存在的问题

① 在SpringBoot项目 spring-boot-web-config2 工程中,通过坐标引入itheima-utils依赖

java 复制代码
@Component
public class TokenParser {
    public void parse(){
        System.out.println("TokenParser ... parse ...");
    }
}

② 在测试类中,添加测试方法

java 复制代码
@SpringBootTest
public class AutoConfigurationTests {

    @Autowired
    private ApplicationContext applicationContext;


    @Test
    public void testTokenParse(){
        System.out.println(applicationContext.getBean(TokenParser.class));
    }

    //省略其他代码...
}

③ 测试方法

异常信息描述: 没有com.example.TokenParse类型的bean

说明:在Spring容器中没有找到com.example.TokenParse类型的bean对象

思考:引入进来的第三方依赖当中的bean以及配置类为什么没有生效?

  • 原因在我们之前讲解IOC的时候有提到过,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。

  • SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。

  • 当前包:com.itheima, 第三方依赖中提供的包:com.example(扫描不到)

(2) 解决方案
方案一:@ComponentScan组件扫描(不推荐)
java 复制代码
@SpringBootApplication
@ComponentScan({"com.itheima","com.example"}) //指定要扫描的包
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}

缺点:

  • 使用繁琐

  • 性能低

方案二:@Import导入(一般般)

导入形式主要有以下几种:

  • 导入普通类

  • 导入配置类

  • 导入ImportSelector接口实现类

① 使用@Import导入普通类:
java 复制代码
@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
@SpringBootApplication
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}
② 使用@Import导入配置类:
  • 配置类
java 复制代码
@Configuration
public class HeaderConfig {
    @Bean
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

    @Bean
    public HeaderGenerator headerGenerator(){
        return new HeaderGenerator();
    }
}
  • 启动类
java 复制代码
@Import(HeaderConfig.class) //导入配置类
@SpringBootApplication
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}
③ 使用@Import导入ImportSelector接口实现类:
  • ImportSelector接口实现类
java 复制代码
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //返回值字符串数组(数组中封装了全限定名称的类)
        return new String[]{"com.example.HeaderConfig"};
    }
}

核心逻辑:

return new String[]{"com.example.HeaderConfig"}:这是核心!返回值是类的全限定名数组 (包名 + 类名),意思是 "告诉 Spring,把这些类创建成 Bean,加入容器"。这里只返回了HeaderConfig,就是让 Spring 强制注册这个类为 Bean。

  • 启动类
java 复制代码
@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}
方案三:使用第三方依赖提供的 @EnableXxxxx注解(推荐)
  • 第三方依赖中提供的注解
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig { 
}
  • 在使用时只需在启动类上加上@EnableXxxxx注解即可
java 复制代码
@EnableHeaderConfig  //使用第三方依赖提供的Enable开头的注解
@SpringBootApplication
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}
2. 原理分析

(1) 要搞清楚SpringBoot的自动配置原理,要从SpringBoot启动类上使用的核心注解@SpringBootApplication开始分析:

在@SpringBootApplication注解中包含了:

  • 元注解

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

  • @ComponentScan

① @Target(ElementType.TYPE)

  • 核心作用:指定当前注解可以标注在哪些代码元素上。
  • ElementType.TYPE 表示这个注解只能用在类、接口、枚举类型上,不能用在方法、字段、参数等位置。
  • @SpringBootApplication 的意义:这就是为什么它只能加在 SpringBoot 的启动类(类级别)上,而不能加在方法或字段上。

② @SpringBootConfiguration

  • @SpringBootConfiguration注解上使用了@Configuration,表明SpringBoot启动类就是一个配置类。
  • @Indexed注解,是用来加速应用启动的(不用关心)。

③ @ComponentScan

  • @ComponentScan注解是用来进行组件扫描的,扫描启动类所在的包及其子包下所有被@Component及其衍生注解声明的类。

  • SpringBoot启动类,之所以具备扫描包功能,就是因为包含了@ComponentScan注解。

④ @EnableAutoConfiguration

封装了@Import注解(Import注解中指定了一个ImportSelector接口的实现类)

  • 在实现类重写的selectImports()方法,读取当前项目下所有依赖jar包中META-INF/spring.factories、META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports两个文件里面定义的配置类(配置类中定义了@Bean注解标识的方法)。
3. @Conditional
(1) 介绍

① 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。

② 位置:方法、类

③ @Conditional本身是一个父注解,派生出大量的子注解:

  • @ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。

  • @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。

  • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。

(2) 代码示例
① @ConditionalOnClass注解
java 复制代码
@Configuration
public class HeaderConfig {

    @Bean
    @ConditionalOnClass(name="io.jsonwebtoken.Jwts")//环境中存在指定的这个类,才会将该bean加入IOC容器
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
    //省略其他代码...
}

核心逻辑:

@ConditionalOnClass(name="io.jsonwebtoken.Jwts"):条件判断

  • 作用:@Bean加 "注册条件"------ 只有当项目的类路径(classpath) 中存在io.jsonwebtoken.Jwts这个类时,Spring 才会执行这个headerParser()方法,把HeaderParser注册成 Bean;如果不存在这个类,这个@Bean方法会被直接忽略,HeaderParser不会被创建。
  • HeaderParser大概率是用来解析 JWT 令牌的工具类,而io.jsonwebtoken.Jwts是 JWT 框架(jjwt)的核心类 ------ 如果你的项目没引入 jjwt 的依赖(比如 Maven 的 jjwt 坐标),Jwts类就不存在,此时创建HeaderParser不仅没用,还可能因为缺少依赖报错。

pom.xml

java 复制代码
<!--JWT令牌-->
<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
</dependency>
② @ConditionalOnMissingBean注解
java 复制代码
@Configuration
public class HeaderConfig {

    @Bean
    @ConditionalOnMissingBean //不存在该类型的bean,才会将该bean加入IOC容器
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
    //省略其他代码...
}

SpringBoot在调用@Bean标识的headerParser()前,**IOC容器中是没有HeaderParser类型的bean,**所以HeaderParser对象正常创建,并注册到IOC容器中。

再次修改@ConditionalOnMissingBean注解:

java 复制代码
@Configuration
public class HeaderConfig {

    @Bean
    @ConditionalOnMissingBean(name="deptController2")//不存在指定名称的bean,才会将该bean加入IOC容器
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
    //省略其他代码...
}

核心逻辑:

Spring 会先检查 IoC 容器:有没有名称为deptController2的 Bean(不管这个 Bean 是什么类型)

  • ✅ 容器中没有deptController2的 Bean → 执行headerParser()方法,注册HeaderParser
  • ❌ 容器中deptController2的 Bean(比如有个@Controller("deptController2") public class DeptController {})→ 跳过方法,HeaderParser不注册。

再次修改@ConditionalOnMissingBean注解:

java 复制代码
@Configuration
public class HeaderConfig {

    @Bean
    @ConditionalOnMissingBean(HeaderConfig.class)//不存在指定类型的bean,才会将bean加入IOC容器
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
    //省略其他代码...
}

核心逻辑:按「Bean 类型」判断

Spring 会检查 IoC 容器:有没有HeaderConfig类型的 Bean(不管这个 Bean 叫什么名字)

⚠️ 这里有个关键坑(新手必踩):HeaderConfig上加了@Configuration注解,而@Configuration本质是@Component的变种 ------Spring 启动时,会自动把HeaderConfig本身注册成 Bean(类型就是 HeaderConfig)。

所以这个判断的结果永远是:

  • ❌ 容器中一定有 HeaderConfig类型的 Bean → 永远跳过headerParser()方法 → HeaderParser永远不会被注册!
③ @ConditionalOnProperty注解(这个注解和配置文件当中配置的属性有关系)

先在application.yml配置文件中添加如下的键值对:

XML 复制代码
name: itheima

在声明bean的时候就可以指定一个条件@ConditionalOnProperty

java 复制代码
@Configuration
public class HeaderConfig {

    @Bean
    @ConditionalOnProperty(name ="name",havingValue = "itheima")//配置文件中存在指定属性名与值,才会将bean加入IOC容器
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

    @Bean
    public HeaderGenerator headerGenerator(){
        return new HeaderGenerator();
    }
}

核心逻辑: @ConditionalOnProperty注解

这个注解是 SpringBoot 特有的 "配置条件注解",核心是读取配置文件的属性,判断是否满足条件,满足才注册 Bean

4. 案例
(1) 需求

自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOSSUtils的自动配置。

目标:引入起步依赖引入之后,要想使用阿里云OSS,注入AliyunOSSUtils直接使用即可。

(2) 思路分析

① 创建自定义starter模块(进行依赖管理)

  • 把阿里云OSS所有的依赖统一管理起来

② 创建autoconfigure模块

  • 在starter中引入autoconfigure (我们使用时只需要引入starter起步依赖即可)

③ 在autoconfigure中完成自动配置

  • 定义一个自动配置类,在自动配置类中将所要配置的bean都提前配置好

  • 定义配置文件,把自动配置类的全类名定义在配置文件中

(3) 代码实现

① 在类上添加的@Component注解还有用吗?

1). 先明确@Component的作用

@Component的核心作用是:告诉 Spring"扫描到这个类时,把它注册成 IoC 容器里的 Bean" 。但这个作用生效的前提是:Spring 能扫描到这个类所在的包

2). SpringBoot 的默认扫描规则

SpringBoot 启动类上的@SpringBootApplication注解,自带了@ComponentScan(组件扫描)功能,默认只扫描:启动类所在的包,以及这个包的所有子包

比如:

  • 如果启动类在com.example.demo包下,Spring 只会扫描com.example.democom.example.demo.servicecom.example.demo.controller等子包;
  • 而你截图里的类在com.aliyun.oss包下,这个包不在启动类的包范围内,所以 Spring根本不会扫描这个包

② AliOSSAutoConfiguration类:

java 复制代码
@Configuration//当前类为Spring配置类
@EnableConfigurationProperties(AliOSSProperties.class)//导入AliOSSProperties类,并交给SpringIOC管理
public class AliOSSAutoConfiguration {


    //创建AliOSSUtils对象,并交给SpringIOC容器
    @Bean
    public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties){
        AliOSSUtils aliOSSUtils = new AliOSSUtils();
        aliOSSUtils.setAliOSSProperties(aliOSSProperties);
        return aliOSSUtils;
    }
}

核心逻辑:

1). @EnableConfigurationProperties

这个注解加在配置类上,传入AliOSSProperties.class,会做两件事:

  • 第一件事 :把AliOSSProperties注册成 Spring IoC 容器中的 Bean (相当于给AliOSSProperties加了@Component);
  • 第二件事 :启用AliOSSProperties上的@ConfigurationProperties注解,让 Spring 把配置文件中ali.oss.*的属性值,自动绑定到AliOSSProperties对象的字段中(比如把ali.oss.access-key的值赋给accessKey字段)。

2). 看配置类中的@Bean方法:

  • AliOSSProperties aliOSSProperties:方法参数 → Spring 会自动从 IoC 容器中找到AliOSSProperties类型的 Bean,注入到这个参数里(依赖注入,不用你手动 new);
  • aliOSSUtils.setAliOSSProperties(aliOSSProperties):把注入进来的 "绑定了配置的 AliOSSProperties 对象",通过 setter 方法 "塞" 到AliOSSUtils实例中;
  • 最终返回的AliOSSUtils对象,就拥有了 OSS 的配置信息,后续调用它的uploadFile方法时,就能直接用这些配置。

③ 在aliyun-oss-spring-boot-autoconfigure模块中的resources下,新建自动配置文件:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

java 复制代码
com.aliyun.oss.AliOSSAutoConfiguration

1). 为什么需要这个文件?

这个文件的作用就是:给 SpringBoot "递一张地址单",告诉它 "去加载 com.aliyun.oss.AliOSSAutoConfiguration 这个配置类",不管这个类在哪个包,SpringBoot 都会找到它。

2). 这个文件的具体作用(串起整个流程)

  • 你在这个文件里写了com.aliyun.oss.AliOSSAutoConfiguration(配置类的全限定名);
  • 当用户的项目引入你的 aliyun-oss-spring-boot-starter 依赖后,这个文件会被打包到 Starter 的 Jar 包中;
  • 用户启动 SpringBoot 项目时,SpringBoot 会自动扫描所有依赖 Jar 包中的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件;
  • 读取到你写的AliOSSAutoConfiguration类名,然后加载这个配置类;
  • 加载后,AliOSSAutoConfiguration里的逻辑(注册 AliOSSProperties、创建 AliOSSUtils Bean)就会执行,最终用户能直接 @Autowired 使用 AliOSSUtils。
相关推荐
时代的凡人8 小时前
0208晨间笔记
笔记
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
testpassportcn9 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
游乐码12 小时前
c#变长关键字和参数默认值
学习·c#
饭碗、碗碗香13 小时前
【Python学习笔记】:Python的hashlib算法简明指南:选型、场景与示例
笔记·python·学习
Wils0nEdwards13 小时前
初中化学1
笔记
魔力军13 小时前
Rust学习Day4: 所有权、引用和切片介绍
开发语言·学习·rust
wubba lubba dub dub75013 小时前
第三十六周 学习周报
学习
学编程的闹钟14 小时前
PHP字符串表示方式全解析
学习
Lbs_gemini060314 小时前
01-01-01 C++编程知识 C++入门 工具安装
c语言·开发语言·c++·学习·算法