【SpringBoot学习13】Bean的加载方式,加载控制以及配置管理

  • Post author:
  • Post category:其他

1. Bean的加载方式

1.1 xml方式声明Bean

<?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="cat" class="com.hx.pojo.Cat"></bean>
    <bean class="com.hx.pojo.Cat"></bean>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"></bean>
</beans>

1.2 xml+注解方式声明Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       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
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.hx"/>
</beans>

使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean

@Service
public class BookServiceImpl implements BookService {
}

使用@Bean定义第三方bean,并将所在类定义为配置类或Bean

@Component
public class DbConfig {
@Bean
public DruidDataSource getDataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
	}
}

1.3 注解方式声明Bean

Configuration
@ComponentScan("com.hx")
public class SpringConfig {
@Bean
public DruidDataSource getDataSource(){
DruidDataSource ds = new DruidDataSource();
return ds;
	}
}

@Configuration配置项如果不用于被扫描可以省略

public class TestDemo {

    @Test
    public void test01(){
        //使用配置类
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] beans = context.getBeanDefinitionNames();
        for (String bean : beans) {
            System.out.println(bean);
        }
    }
}

使用FactroyBean接口

补充一个小知识,spring提供了一个接口FactoryBean,也可以用于声明bean,只不过实现了FactoryBean接口的类造出来的对象不是当前类的对象,而是FactoryBean接口泛型指定类型的对象

如下列,造出来的bean并不是DogFactoryBean,而是Dog。有什么用呢?可以在对象初始化前做一些事情,下例中的注释位置就是让你自己去扩展要做的其他事情的。

public class DogFactoryBean implements FactoryBean<Dog> {
    @Override
    public Dog getObject() throws Exception {
        Dog d = new Dog();
        //.........
        return d;
    }
    @Override
    public Class<?> getObjectType() {
        return Dog.class;
    }
    @Override
    public boolean isSingleton() {
        return true;
    }
}

proxyBeanMethods属性

前面的例子中用到了@Configuration这个注解,当我们使用AnnotationConfigApplicationContext加载配置类的时候,配置类可以不添加这个注解。但是这个注解有一个更加强大的功能,它可以保障配置类中使用方法创建的bean的唯一性。为@Configuration注解设置proxyBeanMethods属性值为true即可,由于此属性默认值为true。假如将其属性设置为false,则其创造出来的对象就不是同一个了。

@Configuration(proxyBeanMethods = true)
public class SpringConfig33 {
    @Bean
    public Cat cat(){
        return new Cat();
    }
}

下面通过容器再调用上面的cat方法时,得到的就是同一个对象了。注意,必须使用spring容器对象调用此方法才有保持bean唯一性的特性。

public class App33 {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig33.class);
        String[] names = ctx.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
        System.out.println("-------------------------");
        SpringConfig33 springConfig33 = ctx.getBean("springConfig33", SpringConfig33.class);
        System.out.println(springConfig33.cat());
        System.out.println(springConfig33.cat());
        System.out.println(springConfig33.cat());
    }
}

1.4 使用@Import注解注入bean

使用扫描的方式加载bean是开发中常见的bean的加载方式,但是由于扫描的时候不仅可以加载到你要的东西,还有可能加载到各种各样的乱七八糟的东西,万一没有控制好得不偿失了。
所以我们需要一种精准制导的加载方式,使用@Import注解就可以解决你的问题它可以加载所有的一切,只需要在注解的参数中写上加载的类对应的.class即可。有人就会觉得,还要自己手写,多麻烦,不如扫描好用。对呀,但是他可以指定加载啊,好的命名规范配合@ComponentScan可以解决很多问题,但是@Import注解拥有其重要的应用场景。有没有想过假如你要加载的bean没有使用@Component修饰呢?这下就无解了,而@Import就无需考虑这个问题。

精确制导,注入所需要的Bean。

@Import({Dog.class,DbConfig.class})
public class SpringConfig4 {
}

1.5 编程形式注册bean

前面介绍的加载bean的方式都是在容器启动阶段完成bean的加载,下面这种方式就比较特殊了,可以在容器初始化完成后手动加载bean。通过这种方式可以实现编程式控制bean的加载。

public class App5 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //上下文容器对象已经初始化完毕后,手工加载bean
        ctx.register(Mouse.class);
    }
}

其实这种方式坑还是挺多的,比如容器中已经有了某种类型的bean,再加载会不会覆盖呢?这都是要思考和关注的问题。慎用。

public class App5 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        //上下文容器对象已经初始化完毕后,手工加载bean
        ctx.registerBean("tom", Cat.class,0);
        ctx.registerBean("tom", Cat.class,1);
        ctx.registerBean("tom", Cat.class,2);
        System.out.println(ctx.getBean(Cat.class));
    }
}

其实这种加载是会进行覆盖的,谨慎使用。

1.6 导入实现了ImportSelector接口的类

在方式五种,我们感受了bean的加载可以进行编程化的控制,添加if语句就可以实现bean的加载控制了。但是毕竟是在容器初始化后实现bean的加载控制,那是否可以在容器初始化过程中进行控制呢?答案是必须的。实现ImportSelector接口的类可以设置加载的bean的全路径类名,记得一点,只要能编程就能判定,能判定意味着可以控制程序的运行走向,进而控制一切。

​ 现在又多了一种控制bean加载的方式,或者说是选择bean的方式。

