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

文章目录

  • 1、纯xml声明bean
  • 2、xml+注解方式声明bean
  • 3、纯注解声明bean
  • 4、@Import导入一个类成为Bean
  • 5、使用上下文对象在容器初始化完毕后注册一个bean
  • 6、接口ImportSelector搭配@Import
  • 7、接口ImportBeanDefinitionRegistrar搭配@Import
  • 8、接口BeanDefinitionRegistryPostProcessor搭配@Import
  • 9、一点想法
  • 10、补充点:FactoryBean
  • 11、补充点:@ImportResource
  • 12、补充点:@Component和@Configuration的分析

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

<dependency>
	<groupId>org.springframeworkgroupId>
	<artifactId>spring-contextartifactId>
	<version>5.3.23version>
dependency>

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

【原理篇】一、声明Bean的八种方式_第1张图片
以造车为例对比造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>

看下效果:

public class App1{

	public static  void main(String[] args){
		
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		
		for(String beanName : context.getBeanDefinitionNames()){
		
			System.out.println(beanName);
		}
	}
}

2、xml+注解方式声明bean

使用@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;
	}
}

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>

【原理篇】一、声明Bean的八种方式_第2张图片

3、纯注解声明bean

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

@Configuration
//@Configuration配置项如果不用于被扫描可以省略
@ComponentScan("com.plat")
public class SpringConfig {

}

打印下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);
		}
	}
}

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

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

public class Dog {
}

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

@Import(Dog.class)
public class SpringConfig {
}

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

@Import(DbConfig.class)
public class SpringConfig {
}

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

如上,如果@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);
		}
	}
}

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

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

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

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

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,也可以这么写
	}
}
@Import(MyImportSelectors.class)
public class SpringConfig {
}

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

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

importingClassMetadata.getClassName()

打印这个方法输出:

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)导入这个类的那个配置类

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

Boolean flag = importingClassMetadata.hasAnnotation("org.springframework.context.annotation.configuration");

//true

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

Map<string,Object> attribute = importingClassMetadata.getAnnotationAttributes("org.springframework.context.annotation.componentScan");

//拿属性,接着再自己判断是否为空
map.get("basePackages");

用这些方法,对上面导入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

亮点不是在于导入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);
		
	}
}

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

@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringConfig {
}

验证一下:

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

亮点是通过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);
	}
}

@Import(MyPostProcessor.class)
public class SpringConfig {
}

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

9、一点想法

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

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

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

【原理篇】一、声明Bean的八种方式_第3张图片

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逻辑,直接加无参构造里也能实现
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;
	}

}
@Configuration
public class SpringConfig{

	//此时,返回的Bean是Book类型,而不是new出来的这个类型自身
	@Bean
	public BookFactoryBean book(){
		return new BookFactoryBean();
	}
}

11、补充点:@ImportResource

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

@ImportResource("applicationContext.xml")
@Configuration
@ComponentScan("com.plat")
public class SpringConfig {

}

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

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

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

【原理篇】一、声明Bean的八种方式_第4张图片

【原理篇】一、声明Bean的八种方式_第5张图片

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

@Configuration(proxyBeanMethods = false)
public class SpringConfig {
}
public class App1{

	public static  void main(String[] args){
				
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);  
		
		System.out.println(context.getBean(SpringConfig.class));
		
	}
}

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

@Configuration  //默认为true
public class SpringConfig {
}

此时返回的是一个代理对象: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();
	}
}
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();
	}
}

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

【原理篇】一、声明Bean的八种方式_第6张图片

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

你可能感兴趣的:(SpringBoot,spring,boot,Bean)