【Spring Boot入门(3)】SpringBoot容器底层相关注解介绍

1,018 阅读15分钟

1、SpringBoot组件添加之@configuration注解

1.1、@configuration注解基本使用

项目结构

image.png

1、添加两个组件,一个是 User(用户)组件、一个是 Pet(宠物)组件

User类:

package com.lemon.boot.bean;

/**
 * @Author Lemons
 * @create 2022-03-07-18:55
 */
public class User {
    private String name;
    private Integer age;

    public User(){

    }

    public User(String name,Integer age){
        this.name = name;
        this.age = age;
    }

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }

    public Integer getAge(){
        return age;
    }

    public void setAge(Integer age){
        this.age = age;
    }

}

Pet类

package com.lemon.boot.bean;

/**
 * @Author Lemons
 * @create 2022-03-07-18:54
 */
public class Pet {
    private String name;
    public Pet(){

    }

    public Pet(String name){
        this.name = name;
    }


    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }
}

2、现在将这两个组件加到容器中,按照原来的原生Spring方式可以这样做 :

① resource下创建一个 Spring 的配置文件 beans.xml;

image.png

② 以前 Spring xml 配置的方式:使用 bean 标签,给容器中添加上user和pet的两个组件,在里边可以给组件添加属性;

<?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 id = "user01" class="com.lemon.boot.bean.User">
        <property name = "name" value="小明"></property>
        <property name = "age" value="19"></property>
    </bean>

    <bean id = "pet01" class="com.lemon.boot.bean.Pet">
        <property name = "name" value="假老练"></property>
    </bean>
</beans>

③ 但是SpringBoot 已经不写 xml 配置文件了,SpringBoot 可以在底层用 @configuration 注解:创建一个类,在类上方使用这个注解,这个注解就是告诉 SpringBoot 这是一个配置类,等同于以前的配置文件。以前用 bean 标签,给容器中添加组件,现在用方法构造出来,并在上方使用@Bean注解,以方法名作为组件的id,返回类型就是组件类型,方法返回的值(对象)就是组件在容器中的实例。

这个类定义为Myconfig,放在和主程序所在包下的config

@Configuration //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
	//给容器中添加组件,以方法名作为组件的id,
    //返回类型就是组件类型,方法返回的值(对象)就是组件在容器中的实例
    @Bean 
    public User user01(){
        return new User("小明",18);
    }
	//如果不想让方法名作为组件名id,也可以在Bean标签直接给一个自定义的名字
    @Bean("jialaolian") 
    public Pet pet01(){
        return new Pet("假老练");
    }
}

④ 验证容器中有这两个组件:

  • MainApplication 启动类:
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        //2.容器里面包含我们当前应用的所有组件,
        //查看容器里边的组件,只要有我们这个组件,说明这个组件就能工作,这就是原理
        String[] names = run. getBeanDefinitionNames();//获取所有我们组件定义的名字;
        for (String name : names) {
            System.out.println(name);
        }
    }
}

运行主程序,结果为:可以看出,容器中是存在这两个组件的

image.png

⑤ 配置类里面使用 @Bean 标注在方法上给容器注册组件,默认是单实例的(Bean 默认是单例模式),这一点是和原生Spring一样的,如下进行验证:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        //2.容器里面包含我们当前应用的所有组件,
        //查看容器里边的组件,只要有我们这个组件,说明这个组件就能工作,这就是原理
        String[] names = run. getBeanDefinitionNames();//获取所有我们组件定义的名字;
        for (String name : names) {
            System.out.println(name);
        }

        //从容器中多次获取组件
        Pet Caty01 = run.getBean("jialaolian",Pet.class);
        Pet Caty02 = run.getBean("jialaolian",Pet.class);

        System.out.println("组件:"+(Caty01 == Caty02)); //判读组件是不是单实例的
    }
}

结果

组件:true

3、@Configuration 标注的这个配置类 MyConfig 它本身也是一个组件,配置类也是容器中的一个组件,验证:

//得到容器中的MyConfig类对象
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);

结果:配置类是个CGLIB生成的代理对象

com.lemon.boot.Config.MyConfig$$EnhancerBySpringCGLIB$$a3d18d30@61efae4d

1.2、@Configuration注解源码

1、注解源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
	//代理 Bean 的方法
    boolean proxyBeanMethods() default true;
}

可以发现在 SpringBoot2.0 以后的版本里边,基于 Spring5.2 以后,Configuration 类多了一个属性proxyBeanMethods默认是 true;这是与 SpringBoot1.0 的不同。

2、现在我们在我们的自定义配置类MyConfig.class使用这个属性

