<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.3.23version>
dependency>
以下八种方式,按照Bean创建的时机来演示,由浅入深。先直观看下Bean的创建流程:
BeanDefinition
对象中类路径下,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);
}
}
}
使用@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>
在上面半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);
}
}
}
此形式可以有效的降低源代码与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都可以被一同导入。
注意下面用到的注册方法,是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);
导入实现了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是否加载 。通俗说,就是谁导入它,它就可以查谁的户口,比如用了什么注解,注解有什么属性
导入实现了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的定义态。
导入实现了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长啥样,我有话语权。
写到这儿,突然想到:纯Java开发和框架开发相比,前者就像一个空房子,你想把它变成一个家(类比开发一个服务或者系统),就得自己手搓家具,比如挂衣柱、床(类比通用代码),再把你的衣服挂上去(衣服类比你的业务代码)。而框架则是在空房子里给你造好了家具,家具挂衣柱上那些预留的
触手,就像框架的这些扩展接口
,我们只需利用它们去把衣服往预留的触手上一件件挂就行。
===========================================================
到此,八种方式整理完了,后四种支持对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();
}
}
如果之间旧项目用xml定义Bean,现在改配置类,如何导入之前xml的东西? ==> @ImportResource
@ImportResource("applicationContext.xml")
@Configuration
@ComponentScan("com.plat")
public class SpringConfig {
}
如此,就既能加载xml中的Bean,也能扫描注解定义的Bean
关于@Component和@Configuration,前者只有一个属性value,即Bean的名称,而@Configuration除了value还有一个proxyBeanMethods
属性:
关于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变成多例的方式很多,一般很少去通过改这个属性来实现。