• 【原理篇】一、声明Bean的八种方式



    创建一个纯Maven工程来演示Bean的多种声明和定义的方式,这里只引入spring-context的依赖:

    <dependency>
    	<groupId>org.springframeworkgroupId>
    	<artifactId>spring-contextartifactId>
    	<version>5.3.23version>
    dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    以下八种方式,按照Bean创建的时机来演示,由浅入深。先直观看下Bean的创建流程:

    在这里插入图片描述
    以造车为例对比造Bean:

    • 概念态Bean:刚使用@Bean或者配置完各项Bean的信息
    • 定义态Bean:创建完ApplicationContext容器,概念态的信息如:id、scope、lazy等信息被读取到BeanDefinition对象中
    • 纯净态Bean:Spring容器通知BeanFactory生产,但这时只是刚实例化,没有依赖注入,先存于二级缓存里(循环依赖问题才会体现出纯净态Bean的作用)
    • 成熟态Bean:属性注入,成为一个成熟态Bean,加入到单例池(一个Map,也叫一级缓存)

    1、纯xml声明bean

    类路径下,applicationContext.xml文件的内容:

    
    <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="bookService" 
    		  class="com.plat.service.impl.Book1ServiceImpl" 
    		  scope="singleton"/>
    		  
    	
    	<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/>
    beans>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    看下效果:

    public class App1{
    
    	public static  void main(String[] args){
    		
    		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    		
    		for(String beanName : context.getBeanDefinitionNames()){
    		
    			System.out.println(beanName);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2、xml+注解方式声明bean

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

    @Service
    public class BookServiceImpl implements BookService {
    }
    
    
    • 1
    • 2
    • 3
    • 4

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

    @Component
    public class DbConfig {
    	@Bean
    	public DruidDataSource getDataSource(){
    		DruidDataSource ds = new DruidDataSource();
    		return ds;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    xml中声明一个命名空间context,指定扫描路径:

    
    <beans xmlns="http://www.springframework.org/schema/beans"
    		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    		xmlns:context="http://www.springframework.org/schema/context"
    		xsi:schemaLocation="http://www.springframework.org/schema/beans
    		https://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.plat"/>
    beans>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    3、纯注解声明bean

    在上面半xml半注解的基础上,把xml替换成一个配置类,加@ComponentScan(“com.plat”)注解

    @Configuration
    //@Configuration配置项如果不用于被扫描可以省略
    @ComponentScan("com.plat")
    public class SpringConfig {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    打印下IoC容器的所有Bean:

    public class App1{
    
    	public static  void main(String[] args){
    		
    		//此时SpringConfig.class这个类也会被加载成Bean,即使上面没加@Component
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);  
    		
    		for(String beanName : context.getBeanDefinitionNames()){
    		
    			System.out.println(beanName);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    4、@Import导入一个类成为Bean

    此形式可以有效的降低源代码与Spring技术的耦合度,在spring技术底层及诸多框架的整合中大量使用 ,比如:

    public class Dog {
    }
    
    • 1
    • 2

    要将这个类的对象做成Bean,不用改原代码,不加任何Spring的东西,无侵入式,直接:

    @Import(Dog.class)
    public class SpringConfig {
    }
    
    
    • 1
    • 2
    • 3
    • 4

    此时Bean的名称是以其全路径.类名为name,而不是像扫描产生的Bean,以类名首字母小写为Bean的name。

    @Import(DbConfig.class)
    public class SpringConfig {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    @Configuration
    public class DbConfig {
    	@Bean
    	public DruidDataSource getDataSource(){
    		DruidDataSource ds = new DruidDataSource();
    		return ds;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如上,如果@Import导入的不是一个普通类,而是一个配置类,那配置类中定义的Bean也会被一同导入。且,不管配置类有无@Configuration注解,其里面定义的Bean都可以被一同导入。

    5、使用上下文对象在容器初始化完毕后注册一个bean

    注意下面用到的注册方法,是AnnotationConfigApplicationContext实现类的方法,不是接口ApplicationContext中的方法,这儿就别写成多态形式了。

    public class AppImport {
    	public static void main(String[] args) {
    	
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    		
    		ctx.register(Dog.class);  //注册Bean,此时Bean名称默认是类名首字母小写
    		
    		ctx.registerBean("tom",Cat.class);  //注册Bean,并指定Bean Name
    		
    		String[] names = ctx.getBeanDefinitionNames();
    		
    		for (String name : names) {
    			System.out.println(name);
    		}
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    registerBean方法还有第三个参数,是一个可变长参数,给这个类的构造方法用的。

    ctx.registerBean("tom",Cat.class);  
    
    • 1

    连续注册三次,保留最后一个,前面的被覆盖,就像key相同的数据put进map

    ctx.registerBean("tom",Cat.class);  
    ctx.registerBean("tom",Cat.class);  
    ctx.registerBean("tom",Cat.class);  
    
    • 1
    • 2
    • 3

    6、接口ImportSelector搭配@Import

    导入实现了ImportSelector接口的类,实现对导入源的编程式处理:

    public class MyImportSelectors implements ImportSelector{
    
    	@Override
    	public String[] selectImports(AnnotationMetadata importingClassMetadata){
    
    		return new String[]{"com.plat.bean.Tree"};  //数组中写要注册为Bean的类的全路径类名,或者要加载的Bean的类的全路径类名
    		//声明一个类的Bean,再加载到IoC容器,或者这个类本身已被声明为一个Bean,也可以这么写
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    @Import(MyImportSelectors.class)
    public class SpringConfig {
    }
    
    
    • 1
    • 2
    • 3
    • 4

    此时就可以在Ioc容器中拿到Tree的Bean。那问题来了,既然能直接@Import导入Tree,干嘛又绕一圈再@Import另一个中间类 ⇒ 程序没写完,要利用重写方法里的形参AnnotationMetadata对象来完善,即注解元数据对象。

    先测一下这个AnnotationMetadata对象的一些方法:

    importingClassMetadata.getClassName()
    
    • 1

    打印这个方法输出:

    public class MyImportSelectors implements ImportSelector{
    
    	@Override
    	public String[] selectImports(AnnotationMetadata importingClassMetadata){
    	
    		System.out.pritnln(importingClassMetadata.getClassName());
    		
    		return new String[]{"com.plat.bean.Tree"};
    	}
    }
    
    ====
    //输出SpringConfig.class
    服务启动过程中,getClassName方法输出的类名,正好是@Import(MyImportSelectors.class)导入这个类的那个配置类
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    再看hasAnnotation()方法,判断上面的这个配置类有无某个注解(注意,是谁@Import了MyImportSelectors类,就检测谁):

    Boolean flag = importingClassMetadata.hasAnnotation("org.springframework.context.annotation.configuration");
    
    //true
    
    • 1
    • 2
    • 3

    getAnnotationAttributes()方法,获取配置类上的某个注解的属性:

    Map<string,Object> attribute = importingClassMetadata.getAnnotationAttributes("org.springframework.context.annotation.componentScan");
    
    //拿属性,接着再自己判断是否为空
    map.get("basePackages");
    
    • 1
    • 2
    • 3
    • 4

    用这些方法,对上面导入Bean的程序做出最终的改良:

    public class MyImportSelector implements ImportSelector {
    
    	@Override
    	public String[] selectImports(AnnotationMetadata metadata) {
    	
    		boolean flag = metadata.hasAnnotation("org.springframework.context.annotation.configuration");
    		
    		if(flag){
    			return new String[]{"com.plat.domain.Dog"};
    		}
    		
    		return new String[]{"com.plat.domain.Cat"};
    	}
    }
    
    //@Import我的配置类,有@Configuration注解,我就加载Dog类为一个Bean,没有这个注解,我就加载Cat类成为Bean
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    亮点不是在于导入Bean,而是判断什么时候可以注册Bean,在于根据导入MyImportSelector类的那个配置类的信息来控制Bean是否加载通俗说,就是谁导入它,它就可以查谁的户口,比如用了什么注解,注解有什么属性

    7、接口ImportBeanDefinitionRegistrar搭配@Import

    导入实现了ImportBeanDefinitionRegistrar接口的类,通过BeanDefinition的注册器来注册一个bean,实现对IoC容器中bean的干预,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果。

    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    	
    	//没有抽象方法,default的,自己找一下再重写
    	@Override
    	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
    	BeanDefinitionRegistry registry) {
    		
     		//AnnotationMetadata的玩法和上面一样,就不重复了
     		//只演示BeanDefinitionRegistry
     		//获取BeanDefinition的方式很多,按需写
    		BeanDefinition beanDefinition = BeanDefinitionBuilder
    			.rootBeanDefinition(Tree.class)
    			.getBeanDefinition();
    		//自行按需set
    		beanDefinition.setXXX();	
    		registry.registerBeanDefinition("tree", beanDefinition);
    		
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    和上一种方式相比,多了一个BeanDefinitionRegistry的形参,也就是说既可以像上面一样做判断和控制Bean,也可以通过BeanDefinition来操作和注册Bean。

    @Import(MyImportBeanDefinitionRegistrar.class)
    public class SpringConfig {
    }
    
    
    • 1
    • 2
    • 3
    • 4

    验证一下:

    public class App1{
    
    	public static  void main(String[] args){
    		
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);  
    		
    		System.out.println(context.getBean(Tree.class));
    	}
    }
    ====
    输出:
    com.plat.bean.Tree@6bc407fd
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    亮点是通过BeanDefinition操作IoC容器中的Bean,对应Bean的定义态。

    8、接口BeanDefinitionRegistryPostProcessor搭配@Import

    导入实现了BeanDefinitionRegistryPostProcessor接口的类,通过BeanDefinition的注册器注册实名bean,实现对容器中bean的最终裁定,在Bean的创建流程中,这种方式比第七种更靠后

    public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    
    	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    	
    		BeanDefinition beanDefinition = BeanDefinitionBuilder
    			.rootBeanDefinition(BookServiceImpl4.class)
    			.getBeanDefinition();
    			
    		registry.registerBeanDefinition("bookService", beanDefinition);
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    @Import(MyPostProcessor.class)
    public class SpringConfig {
    }
    
    
    • 1
    • 2
    • 3
    • 4

    这种方式利用BeanDefinition后置处理器,亮点在于,等你们对BeanDefinition的各种修改都做完了,我才来改,因为我最后一个改,所以我改完的形态就是最终形态,最后这个Bean长啥样,我有话语权。

    9、一点想法

    写到这儿,突然想到:纯Java开发和框架开发相比,前者就像一个空房子,你想把它变成一个家(类比开发一个服务或者系统),就得自己手搓家具,比如挂衣柱、床(类比通用代码),再把你的衣服挂上去(衣服类比你的业务代码)。而框架则是在空房子里给你造好了家具,家具挂衣柱上那些预留的触手,就像框架的这些扩展接口,我们只需利用它们去把衣服往预留的触手上一件件挂就行。

    ===========================================================

    到此,八种方式整理完了,后四种支持对Bean的加载做控制,即决定什么时候加载为Bean,什么时候不做处理。加载控制下篇整理,这篇太长了。以下是一些补充知识点。

    请添加图片描述

    10、补充点:FactoryBean

    • 实现FactoryBean接口的类,可以在其bean加载到容器之前进行相关逻辑处理操作
    • 也可以将一种Bean转换为另外一种Bean,FactoryBean泛型中是什么类,最后就是什么类型
    //先看bean加载到容器之前进行相关逻辑处理操作
    @Component
    @Data
    public class Book implements FactoryBean<Book>{
    
    	private String name;
    
    	@Override
    	public Book getObject() throws Exception {
    		Book book = new Book();
    		// 进行book对象相关的初始化工作
    		book.setName("testFactroyBean");
    		return book;
    	}
    	
    	@Override
    	public Class<?> getObjectType() {
    		return Book.class;
    	}
    
    	@Override
    	public boolean isSingleton(){
    		return false;
    	}
    }
    //以上这么用,FactoryBean大材小用了,上面的set逻辑,直接加无参构造里也能实现
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    public class BookFactoryBean implements FactoryBean<Book> {
    	@Override
    	public Book getObject() throws Exception {
    		Book book = new Book();
    		// 进行book对象相关的初始化工作
    		book.setName("testFactroyBean");
    		return book;
    	}
    	
    	@Override
    	public Class<?> getObjectType() {
    		return Book.class;
    	}
    
    	@Override
    	public boolean isSingleton(){
    		return false;
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    @Configuration
    public class SpringConfig{
    
    	//此时,返回的Bean是Book类型,而不是new出来的这个类型自身
    	@Bean
    	public BookFactoryBean book(){
    		return new BookFactoryBean();
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    11、补充点:@ImportResource

    如果之间旧项目用xml定义Bean,现在改配置类,如何导入之前xml的东西? ==> @ImportResource

    @ImportResource("applicationContext.xml")
    @Configuration
    @ComponentScan("com.plat")
    public class SpringConfig {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如此,就既能加载xml中的Bean,也能扫描注解定义的Bean

    12、补充点:@Component和@Configuration的分析

    关于@Component和@Configuration,前者只有一个属性value,即Bean的名称,而@Configuration除了value还有一个proxyBeanMethods属性:

    在这里插入图片描述

    在这里插入图片描述

    关于proxyBeanMethods属性,默认为true,看下为false的情况:

    @Configuration(proxyBeanMethods = false)
    public class SpringConfig {
    }
    
    • 1
    • 2
    • 3
    public class App1{
    
    	public static  void main(String[] args){
    				
    		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);  
    		
    		System.out.println(context.getBean(SpringConfig.class));
    		
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时返回一个普通Java对象com.plat.config.SpringConfig@6dd7b5a3,改回默认的true,再获取这个Bean打印:

    @Configuration  //默认为true
    public class SpringConfig {
    }
    
    • 1
    • 2
    • 3

    此时返回的是一个代理对象:com.plat.config.SpringConfig$$EnhancerBySpringCGLIB$$66cb5ddf@7b205dbd用代理对象得到的Bean是从容器中获取的而不是重新创建的。

    @Configuration(proxyBeanMethods = false)
    public class SpringConfig3 {
    
    	@Bean
    	public Book book(){
    		System.out.println("book init ...");
    		return new Book();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    public class AppObject {
    	public static void main(String[] args) {
    		ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    		SpringConfig3 config = ctx.getBean("Config", SpringConfig.class);
    		config.book();  //上面proxyBeanMethods = false,那这两个book对象就不是同一个
    		config.book();
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这也是很多代码里为啥会直接调用注册Bean的方法来拿Bean的原因,比如之前的MQ绑定Queue和Exchange:

    在这里插入图片描述

    当然,把Bean变成多例的方式很多,一般很少去通过改这个属性来实现。

  • 相关阅读:
    Maven详细总结
    停车场寻车系统(识别车牌,手机app查询相关信息)
    生成模型 评价指标 D-1,distinct-n
    C++stack&queue
    Java中的参数传递到底是值传递还是参数传递
    记一次阿里云oss文件上传服务假死
    性能测试-linux-top/vmstat/dstat命令,闭着眼睛也要背出来
    PG::Potato
    day35
    计算机网络 静态路由及动态路由RIP
  • 原文地址:https://blog.csdn.net/llg___/article/details/133680946