《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 3
2.4 注解驱动的 IOC
从 Spring Boot 发布以来,使用 XML 配置文件的场景越来越少,取而代之的是基于注解配置类来驱动 IOC 容器。从 Spring Framework 推出3.0版本后,支持的最低 Java 版本为 Java 5,Java 5 最有特点的新特性之一就是引入了注解,Spring Framework 3.0 开始也通过引入大量注解代替XML 的方式进行声明式开发。
2.4.1 注解驱动 IOC 的依赖查找
相较于 XML 配置文件的驱动方式,基于注解驱动的配置会全部编写在配置类中,一个配置类可以理解为一个 XML 配置文件。配置类只需要在类上标注一个 @Configuration 注解。
与 XML 配置文件使用 标签的方式类比,通过注解配置类注册 Bean 所使用的是 @Bean 注解。方法的返回值代表注册的类型,方法名代表 Bean 的 id。也可以直接在 @Bean 注解中使用 name 属性显式地声明 Bean 的 id。
packagecom.yangjunbo.annotation.examplea;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassQuickstartConfiguration{// 等同于 <bean id="person" class="com.yangjunbo.annotation.examplea.Person"/>@Bean(name="person")publicPersonperson(){returnnewPerson();}}注解驱动的场景下,Spring Framework 提供的是 AnnotationConfigApplicationContext,用作注解驱动 IOC 容器。整体的编码方式非常简单,甚至与 XML 配置文件的方式没有什么区别。
packagecom.yangjunbo.annotation.examplea;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{ApplicationContextctx=newAnnotationConfigApplicationContext(QuickstartConfiguration.class);Personperson=ctx.getBean(Person.class);System.out.println(person);}}2.4.2 注解驱动 IOC 的依赖注入
编写基于 @Bean 注解的依赖注入,注入哪些属性完全是在代码中编写的。这种写法与使用 XML 配置文件的方式完全等价。
packagecom.yangjunbo.annotation.examplea;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassAnnotationDIConfiguration{@BeanpublicPersonperson(){Personperson=newPerson();person.setName("person");person.setAge(123);returnperson;}@BeanpublicCatcat(){Catcat=newCat();cat.setName("test-cat-anno");// 直接拿上面的person()方法作为返回值即可,相当于refcat.setMaster(person());returncat;}}packagecom.yangjunbo.annotation.examplea;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{ApplicationContextctx=newAnnotationConfigApplicationContext(QuickstartConfiguration.class);Personperson=ctx.getBean(Person.class);System.out.println(person);ApplicationContextctx2=newAnnotationConfigApplicationContext(AnnotationDIConfiguration.class);Catcat=ctx2.getBean(Cat.class);System.out.println(cat);}}2.4.3 组件注册与扫描机制
翻看 AnnotationConfigApplicationContext 的构造方法,可以发现它还有一个传入 basePackage 的构造方法,要想理解这个概念,就需要了解注解驱动中一个非常重要的机制:组件注册与扫描。
1.组件注册的根源:@Component
Spring Framework 规定,当一个类上标注了 @Component 注解(并被组件扫描)时,这个类将在 IOC 容器初始化时被创建,并注册到 IOC 容器中成为一个 Bean。
默认情况下,组件生成的 beanName 为“首字母小写的类名”(例如 Person 的默认名称是 person,DemoServiceImpl 的默认名称是demoServiceImpl)。如果需要指定 Bean 的名称,就在 @Component 注解中声明 value 属性。
注意,如果 标签不声明 id 属性,默认生成的 Bean 的名称不是“首字母小写的类名”,这一点与注解扫描的规则不同!
packagecom.yangjunbo.annotation.exampleb;importorg.springframework.stereotype.Component;@ComponentpublicclassPerson{}2.组件扫描
如果只是将 @Component 注解标注在类上而不进行扫描,那么 IOC 容器将无法感知到组件注册,当没有扫描组件时强行获取 Bean,一定会抛出 NoSuchBeanDefinitionException 异常,为此需要引入一个新的注解用于组件扫描:@ComponentScan。
@ComponentScan注解通常标注在配置类上。在代码清单2-31中,我们在配置类ComponentScanConfiguration上额外标注一个@ComponentScan 注解,并指定要扫描的包路径,它就可以扫描指定路径包及子包下的所有 @Component 组件。如果不指定扫描路径,就默认扫描本类所在包及子包下的所有 @Component 组件。可以一次性声明多个扫描的包。
声明了 @ComponentScan 之后,重新启动配置类,可以发现 Person 已经成功被注册,如果 Spring Framework 的版本比较老,可能会看到如下的写法:@ComponentScan(basePackages=“com.linkedbear.spring.annotation.c_scan.bean”),这两个写法实质上是一样的。
packagecom.yangjunbo.annotation.exampleb;importorg.springframework.context.annotation.ComponentScan;importorg.springframework.context.annotation.Configuration;@Configuration@ComponentScan("com.yangjunbo.annotation.exampleb")publicclassComponentScanConfiguration{}Spring Framework 还提供了第二种组件扫描的使用方式。在 AnnotationConfigApplicationContext 的构造方法中有一个类型为 String 可变参数的构造方法,当我们传入需要扫描的包之后,也可以直接扫描到那些标注了 @Component 的 Bean 并注册到 IOC 容器中。
packagecom.yangjunbo.annotation.exampleb;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{ApplicationContextctx=newAnnotationConfigApplicationContext(ComponentScanConfiguration.class);System.out.println(ctx.getBean(Person.class));ApplicationContextctx2=newAnnotationConfigApplicationContext("com.yangjunbo.annotation.exampleb");System.out.println(ctx2.getBean(Person.class));}}对于 XML 配置文件驱动的 IOC 容器同样可以启用组件扫描,只需要在 XML 配置文件中声明一个标签 context:component-scan,之后使用ClassPathXmlApplicationContext 驱动 IOC 容器,同样可以获取 person 对象。
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"><context:component-scanbase-package="com.yangjunbo.annotation.exampleb"/></beans>packagecom.yangjunbo.annotation.exampleb;importorg.springframework.beans.factory.BeanFactory;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;importorg.springframework.context.support.ClassPathXmlApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{ApplicationContextctx=newAnnotationConfigApplicationContext(ComponentScanConfiguration.class);System.out.println(ctx.getBean(Person.class));ApplicationContextctx2=newAnnotationConfigApplicationContext("com.yangjunbo.annotation.exampleb");System.out.println(ctx2.getBean(Person.class));BeanFactorybeanFactory=newClassPathXmlApplicationContext("annotation/inject-scan.xml");Personperson=beanFactory.getBean(Person.class);System.out.println(person);}}3.组件注册的其他注解
Spring Framework 为了迎合 Web 应用开发时所采用的经典三层架构,额外提供了三个注解:@Controller、@Service、@Repository,分别对应三层架构中的表现层、业务层、持久层。这三个注解的作用与 @Component 完全一致,其实它们的底层也都是 @Component。
有了以上三个注解,在进行符合三层架构的应用开发时,对于那些业务逻辑层的类,就可以直接标注注解,而不用一个个地写 标签或者借助 @Bean 注解进行组件注册。
4.@Configuration 也是 @Component
如果在驱动注解 IOC 容器时,直接扫描包括配置类在内的整个根包,则在运行 main 方法时会看到配置类 ComponentScanConfiguration 也被注册到 IOC 容器中。
packagecom.yangjunbo.annotation.exampleb;importorg.springframework.beans.factory.BeanFactory;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;importorg.springframework.context.support.ClassPathXmlApplicationContext;importjava.util.stream.Stream;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{ApplicationContextctx=newAnnotationConfigApplicationContext(ComponentScanConfiguration.class);System.out.println(ctx.getBean(Person.class));ApplicationContextctx2=newAnnotationConfigApplicationContext("com.yangjunbo.annotation.exampleb");System.out.println(ctx2.getBean(Person.class));BeanFactorybeanFactory=newClassPathXmlApplicationContext("annotation/inject-scan.xml");Personperson=beanFactory.getBean(Person.class);System.out.println(person);ApplicationContextctx3=newAnnotationConfigApplicationContext("com.yangjunbo.annotation.exampleb");String[]beanNames=ctx3.getBeanDefinitionNames();Stream.of(beanNames).forEach(System.out::println);}}翻看 @Configuration 注解的源码,可以发现它也被标注了 @Component 注解,说明被标注 @Configuration 注解的类对于 IOC 容器而言同样也是一个 Bean。
2.4.4 注解驱动与XML驱动互通
如果一个应用中既有注解驱动配置,又有 XML 配置文件,则可能出现由一方引入另一方配置的情况。
1.XML 配置文件引入注解驱动
在 XML 中要引入注解驱动,需要开启注解配置,同时注册对应的配置类。
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"><!-- 开启注解配置 --><context:annotation-config/><beanclass="com.yangjunbo.annotation.examplec.AnnotationDIConfiguration"/></beans>packagecom.yangjunbo.annotation.examplec;importorg.springframework.beans.factory.BeanFactory;importorg.springframework.context.support.ClassPathXmlApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{BeanFactorybeanFactory=newClassPathXmlApplicationContext("annotation/examplec/inject-annotation.xml");Catcat=beanFactory.getBean(Cat.class);System.out.println(cat);}}2.注解配置类引入 XML 配置文件
在注解配置类中引入 XML 配置文件,需要在配置类上标注 @ImportResource 注解,并声明配置文件的路径。
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="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"><beanid="person"class="com.yangjunbo.annotation.examplec.Person"><propertyname="name"value="test-person-byset"/><propertyname="age"value="18"/></bean><beanid="cat"class="com.yangjunbo.annotation.examplec.Cat"><propertyname="name"value="test-cat"/><!-- ref引用上面的person对象 --><propertyname="master"ref="person"/></bean></beans>packagecom.yangjunbo.annotation.examplec;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.ImportResource;@Configuration@ImportResource("classpath:annotation/examplec/inject-configuration.xml")publicclassXmlDIConfiguration{}packagecom.yangjunbo.annotation.examplec;importcom.yangjunbo.annotation.exampleb.ComponentScanConfiguration;importcom.yangjunbo.annotation.exampleb.Person;importorg.springframework.beans.factory.BeanFactory;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;importorg.springframework.context.support.ClassPathXmlApplicationContext;publicclassAnnotationConfigApplication{publicstaticvoidmain(String[]args)throwsException{BeanFactorybeanFactory=newClassPathXmlApplicationContext("annotation/examplec/inject-annotation.xml");Catcat=beanFactory.getBean(Cat.class);System.out.println(cat);ApplicationContextctx=newAnnotationConfigApplicationContext(XmlDIConfiguration.class);System.out.println(ctx.getBean(Cat.class));}}