今天学习了 SpringBoot 中的一些注解,不过有些注解是 Spring 中的,它们分别是 @Configuration、@Import、@Bean、@Conditional、@ImportResource、@ConfigurationProperties、@EnableConfigurationProperties 注解。

学完这些注解之后,突然有一个想法。就是之前在 SpringBoot 的 Web 模块的时候,SpringBoot 好像还是需要我们来自己编写拦截器,这就感到费时间,有点不符合 SpringBoot 的少量配置的特点。暑假想自己尝试写一个基于 Spring MVC 的拦截器的第三方库。

7af40ad162d9f2d314edf006abec8a136227cc5f

我觉得这个还是等到把雷神的 SpringBoot 给弄完,能在暑假之内做出来是最好的了。

Configuration 注解

Configutation 注解,标记在某一个类上,这个类就成为了一个配置类了。标记的效果等同于在 Spring 的 xml 配置文件一样。

有一个名词叫”注册“,注册的意思就是创建一个对象,像是 new Random(); 这种写法。

既然和 xml 的配置文件一样注册对象,那它向容器中注册对象的方式是这样的。也就是说,需要先声明一个方法,需要在方法上面标记一个 Bean 注解,Bean 注解就是在 Spring 的 ioc 容器(后续简称容器)中添加对象,默认是以方法名作为对象id,返回类型就是对象的类型。

@Configuration
public class MyConfig {
    
    @Bean("zhangsan")
    public User user01() {
        User zhangsan = new User("zhangsan", 10);
        return zhangsan;
    }
    
    @Bean
    public User user02() {
        User lisi = new User("lisi", 10);
        return lisi;
    }
    
    @Bean
    public Pet pet01() {
        return new Pet("tomcat");
    }

    @Bean
    public Pet pet02() {
        return new Pet("tomcat222");
    }
    
}

其中,我们可以自己在 Bean 注解中指定id,比如 @Bean("zhangsan"),指定名字完之后,那么这个对象的 id 就不再是 user01 ,而是 zhangsan 。

如何得到容器中的对象

获取容器的对象也非常简单,和以前使用 ApplicationContext 对象一样,也是利用 getBean 来获取的。

// Spring IOC容器
ConfigurableApplicationContext run = SpringApplication.run(SpringBootApplication.class, args);
// 获取 user02
User user02 = (User) run.getBean("user02");
System.out.println(user02);
// 获取 zhangsan
User zhangsan = run.getBean("zhangsan", User.class);
System.out.println(zhangsan);

运行结果如下:

User{brand='lisi', age=10}
User{brand='zhangsan', age=10}

单实例

如果我们多次获取容器中的对象,那么我们获取到的对象是否为同一个呢?通过以下代码进行验证。

// 两个实例是不是一样的?
Pet pet1 = (Pet) run.getBean("pet01");
Pet pet2 = (Pet) run.getBean("pet01");
System.out.println("pet1==pet2 ? " + (pet1 == pet2));

运行结果如下:

pet1==pet2 ? true

运行结果为 true ,意味着所得到的对象实例是单实例的。

配置类本身也是对象

我们创建的配置类其实本身也是一个对象。我们也可以将它给调用出来

MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);

运行结果如下:

top.bestguo.config.MyConfig$$EnhancerBySpringCGLIB$$137f765f@62315f22

不过,我们得到的配置类是一个加强版的类。一看到 CGLIB 就知道,这是一个被代理过的对象,里面的对象也是由 CGLIB 进行代理的。那它代理了什么呢?

其实,在 SpringBoot 2.3 之后的版本,由于 SpringBoot 2.3 是基于 Spring 5.2+ 的,Configuration 注解多了一个叫做 proxyBeanMethod 的属性,是因为它可以设置 true 和 false ,如果设置成了 true 就是这个对象是被代理的,设置成 false 就是未被代理的。

那,代理的意义何在呢?🧐🧐

proxyBeanMethod

该注解中的属性,默认值为 true 。

上面部分提到过这个属性设置成 true 和 false ,配置类分别就是被代理和未被代理的。上面也有提到,配置类也是一个对象,配置类对象里面不是有很多方法吗?那我们调用一下这里面的方法试试看。

如果我多次调用这个配置类对象中的方法,直接获取 ioc 容器中注册的对象,还是仅仅只是单纯的方法调用,每次的调用返回的对象都是不同的呢?

MyConfig bean = run.getBean(MyConfig.class);
User user01 = bean.user01();
User user011 = bean.user01();
System.out.println("从配置容器中调用的方法,是一样的吗?" + (user01 == user011));

运行结果如下,发现得到的方法是同一个对象。