@Configuration(proxyBeanMethods = true) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

    @Bean //给容器中添加组件,以方法名作为组件的id,返回类型就是组件类型,方法返回的值(对象)就是组件在容器中的实例
    public User user01(){
        return new User("小明",18);
    }

    @Bean("jialaolian") //如果不想让方法名作为组件名id也可以在Bean标签直接给一个自定义的名字
    public Pet pet01(){
        return new Pet("假老练");
    }
}

在 MyConfig 类多次调用 user01 方法,那么方法返回的对象是从容器中拿还是就是普通的调用方法?验证如下:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args){
        //返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        
        //得到容器中的MyConfig类对象
        MyConfig bean = run.getBean(MyConfig.class);
        System.out.println(bean);

        //在MyConfig类多次调用user01方法
        User user = bean.user01();
        User user1 = bean.user01();

        System.out.println(user == user1);
    }
}

结果如下: "

true

这说明,外部无论对配置类中的这个组件注册方法调用多少次,获取的都是之前注册到容器中的单实例对象。user、user1两个对象相等的原因在于 Configuration 的这个属性 @Configuration(proxyBeanMethods = true),容器中获取组件的这个对象 MyConfig 不是一个普通对象,而是 MyConfig$$EnhancerBySpringCGLIB,实际上是被 SpringCGLIB 增强了的代理对象,也就是说我们获取到的是代理对象,代理对象调用 user01 ,SpringBoot 里的默认逻辑就是如果 @Configuration(proxyBeanMethods = true),我们这个类(MyConfig),我们获取到的就是代理对象,代理对象调用方法,SpringBoot 默认就会检查容器中有没有这个方法已经返回的组件,如果有就从缓存里取,没有在新创建,保持组件单实例。

3、若将 proxyBeanMethods 属性改为 false,运行结果则是false,则证明获取到的 MyConfig 不再是代理对象,多次调用方法得到的不是同一个实例,也就是说调用方法每次都会新创建一个对象。

注意:SpringBoot5.2以后的版本修改为false会标红,但是不影响程序运行

1.3、Full 模式与 Lite 模式

SpringBoot 在底层 Configuration 的两个配置:

  • Full 全配置模式(@Configuration(proxyBeanMethods = true)保证每个 @Bean 方法被调用多少次返回的组件都是单实例的;
  • Lite 轻量级配置模式(@Configuration(proxyBeanMethods = false)每个 @Bean 方法被调用多少次返回的组件都是新创建的。

1.3.1、组件依赖

上述两个模式就轻松的解决了组件依赖的这个问题,举例子用户养宠物:

  • 修改 User.class

       ```java
    

public class User { private String name; private Integer age; private Pet pet; //增加宠物属性

public Pet getPet(){  //提供get/set
    return pet;
}

public void setPet(Pet pet){
    this.pet = pet;
}

public User(){

}

public User(String name,Integer age){
    this.name = name;
    this.age = age;
}

public String getName(){
    return name;
}

public void setName(String name){
    this.name = name;
}

public Integer getAge(){
    return age;
}

public void setAge(Integer age){
    this.age = age;
}

} ```

  • MyConfig.class:设为Full 全配置模式
@Configuration(proxyBeanMethods = true)
public class MyConfig {
    
 @Bean
    public User user01(){
        User zhangsan = new User("zhangsan",18);
        //User组件依赖了Pet组件
        zhangsan.setPet(Cat());
        return zhangsan;
    }

    @Bean("jialaolian")   //@bean添加组件到容器
    public Pet Cat(){
        return new Pet("假老练");
    }
}
  • MainApplication.class
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args){
        //返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        User user01 = run.getBean("user01",User.class);
        Pet jialaolian = run.getBean("jialaolian",Pet.class);

        System.out.println("容器中的宠物和调用方法得到的宠物是否一样:"+(user01.getPet() == jialaolian));
    }
}

运行结果说明,用户的宠物就是容器中的宠物

容器中的宠物和调用方法得到的宠物是否一样::true

若将 proxyBeanMethods = true 改为 false,运行结果说明用户的宠物不是容器中的宠物

容器中的宠物和调用方法得到的宠物是否一样::false

1.3.2、两个模式使用场景

  • 配置类组件之间无依赖关系用 Lite **模式,加速容器启动过程,减少判断。**创建新组建用lite,因为容器里面没有,不需要检查

  • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,可以快速获取到bean,减少新生成实例的消耗,减少 jvm 垃圾回收,用 Full 模式。

  • 这里有无依赖多指是否在一个类中使用到另一个类,例如在user类里面,Cat类是user里的一个属性,这样user和cat类之间就产生了依赖关系

1.4、总结及注意事项

Full模式与Lite模式注意点如下:Full模式和Lite模式是针对spring配置而言的,和xml配置无关。