MyImportSelector.java

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        //return new String[0];
        //各种条件的判定,判定完毕后,决定是否装载指定的bean
        if(metadata.hasAnnotation("org.springframework.context.annotation.Configuration")){
            return new String[]{"com.hx.pojo.Cat"};
        }
        return new String[]{"com.hx.pojo.Dog"};
    }
}

SpringConfig3.java

@Configuration
@Import(MyImportSelector.class)
public class SpringConfig3 {
}
public class TestDemo3 {

    @Test
    public void test01(){
        //使用配置类
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig3.class);
        String[] beans = context.getBeanDefinitionNames();
        for (String bean : beans) {
            System.out.println(bean);
        }
    }
}

在这里插入图片描述
总结

  1. bean的定义由前期xml配置逐步演化成注解配置,本质是一样的,都是通过反射机制加载类名后创建对象,对象就是spring管控的bean
  2. @Import注解可以指定加载某一个类作为spring管控的bean,如果被加载的类中还具有@Bean相关的定义,会被一同加载
  3. spring开放出了若干种可编程控制的bean的初始化方式,通过分支语句由固定的加载bean转成了可以选择bean是否加载或者选择加载哪一种bean

2. Bean的加载控制

开发中不可能在spring容器中进行bean的饱和式加载的。什么是饱和式加载,就是不管用不用,全部加载。比如jdk中有两万个类,那就加载两万个bean,显然是不合理的,因为你压根就不会使用其中大部分的bean。

那合理的加载方式是什么?肯定是必要性加载,就是用什么加载什么。继续思考,加载哪些bean通常受什么影响呢?最容易想的就是你要用什么技术,就加载对应的bean。用什么技术意味着什么?就是加载对应技术的类。

所以在spring容器中,通过判定是否加载了某个类来控制某些bean的加载是一种常见操作。下例给出了对应的代码实现,其实思想很简单,先判断一个类的全路径名是否能够成功加载,加载成功说明有这个类,那就干某项具体的工作,否则就干别的工作。

下面代码就是通过反射获取一个类,判断该类是否存在,存在就加载相对应的类,否则就不加载。

public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //return new String[]{"com.xnh.pojo.Cat"};
        try {
            Class<?> clazz = Class.forName("com.xnh.pojo.Dog");
            if(clazz!=null){
                return new String[]{"com.xnh.pojo.Cat"};
            }
        } catch (ClassNotFoundException e) {
            return new String[0];
        }
        return null;
    }
}

通过上述的分析,可以看到此类操作将成为开发中的常见操作,于是springboot将把这些常用操作给我们做了一次封装。这种逻辑判定你开发者就别搞了,我springboot信不过你这种新手开发者,我给你封装一下,做几个注解,你填参数吧。

下例使用@ConditionalOnClass注解实现了当虚拟机中加载了com.itheima.bean.Wolf类时加载对应的bean。比较一下上面的代码和下面的代码,有没有感觉很清爽。其实此类注解还有很多。

即com.xnh.bean这个包下是否有Wolf这个类,有就加载下面的Cat类,否则就不加载。

@Bean
@ConditionalOnClass(name = "com.xnh.bean.Wolf")
public Cat tom(){
    return new Cat();
}

@ConditionalOnMissingClass注解控制虚拟机中没有加载指定的类才加载对应的bean。

@Bean
@ConditionalOnMissingClass("com.xnh.bean.Dog")
public Cat tom(){
    return new Cat();
}

除了判定是否加载类,还可以对当前容器类型做判定,下例是判定当前容器环境是否是web环境。

@Bean
@ConditionalOnWebApplication
public Cat tom(){
    return new Cat();
}

当多条件存在时,只有多个条件均满足才会加载。

public class SpringConfig {
    @Bean

    //@Conditional()
    //有Dog这个类就加载下面方法产生的类,建议使用全路径名称书写
    //@ConditionalOnClass(name = "com.xnh.pojo.Dog")
    //没有Mouse这个类就加载下面方法产生的类
    @ConditionalOnMissingClass(value = "com.xnh.pojo.Mouse")
    @ConditionalOnNotWebApplication
    public Cat Tom(){
        return new Cat();
    }
}

3. Bean的依赖属性配置管理

bean的加载及加载控制已经搞完了,下面研究一下bean内部的事情。bean在运行的时候,实现对应的业务逻辑时有可能需要开发者提供一些设置值,有就是属性了。如果使用构造方法将参数固定,灵活性不足,这个时候就可以使用前期学习的bean的属性配置相关的知识进行灵活的配置了。先通过yml配置文件,设置bean运行需要使用的配置信息

cartoon:
  cat:
    name: "图多盖洛"
    age: 5
  mouse:
    name: "泰菲"
    age: 1

然后定义一个封装属性的专用类,加载配置属性,读取对应前缀相关的属性值。

@ConfigurationProperties(prefix = "cartoon")
@Data
public class CartoonProperties {
    private Cat cat;
    private Mouse mouse;
}

最后在使用的位置注入对应的配置即可。

@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse{
    @Autowired
    private CartoonProperties cartoonProperties;
}

建议在业务类上使用@EnableConfigurationProperties声明bean,这样在不使用这个类的时候,也不会无故加载专用的属性配置类CartoonProperties,减少spring管控的资源数量。

总结

  1. bean的运行如果需要外部设置值,建议将设置值封装成专用的属性类* * * * Properties
  2. 设置属性类加载指定前缀的配置信息
  3. 在需要使用属性类的位置通过注解@EnableConfigurationProperties加载bean,而不要直接在属性配置类上定义bean,减少资源加载的数量,因需加载而不要饱和式加载。

版权声明:本文为qq_54699828原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。