从配置容器中调用的方法,是一样的吗?true

那我们把 proxyBeanMethod 值改成 false 会怎么样呢?运行结果如下。

top.bestguo.config.MyConfig@5bdaf2ce
从配置容器中调用的方法,是一样的吗?false

很显然,MyConfig 这个配置类不再被代理了,并且这两次的调用都是 false。那我们直接通过 getBean 的方式看看得到的两个对象是否为同一个。

User zhangsan = run.getBean("zhangsan", User.class);
User zhangsan2 = run.getBean("zhangsan", User.class);
System.out.println("是不是同一个?" + (zhangsan == zhangsan2));

运行结果如下,发现这是同一个对象。

是不是同一个?true

所以,proxyBeanMethod 对象的用途,如果设置成 true ,那么在调用 MyConfig 配置类中的方法时,就会先去拿到这个 Bean 的 id,来确认这个对象是不是存在于容器中,如果存在与容器中,调用配置类中的方法时就直接去拿容器中的对象。

如果是 false ,它就不会通过 id 去确认容器中是否存在该对象,相当于是直接调用 MyConfig 中的方法而已,并且这个方法返回的对象并不是从容器中直接获取的,而是单纯的在方法中 new 一个对象出来。每一次 new 出来的对象都是不同的。

使用场景 – 组件依赖

组件的意思就是容器中所实例化的对象。

一位名叫 zhangsan 的主人,它刚刚养了一只狗,这只狗狗的名字叫 ”tomcat“。刚没养几天,主人就要回老家三个月有事情,它只好将这只狗狗放到了宠物临时收养的机构进行收养。等主人回到家,回到宠物临时收养机构领取自己的宠物狗狗。

7af40ad162d9f2d314edf006abec8a136227cc5f

回到家之后,它去领养。宠物临时收养机构管理人员可能将该主人的狗狗信息丢了,只记得狗狗的名字;或者也有可能什么都记得,管理人员这种行为可将其类比成 proxyBeanMethod 值为 false 和proxyBeanMethod 值为 true 的两种情况。

以下是主人来领取宠物的代码。

public class MyConfig {

    @Bean("zhangsan")
    public User user01() {
        User zhangsan = new User("zhangsan", 10);
        zhangsan.setPet(pet01());
        return zhangsan;
    }

    @Bean
    public Pet pet01() {
        return new Pet("tomcat");
    }

}

如果 proxyBeanMethod 值为 false 时,也就是狗狗信息丢了,只记得狗狗的名字。我们来验证验证工作人员是否找对了。

// 组件依赖示例
User zhangsan = run.getBean("zhangsan", User.class);
// 判断用户的宠物是容器中的宠物吗
Pet pet1 = (Pet) run.getBean("pet01");
System.out.println("用户zhangsan的宠物是它的吗?" + (zhangsan.getPet() == pet1));

那么执行的结果如下,值为 false。很遗憾,工作人员的失职行为已经是非常严重的了。

用户zhangsan的宠物是它的吗?false

如果 proxyBeanMethod 值为 true 时,也就是狗狗信息还在。那么执行的结果如下。很好,工作人员做的不错。

用户zhangsan的宠物是它的吗?true

例子总结

proxyBeanMethod 为 true 或者为 false 时,对应的模式如下。

  • Full(proxyBeanMethods = true) – 如果组件之间有依赖,有依赖的就设置为 true
  • Lite(proxyBeanMethods = false) – 如果只是单纯的创建组件到容器中,且组件之间并没有依赖。那么就设置成为 false

Import 注解

直接通过该注解,将对象实例化之后保存到容器中。能标记的地方非常之多,比如 SpringBoot 的主程序类、配置类、被 Service、Repository、Controller 标记的地方都可以将其标记。

存在的意义就是,有些对象是封装在一个外部的 jar 包中的。封装在 jar 包中的源代码是无法直接更改的,也就是说没办法直接标记这些 Component 等等上面提及到的那些注解。所以可以通过这个注解来将对象保存到容器中。

@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = true)
public class MyConfig {
    // coding......
}

测试代码:

// 获取 User 类型的组件
String[] strings = run.getBeanNamesForType(User.class);
for (String string : strings) {
    System.out.println(string);
}
System.out.println(run.getBean(DBHelper.class));

运行结果如下,其中 DBHelper 本身就是来自于外部的 jar 包。

top.bestguo.bean.User
zhangsan
user02
ch.qos.logback.classic.db.DBHelper@6e4ea0bd

Conditional 注解

该注解也称之为条件注解,该注解的意图就是在特定的场合下,将某一些组件给保存到容器中。其中,它的衍生注解有很多种。如下图所示。