何时为Lite模式:

  • 类上有@Component注解
  • 类上有@ComponentScan注解
  • 类上有@Import注解
  • 类上有@ImportResource注解
  • 类上没有任何注解,但是类中存在@Bean方法
  • 类上有@Configuration(proxyBeanMethods = false)注解

Lite总结:运行时不用生成CGLIB子类,提高运行性能,降低启动时间,可以作为普通类使用。但是不能声明@Bean之间的依赖

何时为Full模式:

  • 标注有@Configuration或者@Configuration(proxyBeanMethods = true)的类被称为Full模式的配置类。

Full模式总结:单例模式能有效避免Lite模式下的错误,性能没有Lite模式好

2、SpringBoot组件添加之@Import注解

1、组件注册其他注解补充

上一小节学完了SpringBoot底层的@Configuration注解,并结合了@Bean注解来进行了组件注册添加演示,这个注解让我们了解了SpringBoot中的组件的添加,除了这些注解以外,@Bean、@Component、@Controller、@Service、@Repository这些以前原生Spring中的注解也都可以用来注册组件到容器中,只要包扫描正确

比如@Bean

//给容器中添加组件,默认以方法名作为组件的id,返回类型就是组件类型,创建对象,就是实例
@Bean
public User user01() {
    return new User("zhangsan", 19);
}

我们简要的说明一下这几个注解的作用:

  • @Bean是用来往容器里面添加组件的,默认以方法名作为组件的id,返回类型就是组件类型,创建对象,就是实例,类似于spring中的
  • @Component表示是一个组件
  • @Controller表示是一个控制器
  • @Service表示是一个业务逻辑组件
  • @Repository表示是一个数据库层面组件
  • 这些注解的使用和以前都是一样的

2、@Import注解导入组件

@Import可以使用在任何一个组件或配置类上面,可以自动创建出它声明的组件,组件的名字默认是类的相对路径(全类名):底层是一个数组,所以该注解可以写多种类型

MyConfig类中使用@Import

@Import({User.class})
@Configuration //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
	//给容器中添加组件,以方法名作为组件的id,
    //返回类型就是组件类型,方法返回的值(对象)就是组件在容器中的实例
    @Bean 
    public User user01(){
        return new User("小明",18);
    }
	//如果不想让方法名作为组件名id,也可以在Bean标签直接给一个自定义的名字
    @Bean("jialaolian") 
    public Pet pet01(){
        return new Pet("假老练");
    }
}

在主启动类获取使用@Import自动生成的User类型的组件:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
     	//按照类型获取组件
        String[] names = run.getBeanNamesForType(User.class);
        for (String name : names) {
            System.out.println(name);
        }
    }
}

结果

com.lemon.boot.bean.User
user01

解释:为什么会有两个User类型的组件对象?

  • 第一个组件:名字叫com.lemon.boot.bean.User,这个组件是我们@import注解添加到容器里去的,组件的名字默认是类的相对路径(全类名)
  • 第二个组件:是我们使用@Bean注解添加到容器里去的,默认以方法名作为组件的id,所以这里就有了两种创建组件的方式

3、@Conditional条件装配注解

条件装配注解:满足Conditional指定的条件,则进行组件注入

  • 放在配置类上表示,当容器中满足条件时,配置类中的组件才生效;
  • 放在配置方法上的时候,表示的意思是当满足条件的时候配置方法才生效;

@Conditional注解是一个根注解,下面派生了许多新的注解,如图: 不同的注解有不同的功能,我们来说明一下@ConditionalOnBean这个注解

image.png

演示:

首先将MyConfig类中的程序的@Bean("jialaolian")删掉,使之cat()方法成为一个普通方法,这样我们就不能创建jialaolian这个组件了到容器里了。然后进行主程序的测试:结果当然是没有

//查看容器中是否包含pet这个组件
boolean pet = run.containsBean("jialaolian"); //false
System.out.println("是否有pet组件:"+ pet);

当然我们的user01组件是有的,但是现在假设我们设置,只有存在jialaolian这个组件的时候才能创建user01组件注册到容器中:如下使用在方法上

@Configuration
public class MyConfig {
    
    //当有名字为jialaolian的组件时该方法才会生效,才会注入user01组件
    @ConditionalOnBean(name="jialaolian" )
    @Bean 
    public User user01(){
        return new User("小明",18);
    }

    public Pet pet01(){
        return new Pet("假老练");
    }
}

测试:获取user01对象,程序没有任何输出,获取不到对象

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        //按照类型获取组件
        String[] names = run.getBeanNamesForType(User.class);
        for (String name : names) {
            System.out.println(name);
        }
    }
}

如果将@ConditionalOnBean标注在类上,说明必须满足该条件,类中所有代码才会全部生效

@Import({User.class})
@Configuration
@ConditionalOnBean(name = "pet")
public class MyConfig {

