卧槽!Spring 中竟然有 12 种定义 Bean 的方法?
前 言
在庞大的 java 体系中,spring 有着举足轻重的地位,它给每位开发者带来了极大的便利和惊喜。我们都知道 spring 是创建和管理 bean 的工厂,它提供了多种定义 bean 的方式,能够满足我们日常工作中的多种业务场景。
那么问题来了,你知道 spring 中有哪些方式可以定义 bean?
我估计很多人会说出以下三种:
没错,但我想说的是以上三种方式只是开胃小菜,实际上 spring 的功能远比你想象中更强大。
之前有粉丝问我有没有什么关于 Spring 的学习资料推荐,这里给大家看看我整理的一些资料
需要资料的同学直接点击spring学习资料整理获取即可
各位看官如果不信,请继续往下看。
1. xml 文件配置 bean
我们先从 xml 配置 bean 开始,它是 spring 最早支持的方式。后来,随着 springboot 越来越受欢迎,该方法目前已经用得很少了,但我建议我们还是有必要了解一下。
1.1 构造器
如果你之前有在 bean.xml 文件中配置过 bean 的经历,那么对如下的配置肯定不会陌生:
这种方式是以前使用最多的方式,它默认使用了无参构造器创建 bean。
当然我们还可以使用有参的构造器,通过<constructor-arg>标签来完成配置。
其中:
index 表示下标,从 0 开始。
value 表示常量值
ref 表示引用另一个 bean
1.2 setter 方法
除此之外,spring 还提供了另外一种思路:通过 setter 方法设置 bean 所需参数,这种方式耦合性相对较低,比有参构造器使用更为广泛。
先定义 Person 实体:
它里面包含:成员变量 name 和 age,getter/setter 方法。
然后在 bean.xml 文件中配置 bean 时,加上<property>标签设置 bean 所需参数。
1.3 静态工厂
这种方式的关键是需要定义一个工厂类,它里面包含一个创建 bean 的静态方法。例如:
接下来定义 Person 类如下:
它里面包含:成员变量 name 和 age,getter/setter 方法,无参构造器和全参构造器。
然后在 bean.xml 文件中配置 bean 时,通过 factory-method 参数指定静态工厂方法,同时通过<constructor-arg>设置相关参数。
1.4 实例工厂方法
这种方式也需要定义一个工厂类,但里面包含非静态的创建 bean 的方法。
Person 类跟上面一样,就不多说了。
然后 bean.xml 文件中配置 bean 时,需要先配置工厂 bean。然后在配置实例 bean 时,通过 factory-bean 参数指定该工厂 bean 的引用。
1.5 FactoryBean
不知道大家有没有发现,上面的实例工厂方法每次都需要创建一个工厂类,不方面统一管理。
这时我们可以使用 FactoryBean 接口。
在它的 getObject 方法中可以实现我们自己的逻辑创建对象,并且在 getObjectType 方法中我们可以定义对象的类型。
然后在 bean.xml 文件中配置 bean 时,只需像普通的 bean 一样配置即可。
轻松搞定,so easy。
注意:getBean("userFactoryBean");获取的是 getObject 方法中返回的对象。而 getBean("&userFactoryBean");获取的才是真正的 UserFactoryBean 对象。
我们通过上面五种方式,在 bean.xml 文件中把 bean 配置好之后,spring 就会自动扫描和解析相应的标签,并且帮我们创建和实例化 bean,然后放入 spring 容器中。
虽说基于 xml 文件的方式配置 bean,简单而且非常灵活,比较适合一些小项目。但如果遇到比较复杂的项目,则需要配置大量的 bean,而且 bean 之间的关系错综复杂,这样久而久之会导致 xml 文件迅速膨胀,非常不利于 bean 的管理。
2. Component 注解
为了解决 bean 太多时,xml 文件过大,从而导致膨胀不好维护的问题。在 spring2.5 中开始支持:@Component、@Repository、@Service、@Controller 等注解定义 bean。
如果你有看过这些注解的源码的话,就会惊奇得发现:其实后三种注解也是 @Component。
@Component 系列注解的出现,给我们带来了极大的便利。我们不需要像以前那样在 bean.xml 文件中配置 bean 了,现在只用在类上加 Component、Repository、Service、Controller,这四种注解中的任意一种,就能轻松完成 bean 的定义。
其实,这四种注解在功能上没有特别的区别,不过在业界有个不成文的约定:
Controller 一般用在控制层
Service 一般用在业务层
Repository 一般用在数据层
Component 一般用在公共组件上
太棒了,简直一下子解放了我们的双手。
不过,需要特别注意的是,通过这种 @Component 扫描注解的方式定义 bean 的前提是:需要先配置扫描路径。
目前常用的配置扫描路径的方式如下:
在 applicationContext.xml 文件中使用context:component-scan标签。例如:
在 springboot 的启动类上加上 @ComponentScan 注解,例如:
直接在 SpringBootApplication 注解上加,它支持 ComponentScan 功能:
当然,如果你需要扫描的类跟 springboot 的入口类,在同一级或者子级的包下面,无需指定 scanBasePackages 参数,spring 默认会从入口类的同一级或者子级的包去找。
此外,除了上述四种 @Component 注解之外,springboot 还增加了 @RestController 注解,它是一种特殊的 @Controller 注解,所以也是 @Component 注解。
@RestController 还支持 @ResponseBody 注解的功能,即将接口响应数据的格式自动转换成 json。
@Component 系列注解已经让我们爱不释手了,它目前是我们日常工作中最多的定义 bean 的方式。
3. JavaConfig
@Component 系列注解虽说使用起来非常方便,但是 bean 的创建过程完全交给 spring 容器来完成,我们没办法自己控制。
spring 从 3.0 以后,开始支持 JavaConfig 的方式定义 bean。它可以看做 spring 的配置文件,但并非真正的配置文件,我们需要通过编码 java 代码的方式创建 bean。例如:
在 JavaConfig 类上加 @Configuration 注解,相当于配置了<beans>标签。而在方法上加 @Bean 注解,相当于配置了<bean>标签。
此外,springboot 还引入了一些列的 @Conditional 注解,用来控制 bean 的创建。
@ConditionalOnClass 注解的功能是当项目中存在 Country 类时,才实例化 Person 类。换句话说就是,如果项目中不存在 Country 类,就不实例化 Person 类。
这个功能非常有用,相当于一个开关控制着 Person 类,只有满足一定条件才能实例化。
spring 中使用比较多的 Conditional 还有:
ConditionalOnBean
ConditionalOnProperty
ConditionalOnMissingClass
ConditionalOnMissingBean
ConditionalOnWebApplication
下面用一张图整体认识一下 @Conditional 家族:
nice,有了这些功能,我们终于可以告别麻烦的 xml 时代了。
4. Import 注解
通过前面介绍的 @Configuration 和 @Bean 相结合的方式,我们可以通过代码定义 bean。但这种方式有一定的局限性,它只能创建该类中定义的 bean 实例,不能创建其他类的 bean 实例,如果我们想创建其他类的 bean 实例该怎么办呢?
这时可以使用 @Import 注解导入。
4.1 普通类
spring4.2 之后 @Import 注解可以实例化普通类的 bean 实例。例如:
先定义了 Role 类:
接下来使用 @Import 注解导入 Role 类:
然后在调用的地方通过 @Autowired 注解注入所需的 bean。
聪明的你可能会发现,我没有在任何地方定义过 Role 的 bean,但 spring 却能自动创建该类的 bean 实例,这是为什么呢?
这也许正是 @Import 注解的强大之处。
此时,有些朋友可能会问:@Import 注解能定义单个类的 bean,但如果有多个类需要定义 bean 该怎么办呢?
恭喜你,这是个好问题,因为 @Import 注解也支持。
甚至,如果你想偷懒,不想写这种 MyConfig 类,springboot 也欢迎。
可以将 @Import 加到 springboot 的启动类上。
这样也能生效?
springboot 的启动类一般都会加 @SpringBootApplication 注解,该注解上加了 @SpringBootConfiguration 注解。
而 @SpringBootConfiguration 注解,上面又加了 @Configuration 注解
所以,springboot 启动类本身带有 @Configuration 注解的功能。
意不意外?惊不惊喜?
4.2 Configuration 类
上面介绍了 @Import 注解导入普通类的方法,它同时也支持导入 Configuration 类。
先定义一个 Configuration 类:
然后在另外一个 Configuration 类中引入前面的 Configuration 类:
这种方式,如果 MyConfig2 类已经在 spring 指定的扫描目录或者子目录下,则 MyConfig 类会显得有点多余。因为 MyConfig2 类本身就是一个配置类,它里面就能定义 bean。
但如果 MyConfig2 类不在指定的 spring 扫描目录或者子目录下,则通过 MyConfig 类的导入功能,也能把 MyConfig2 类识别成配置类。这就有点厉害了喔。
其实下面还有更高端的玩法。
swagger 作为一个优秀的文档生成框架,在 spring 项目中越来越受欢迎。接下来,我们以 swagger2 为例,介绍一下它是如何导入相关类的。
众所周知,我们引入 swagger 相关 jar 包之后,只需要在 springboot 的启动类上加上 @EnableSwagger2 注解,就能开启 swagger 的功能。
其中 @EnableSwagger2 注解中导入了 Swagger2DocumentationConfiguration 类。
该类是一个 Configuration 类,它又导入了另外两个类:
SpringfoxWebMvcConfiguration
SwaggerCommonConfiguration
SpringfoxWebMvcConfiguration 类又会导入新的 Configuration 类,并且通过 @ComponentScan 注解扫描了一些其他的路径。
SwaggerCommonConfiguration 同样也通过 @ComponentScan 注解扫描了一些额外的路径。
如此一来,我们通过一个简单的 @EnableSwagger2 注解,就能轻松的导入 swagger 所需的一系列 bean,并且拥有 swagger 的功能。
还有什么好说的,狂起点赞,简直完美。
4.3 ImportSelector
上面提到的 Configuration 类,它的功能非常强大。但怎么说呢,它不太适合加复杂的判断条件,根据某些条件定义这些 bean,根据另外的条件定义那些 bean。
那么,这种需求该怎么实现呢?
这时就可以使用 ImportSelector 接口了。
首先定义一个类实现 ImportSelector 接口:
重写 selectImports 方法,在该方法中指定需要定义 bean 的类名,注意要包含完整路径,而非相对路径。
然后在 MyConfig 类上 @Import 导入这个类即可:
朋友们是不是又发现了一个新大陆?
不过,这个注解还有更牛逼的用途。
@EnableAutoConfiguration 注解中导入了 AutoConfigurationImportSelector 类,并且里面包含系统参数名称:spring.boot.enableautoconfiguration。
AutoConfigurationImportSelector 类实现了 ImportSelector 接口。
并且重写了 selectImports 方法,该方法会根据某些注解去找所有需要创建 bean 的类名,然后返回这些类名。其中在查找这些类名之前,先调用 isEnabled 方法,判断是否需要继续查找。
该方法会根据 ENABLED_OVERRIDE_PROPERTY 的值来作为判断条件。
而这个值就是 spring.boot.enableautoconfiguration。
换句话说,这里能根据系统参数控制 bean 是否需要被实例化,优秀。
我个人认为实现 ImportSelector 接口的好处主要有以下两点:
把某个功能的相关类,可以放到一起,方面管理和维护。
重写 selectImports 方法时,能够根据条件判断某些类是否需要被实例化,或者某个条件实例化这些 bean,其他的条件实例化那些 bean 等。我们能够非常灵活的定制化 bean 的实例化。
4.4 ImportBeanDefinitionRegistrar
我们通过上面的这种方式,确实能够非常灵活的自定义 bean。
但它的自定义能力,还是有限的,它没法自定义 bean 的名称和作用域等属性。
有需求,就有解决方案。
接下来,我们一起看看 ImportBeanDefinitionRegistrar 接口的神奇之处。
先定义 CustomImportSelector 类实现 ImportBeanDefinitionRegistrar 接口:
重写 registerBeanDefinitions 方法,在该方法中我们可以获取 BeanDefinitionRegistry 对象,通过它去注册 bean。不过在注册 bean 之前,我们先要创建 BeanDefinition 对象,它里面可以自定义 bean 的名称、作用域等很多参数。
然后在 MyConfig 类上导入上面的类:
我们所熟悉的 fegin 功能,就是使用 ImportBeanDefinitionRegistrar 接口实现的:
5. PostProcessor
除此之外,spring 还提供了专门注册 bean 的接口:BeanDefinitionRegistryPostProcessor。
该接口的方法 postProcessBeanDefinitionRegistry 上有这样一段描述:
修改应用程序上下文的内部 bean 定义注册表标准初始化。所有常规 bean 定义都将被加载,但是还没有 bean 被实例化。这允许进一步添加在下一个后处理阶段开始之前定义 bean。
如果用这个接口来定义 bean,我们要做的事情就变得非常简单了。只需定义一个类实现 BeanDefinitionRegistryPostProcessor 接口。
重写 postProcessBeanDefinitionRegistry 方法,在该方法中能够获取 BeanDefinitionRegistry 对象,它负责 bean 的注册工作。
不过细心的朋友可能会发现,里面还多了一个 postProcessBeanFactory 方法,没有做任何实现。
这个方法其实是它的父接口:BeanFactoryPostProcessor 里的方法。
在应用程序上下文的标准 bean 工厂之后修改其内部 bean 工厂初始化。所有 bean 定义都已加载,但没有 bean 将被实例化。这允许重写或添加属性甚至可以初始化 bean。
既然这两个接口都能注册 bean,那么他们有什么区别?
BeanDefinitionRegistryPostProcessor 更侧重于 bean 的注册
BeanFactoryPostProcessor 更侧重于对已经注册的 bean 的属性进行修改,虽然也可以注册 bean。
此时,有些朋友可能会问:既然拿到 BeanDefinitionRegistry 对象就能注册 bean,那通过 BeanFactoryAware 的方式是不是也能注册 bean 呢?
从下面这张图能够看出 DefaultListableBeanFactory 就实现了 BeanDefinitionRegistry 接口。
这样一来,我们如果能够获取 DefaultListableBeanFactory 对象的实例,然后调用它的注册方法,不就可以注册 bean 了?
说时迟那时快,定义一个类实现 BeanFactoryAware 接口:
重写 setBeanFactory 方法,在该方法中能够获取 BeanFactory 对象,它能够强制转换成 DefaultListableBeanFactory 对象,然后通过该对象的实例注册 bean。
当你满怀喜悦的运行项目时,发现竟然报错了:
为什么会报错?
spring 中 bean 的创建过程顺序大致如下:
BeanFactoryAware 接口是在 bean 创建成功,并且完成依赖注入之后,在真正初始化之前才被调用的。在这个时候去注册 bean 意义不大,因为这个接口是给我们获取 bean 的,并不建议去注册 bean,会引发很多问题。
此外,ApplicationContextRegistry 和 ApplicationListener 接口也有类似的问题,我们可以用他们获取 bean,但不建议用它们注册 bean。
end
最后我整理的一些不错的关于 Spring 的学习资料都放这里了
需要资料的同学直接点击spring学习资料整理获取即可
版权声明: 本文为 InfoQ 作者【北游学Java】的原创文章。
原文链接:【http://xie.infoq.cn/article/86215213e9a2999ad3df24a2a】。文章转载请联系作者。
评论