QQ截图20210626230931

ConditionalOnBean 注解

在学习的时候学习到了 ConditionalOnBean 这个注解,它的意思是当容器中存在某个组件时,那么就将这个对象给实例化保存到容器中,否则就不进行任何的操作。

它被标记到配置类中方法上

@Configuration(proxyBeanMethods = true)
public class MyConfig {

    @ConditionalOnBean(name = "pet01") // 当容器中有 pet01 的时候,那么就注册 zhangsan 组件。
    @Bean("zhangsan")
    public User user01() {
        User zhangsan = new User("zhangsan", 10);
        zhangsan.setPet(pet01());
        return zhangsan;
    }

    @Bean
    public User user02() {
        User lisi = new User("lisi", 10);
        return lisi;
    }

    @Bean
    public Pet pet01() {
        return new Pet("tomcat");
    }

    @Bean
    public Pet pet02() {
        return new Pet("tomcat222");
    }

}

运行测试代码

boolean pet01 = run.containsBean("pet01");
System.out.println("容器中有 pet01 组件吗?" + pet01);
boolean pet02 = run.containsBean("pet02");
System.out.println("容器中有 pet02 组件吗?" + pet02);
boolean zhangsan = run.containsBean("zhangsan");
System.out.println("容器中有 zhangsan 组件吗?" + zhangsan);

测试结果如下:

容器中有 pet01 组件吗?true
容器中有 pet02 组件吗?true
容器中有 zhangsan 组件吗?false

测试结果很奇怪,并没有按照我想象中的那样子,也就是 pet01 组件创建它就会创建。后面经过 bilibili 的弹幕中才知道,组件在进行注册的过程中似乎和顺序相关。所以改改顺序看看,将 user01 放到最后。

@Configuration(proxyBeanMethods = true)
public class MyConfig {
    
    @Bean
    public User user02() {
        User lisi = new User("lisi", 10);
        return lisi;
    }
    
    @Bean
    public Pet pet01() {
        return new Pet("tomcat");
    }
    
    @Bean
    public Pet pet02() {
        return new Pet("tomcat222");
    }
    
    @ConditionalOnBean(name = "pet01") // 当容器中有 pet01 的时候,那么就注册 zhangsan 组件。
    @Bean("zhangsan")
    public User user01() {
        User zhangsan = new User("zhangsan", 10);
        zhangsan.setPet(pet01());
        return zhangsan;
    }

}

运行结果如下,发现这才是我预想的结果

容器中有 pet01 组件吗?true
容器中有 pet02 组件吗?true
容器中有 zhangsan 组件吗?true

目前只学习到这个注解,后面在学习到其它的条件注解再来这里记录记录。

ImportResource 注解

这个注解的用途是加载一个 Spring 的 xml 配置文件,将 xml 文件中配置好的对象放入到容器中。

主程序类

@SpringBootApplication(scanBasePackages = "top.bestguo")
@ImportResource("classpath:bean.xml")
public class SpringBootApplication {

    public static void main(String[] args) {

        // Spring IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(SpringBootApplication.class, args);
        boolean haha = run.containsBean("haha");
        System.out.println("容器中有 haha 组件吗?" + haha);
        boolean hehe = run.containsBean("hehe");
        System.out.println("容器中有 hehe 组件吗?" + hehe);
    }
}

xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="top.bestguo.bean.Pet" id="haha">
        <property name="name" value="haha" />
    </bean>

    <bean class="top.bestguo.bean.Pet" id="hehe">
        <property name="name" value="hehe" />
    </bean>

</beans>

运行结果如下,发现已经导入成功。

容器中有 haha 组件吗?true
容器中有 hehe 组件吗?true

ConfigurationProperties 注解

在某个类上标记,且设置完 prefix 前缀之后,可以在 application.properties 配置文件的属性值注入到实例化组件中。

汽车类

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "mycar")
public class Car {

    private String brand;
    private Integer price;
    
    // coding......
    
}

application.properties 配置文件

mycar.brand=BMW
mycar.price=8000

当然,配置完成之后需要在主程序类上标记一个 EnableConfigurationProperties 注解,然后传入 ”类名.class“ 即可。

@ImportResource("classpath:bean.xml")
@EnableConfigurationProperties(Car.class) // 传入”类名.class“ 
@SpringBootApplication(scanBasePackages = "top.bestguo")
public class SpringBootApplication {
    // coding......
}

通过 Web 模块访问,看看能否正常得到。

@RequestMapping("/car")
@ResponseBody
public Car car() {
    return car;
}

运行结果如下,配置文件中的值已经成功注入进来了。

QQ截图20210626235311