    //当有名字为pet的组件时以下代码才会生效
    @Bean
    public User user01() {
        return new User("zhangsan", 19);
    }

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

测试:获取不到结果

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        boolean user = run.containsBean("user01" ) ;//false
        System.out.print1n("容器中user01组件:"+ user);

        boolean tom22 = run.containsBean("pet22" ) ;//false
        System.out.print1n("容器中pet22组件:"+ tom22);
    }
}

4、springBoot原生配置文件导入:@ImportResource注解

步骤:

  • 将原生的Spring.xml的配置文件,通过@ImportResource导入SpringBoot进行解析,完成对应的组件注册
  • 标注位置:在某个配置类的上方
  • 参数:输入你要导入的配置文件的类路径即可

演示:原生的Spring.xml的配置文件beans.xml如下:在类路径classpath:resource

<?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 id = "user22" class="com.lemon.boot.bean.User">
        <property name = "name" value="小明"></property>
        <property name = "age" value="19"></property>
    </bean>

    <bean id = "pet33" class="com.lemon.boot.bean.Pet">
        <property name = "name" value="假老练"></property>
    </bean>
</beans>

这种用原生xml的方式肯定是在容器中没有我们创建的这两个组件[主程序启动类中验证],但是比如遇到一些老项目,就是用这些原生配置文件来写的,你要按照SpringBoot格式转变为注解形式的的,一个一个迁移太麻烦,于是可以直接使用下面这种引入资源的方式:直接在配置类上面用@ImportResource注解引入资源

测试

配置类

@ImportResource("classpath:beans.xml") //导入原生xml文件
@Import({User.class})
@Configuration
@ConditionalOnMissingBean(name = "pet")
public class MyConfig {

    //当有名字为pet的组件不存在时以下代码才会生效
    @Bean
    public User user01() {
        return new User("zhangsan", 19);
    }

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

主启动类进行测试:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

        boolean user = run.containsBean("user01" ) ;//true
        System.out.println("容器中user01组件:"+ user);

        boolean tom22 = run.containsBean("pet22" ) ;//true
        System.out.println("容器中pet22组件:"+ tom22);

        boolean user22 =run.containsBean("user22");
        boolean pet33 = run.containsBean("pet33");
        System.out.println( "user22: "+user22);//true
        System.out.println("pet33:" + pet33);//true
    }
}

5、配置绑定--@ConfigurationProperties

如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;比如连接mysql

以前需要这样的操作才可以:

image.png

在SpringBoot中,将变得非常简单,如下:

5.1、方式1:@Component + @ConfigurationProperties

对应要绑定的类:自定义一个Car类:

注意:

  • 该创建的类car一定要加入到容器中才能生效
  • 前缀必须小写
//只有在容器中的组件,才会拥有SpringBoot提供的强大功能
@Component //Component表示是一个 组件
@ConfigurationProperties(prefix = "mycar")//指定要与配置文件中以某前缀开始的值进行属性匹配
public class Car {
    private String brand;
    private Integer price;
    //提供get/set/toString方法
}

对应的配置文件:resource下的appliction.properties文件,内容如下

mycar.brand=byd
mycar.price=100000

启动类测试查看是否绑定成功:

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        Car car = run.getBean(Car.class);
        System.out.println(car);//true
    }
}

结果:容器中是有这个对象的

Car{brand=byd, price=100000}

或者这样进行测试:在上面的基础上,通过浏览器进入

@RestController
public class HelloController {

    @Autowired  //注入容器中的对象
    Car car;

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

    @RequestMapping("/hello2")
    public String hand01() {
        return "Hello 世界这么大!";
    }
}

结果:

image.png

从两次打印结果看出:

  • 我们的容器里面是有创建的组件的
  • 并且也封装成为了javabean

5.2、方式2:@EnableConfigurationProperties + @ConfigurationProperties

对应要绑定的类: Car类:

@ConfigurationProperties(prefix = "mycar")//指定要与配置文件中以某前缀开始的值进行属性匹配
public class Car {
    private String brand;
    private Integer price;
    //提供get/set/toString方法
}

对应的配置文件:

mycar.brand=byd
mycar.price=100000

启动类使用注解@EnableConfigurationProperties测试查看是否绑定成功: :

//1.开启Car配置绑定功能
//2.把这个car组件自动注册到容器中
@EnableConfigurationProperties(Car.class)
@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        //1.返回我们IOC容器
        ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
        Car car = run.getBean(Car.class);
        System.out.println(car);//true
    }
}

当然,我们也可以通过上述浏览器方式进行测试,这个注解也可以标注在配置类上进行[通常我们也标注在配置类上] ,结果:

image.png

小总结:一般使用方式2 :该方式可以保证中文没有乱码

image.png