Spring 高手之路 2——深入理解注解驱动配置与 XML 配置的融合与区别
1. 配置类的编写与 Bean 的注册
XML
配置中,我们通常采用ClassPathXmlApplicationContext
,它能够加载类路径下的XML
配置文件来初始化Spring
应用上下文。然而,在注解驱动的配置中,我们则使用以Annotation
开头和ApplicationContext
结尾的类,如AnnotationConfigApplicationContext
。AnnotationConfigApplicationContext
是Spring
容器的一种,它实现了ApplicationContext
接口。
对比于 XML
文件作为驱动,注解驱动需要的是配置类。一个配置类就可以类似的理解为一个 XML
。配置类没有特殊的限制,只需要在类上标注一个 @Configuration
注解即可。
我们创建一个 Book
类:
在 xml
中声明 Bean
是通过 <bean>
标签
如果要在配置类中替换掉 <bean>
标签,需要使用 @Bean
注解
我们创建一个配置类来注册这个 Book bean
:
在这个配置中,我们使用了 @Configuration
注解来表示这是一个配置类,类似于一个 XML
文件。我们在 book()
方法上使用了 @Bean
注解,这意味着这个方法将返回一个由 Spring
容器管理的对象。这个对象的类型就是 Book
,bean
的名称id
就是方法的名称,也就是 "book
"。
类似于 XML
配置的 <bean>
标签,@Bean
注解负责注册一个 bean
。你可以把 @Bean
注解看作是 <bean>
标签的替代品。
如果你想要更改这个 bean
的名称,你可以在 @Bean
注解中使用 name
属性:
这样,这个 Book bean
的名称就变成了 "mybook
"。
启动并初始化注解驱动的IOC
容器
ApplicationContext context = new AnnotationConfigApplicationContext(LibraryConfiguration.class)
这个语句创建了一个Spring
的应用上下文,它是以配置类LibraryConfiguration.class
作为输入的,这里明确指定配置类的Spring
应用上下文,适用于更一般的Spring
环境。
对比一下ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
这个语句则是Spring Boot
应用的入口,启动一个Spring Boot
应用。SpringApplication.run()
方法会创建一个Spring Boot
应用上下文(也就是一个SpringApplication
对象),这个上下文包含了Spring Boot
应用所有的Bean
和配置类,还有大量的默认配置。这个方法之后,Spring Boot
的自动配置就会起作用。你可以把SpringApplication.run()
创建的Spring Boot
上下文看作是更加功能丰富的Spring
上下文。
打印结果:
Java Programming
和Unknown
被打印,执行成功。
注意:@SpringBootApplication
是一个复合注解,它等效于同时使用了@Configuration
,@EnableAutoConfiguration
和@ComponentScan
。这三个注解的作用是:
@Configuration
:指明该类是一个配置类,它可能会有零个或多个@Bean
注解,方法产生的实例由Spring
容器管理。@EnableAutoConfiguration
:告诉Spring Boot
根据添加的jar
依赖自动配置你的Spring
应用。@ComponentScan
:Spring Boot
会自动扫描该类所在的包以及子包,查找所有的Spring
组件,包括@Configuration
类。
在非Spring Boot
的传统Spring
应用中,我们通常使用AnnotationConfigApplicationContext
或者ClassPathXmlApplicationContext
等来手动创建和初始化Spring
的IOC
容器。
"非Spring Boot
的传统Spring
应用"是指在Spring Boot
项目出现之前的Spring
项目,这些项目通常需要手动配置很多东西,例如数据库连接、事务管理、MVC
控制器等。这种类型的Spring
应用通常需要开发者对Spring
框架有深入的了解,才能做出正确的配置。
Spring Boot
是Spring
项目的一个子项目,它旨在简化Spring
应用的创建和配置过程。Spring Boot
提供了一系列的"起步依赖",使得开发者只需要添加少量的依赖就可以快速开始项目的开发。此外,Spring Boot
还提供了自动配置的特性,这使得开发者无需手动配置数据库连接、事务管理、MVC
控制器等,Spring Boot
会根据项目的依赖自动进行配置。
因此,"非Spring Boot
的传统Spring
应用"通常需要手动创建和初始化Spring
的IOC
容器,比如使用AnnotationConfigApplicationContext
或ClassPathXmlApplicationContext
等。在Spring Boot
应用中,这个过程被自动化了,开发者只需要在main
方法中调用SpringApplication.run
方法,Spring Boot
就会自动创建和初始化Spring
的IOC
容器。SpringApplication.run(Application.class, args);
语句就是启动Spring Boot
应用的关键。它会启动一个应用上下文,这个上下文会加载所有的Spring
组件,并且也会启动Spring
的IOC
容器。在这个过程中,所有通过@Bean
注解定义的bean
都会被创建,并注册到IOC
容器中。
有人说,那学习 Spring Boot 就好了,学什么 Spring 和 Spring MVC 啊,这不是落后了吗
Spring Boot
并不是Spring
框架的替代品,而是建立在Spring
框架之上的一种工具,它内部仍然使用Spring
框架的很多核心技术,包括Spring MVC
。所以,当我们在使用Spring Boot
时,我们实际上仍然在使用Spring MVC
来处理Web
层的事务。
简而言之,Spring MVC
是一个用于构建Web
应用程序的框架,而Spring Boot
是一个用于简化Spring
应用程序开发的工具,它内部仍然使用了Spring MVC
。你在Spring Boot
应用程序中使用的@Controller
、@Service
、@Autowired
等注解,其实都是Spring
框架提供的,所以,原理性的东西还是需要知道。
2. 注解驱动 IOC 的依赖注入与 XML 依赖注入对比
我们就以上面的例子来说,假设配置类注册了两个bean
,并设置相关的属性:
这里的方法有@Bean
注解,这个注解告诉Spring
,这个方法返回的对象需要被注册到Spring
的IOC
容器中。
如果不用注解,要实现相同功能的话,对应的XML
配置如下:
在这个XML
配置中,我们定义了两个<bean>
元素,分别用来创建Book
对象和Library
对象。在创建Book
对象时,我们使用了<property>
元素来设置title
和author
属性。在创建Library
对象时,我们也使用了<property>
元素,但是这次我们使用了ref
属性来引用已经创建的Book
对象,这就相当于将Book
对象注入到Library
对象中。
3. Spring 中组件的概念
在Spring
框架中,当我们说 "组件" 的时候,我们通常指的是被Spring
管理的各种Java
对象,这些对象在Spring
的应用上下文中作为Bean
存在。这些组件可能是服务层的类、数据访问层的类、控制器类、配置类等等。
@ComponentScan
注解会扫描指定的包(及其子包)中的类,如果这些类上标注了@Component
、@Controller
、@Service
、@Repository
、@Configuration
等注解,那么Spring
就会为这些类创建Bean
定义,并将这些Bean
定义注册到Spring
的应用上下文中。因此,我们通常说@ComponentScan
进行了"组件扫描",因为它扫描的是标注了上述注解的类,这些类在Spring
中都被视为组件。
而这些注解标记的类,最终在Spring
的应用上下文中都会被创建为Bean
,因此,你也可以理解@ComponentScan
为"Bean
扫描"。但是需要注意的是,@ComponentScan
只负责扫描和注册Bean
定义,Bean
定义就是元数据描述,包括了如何创建Bean
实例的信息。
总结一下,@ComponentScan
注解会扫描并注册的"组件"包括:
标注了
@Component
注解的类标注了
@Controller
注解的类(Spring MVC
中的控制器组件)标注了
@Service
注解的类(服务层组件)标注了
@Repository
注解的类(数据访问层组件)标注了
@Configuration
注解的类(配置类)
这些组件最终都会在Spring
的应用上下文中以Bean
的形式存在。
4. 组件注册
这里Library
标注 @Configuration
注解,即代表该类会被注册到 IOC
容器中作为一个 Bean
。
相当于 xml
中的:
如果想指定 Bean
的名称,可以直接在 @Configuration
中声明 value
属性即可
@Component("libra")
就将这个bean
的名称改为了libra
,如果不指定 Bean
的名称,它的默认规则是 “类名的首字母小写”(例如Library
默认名称是 library
)
5. 组件扫描
如果我们只写了@Component
, @Configuration
这样的注解,IOC
容器是找不到这些组件的。
5.1 使用 @ComponentScan 的组件扫描
忽略掉之前的例子,在这里我们需要运行的代码如下:
如果不写@ComponentScan
,而且@Component
注解标识的类不在当前包或者子包,那么就会报错。
难道@Component
注解标识的类在当前包或者当前包的子包,主程序上就可以不写@ComponentScan
了吗?
是的!前面说了,@SpringBootApplication
包含了 @ComponentScan
,其实已经帮我们写了!只有组件和主程序不在一个共同的根包下,才需要显式地使用 @ComponentScan
注解。由于 Spring Boot
的设计原则是“约定优于配置”,所以推荐将主应用类放在根包下。
在应用中,我们的组件(带有 @Component
、@Service
、@Repository
、@Controller
等注解的类)和主配置类位于不同的包中,并且主配置类或者启动类没有使用 @ComponentScan
指定扫描这些包,那么在运行时就会报错,因为Spring
找不到这些组件。
主程序:
@ComponentScan
不一定非要写在主程序(通常是指 Spring Boot
的启动类)上,它可以写在任何配置类(标记有 @Configuration
注解的类)上。@ComponentScan
注解会告诉 Spring
从哪些包开始进行组件扫描。
为了简化配置,我们通常会将 @ComponentScan
放在主程序上,因为主程序一般会位于根包下,这样可以扫描到所有的子包。这里为了演示,并没有把主程序放在根目录。
我们上面说过,@ComponentScan
只负责扫描和注册Bean
定义,只有需要某个Bean
时,这个Bean
才会实例化。
那怎么才能知道是不是需要这个 Bean 呢?
我来给大家举例子,并且还会说明Bean
的创建顺序问题,"需要某个Bean
"通常体现在以下几个方面:
依赖注入(
Dependency Injection
): 如果一个BeanA
的字段或者构造方法被标注为@Autowired
或者@Resource
,那么Spring
就会尝试去寻找类型匹配的BeanB
并注入到BeanA
中。在这个过程中,如果BeanB
还没有被创建,那么Spring
就会先创建BeanB
的实例。
BeanA
依赖于BeanB
。在这种情况下,当你尝试获取BeanA
的实例时,Spring
会首先创建BeanB
的实例,然后把这个实例注入到BeanA
中,最后创建BeanA
的实例。在这个例子中,BeanB
会先于BeanA
被创建。
这种方式的一个主要优点是,我们不需要关心Bean
的创建顺序,Spring
会自动解决这个问题。这是Spring IoC
容器的一个重要特性,也是为什么它能够使我们的代码更加简洁和易于维护的原因。
Spring
框架调用: 有些情况下,Spring
框架的一些组件或者模块可能需要用到你定义的Bean
。比如,如果你定义了一个@Controller
,那么在处理HTTP
请求时,Spring MVC
就会需要使用到这个@Controller Bean
。如果这个时候Bean
还没有被创建,那么Spring
也会先创建它的实例。
假设我们有一个名为BookController
的类,该类需要一个BookService
对象来处理一些业务逻辑。
BookService
类
当Spring Boot
应用程序启动时,以下步骤将会发生:
首先,
Spring
框架通过@ComponentScan
注解扫描类路径,找到了BookController
、BookService
和BookMapper
等类,并为它们创建Bean
定义,注册到Spring
的应用上下文中。当一个请求到达并需要使用到
BookController
时,Spring
框架会尝试创建一个BookController
的Bean
实例。在创建
BookController
的Bean
实例的过程中,Spring
框架发现BookController
类中需要一个BookService
的Bean
实例(通过@Autowired
注解指定),于是Spring
框架会先去创建一个BookService
的Bean
实例。同样,在创建
BookService
的Bean
实例的过程中,Spring
框架发现BookService
类中需要一个BookMapper
的Bean
实例(通过@Autowired
注解指定),于是Spring
框架会先去创建一个BookMapper
的Bean
实例。在所有依赖的
Bean
都被创建并注入之后,BookController
的Bean
实例最终被创建完成,可以处理来自用户的请求了。
在这个过程中,BookController
、BookService
和BookMapper
这三个Bean
的创建顺序是有严格要求的,必须按照他们之间的依赖关系来创建。只有当一个Bean
的所有依赖都已经被创建并注入后,这个Bean
才能被创建。这就是Spring
框架的IoC
(控制反转)和DI
(依赖注入)的机制。
手动获取: 如果你在代码中手动通过
ApplicationContext.getBean()
方法获取某个Bean
,那么Spring
也会在这个时候创建对应的Bean
实例,如果还没有创建的话。
总的来说,"需要"一个Bean
,是指在运行时有其他代码需要使用到这个Bean
的实例,这个"需要"可能来源于其他Bean
的依赖,也可能来源于框架的调用,或者你手动获取。在这种需要出现时,如果对应的Bean
还没有被创建,那么Spring
就会根据之前通过@ComponentScan
等方式注册的Bean
定义,创建对应的Bean
实例。
5.2 xml 中启用 component-scan 组件扫描
对应于 @ComponentScan
的 XML
配置是 <context:component-scan>
标签
在这段 XML
配置中,<context:component-scan>
标签指定了 Spring
需要扫描 com.example
包及其子包下的所有类,这与 @ComponentScan
注解的功能是一样的。
注意:在使用 <context:component-scan>
标签时,需要在 XML
配置文件的顶部包含 context
命名空间和相应的 schema
位置(xsi:schemaLocation
)。
5.3 不使用 @ComponentScan 的组件扫描
如果我们不写@ComponentScan
注解,那么这里可以把主程序改为如下:
AnnotationConfigApplicationContext
的构造方法中有一个是填写basePackages
路径的,可以接受一个或多个包的名字作为参数,然后扫描这些包及其子包。
运行结果如下:
在这个例子中,Spring
将会扫描 com.example
包及其所有子包,查找并注册所有的 Bean
,达到和@ComponentScan
注解一样的效果。
我们也可以手动创建一个配置类来注册bean
,那么想要运行得到一样的效果,需要的代码如下:
主程序:
我们创建了一个配置类LibraryConfiguration
,用于定义Book
和Library
这两个bean
。然后以配置类LibraryConfiguration.class
作为输入的来创建Spring
的IOC
容器(Spring
应用上下文就是Spring IOC
容器)。
运行结果和前面一样。
注意,在这个例子里,如果你写
@ComponentScan
,并且SpringApplication.run(Application.class, args);
作为Spring
上下文,那么这里运行配置类需要去掉Book
和Library
类的@Component
注解,不然会报错A bean with that name has already been defined
。这是因为如果同时在Book
和Library
类上使用了@Component
注解,而且配置类LibraryConfiguration
上使用了@Configuration
注解,这都会被@ComponentScan
扫描到,那么Book
和Library
的实例将会被创建并注册两次。正确的做法是,要么在配置类中通过@Bean
注解的方法创建Book
和Library
的实例,要么在Book
和Library
类上写@Component
注解。如果不是第三方库,我们一般选择后者。
为什么要有配置类出现?所有的 Bean 上面使用 @Component,用 @ComponentScan 注解扫描不就能解决了吗?
我们在使用一些第三方库时,需要对这些库进行一些特定的配置。这些配置信息,我们可能无法直接通过注解或者XML
来完成,或者通过这些方式完成起来非常麻烦。而配置类可以很好地解决这个问题。通过配置类,我们可以在Java
代码中完成任何复杂的配置逻辑。
假设你正在使用 MyBatis
,在这种情况下可能需要配置一个SqlSessionFactory
,在大多数情况下,我们无法(也不应该)直接修改第三方库的代码,所以无法直接在SqlSessionFactory
类或其他类上添加@Configuration
、@Component
等注解。为了能够在Spring
中使用和配置这些第三方库,我们需要创建自己的配置类,并在其中定义@Bean
方法来初始化和配置这些类的实例。这样就可以灵活地控制这些类的实例化过程,并且可以利用Spring
的依赖注入功能。
下面是一个使用@Configuration
和@Bean
来配置MyBatis
的例子:
sqlSessionFactory
方法创建一个SqlSessionFactoryBean
对象,并使用DataSource
(Spring Boot
默认为你配置的一个Bean
)进行初始化。然后,它指定MyBatis mapper XML
文件的位置,最后返回SqlSessionFactory
对象。
通过这种方式,你可以灵活地配置MyBatis
,并将其整合到Spring
应用中。这是一种比使用XML
配置文件或仅仅依赖于自动配置更为灵活和强大的方式。
6. 组件注册的其他注解
@Controller
, @Service
, @Repository
和@Component
一样的效果,它们都会被 Spring IoC
容器识别,并将类实例化为 Bean
。让我们来看这些注解:
@Controller
:这个注解通常标注在表示表现层(比如Web
层)的类上,如Spring MVC
中的控制器。它们处理用户的HTTP
请求并返回响应。虽然@Controller
与@Component
在功能上是类似的,但@Controller
注解的使用表示了一种语义化的分层结构,使得控制层代码更加清晰。
@Service
:这个注解通常用于标注业务层的类,这些类负责处理业务逻辑。使用@Service
注解表明该类是业务处理的核心类,使得代码更具有语义化。
@Repository
:这个注解用于标记数据访问层,也就是数据访问对象或DAO
层的组件。在数据库操作的实现类上使用@Repository
注解,这样Spring
将自动处理与数据库相关的异常并将它们转化为Spring
的DataAccessExceptions
。
在实际开发中,几乎很少看到@Repository
,而是利用 MyBatis
的 @Mapper
或 @MapperScan
实现数据访问,通常做法是,@MapperScan
注解用于扫描特定包及其子包下的接口,这些接口被称为 Mapper
接口。Mapper
接口方法定义了 SQL
查询语句的签名,而具体的 SQL
查询语句则通常在与接口同名的 XML
文件中定义。
@MapperScan("com.example.**.mapper")
会扫描 com.example
包及其所有子包下的名为 mapper
的包,以及 mapper
包的子包。 **
是一个通配符,代表任意深度的子包。
举个例子,以下是一个 Mapper
接口的定义:
对应的 XML
文件(通常位于 resources
目录下,并且与接口在相同的包路径中)
注意:在 XML
文件中的 namespace
属性值必须与 Mapper
接口的全限定类名相同,<select>
标签的 id
属性值必须与接口方法名相同。
然后,在 Spring Boot
的主类上,我们使用 @MapperScan
注解指定要扫描的包:
这样,MyBatis
就会自动为 UserMapper
接口创建一个实现类(实际上是一个代理对象),并将其注册到 Spring IOC
容器中,你就可以在你的服务类中直接注入 BookMapper
并使用它。
可能有小伙伴注意到了,这几个注解中都有这么一段代码
@AliasFor
是 Spring
框架的注解,它允许你在一个注解属性上声明别名。在 Spring
的许多核心注解中,@AliasFor
用于声明一个或多个别名属性。
举个例子,在 @Controller
, @Service
, @Repository
注解中,value()
方法上的 @AliasFor
声明了一个别名属性,它的目标注解是 @Component
,具体的别名属性是 value
。也就是说,当我们在 @Controller
, @Service
, @Repository
注解上使用 value()
方法设置值时,实际上也就相当于在 @Component
注解上设置了 name
属性的值。同时,这也表明了 @Controller
, @Service
, @Repository
注解本身就是一个特殊的 @Component
。
7. 将注解驱动的配置与 XML 驱动的配置结合使用
有没有这么一种可能,一个旧的Spring
项目,里面有很多旧的XML
配置,现在你接手了,想要全部用注解驱动,不想再写XML
配置了,那应该怎么兼容呢?
假设我们有一个旧的Spring XML
配置文件 old-config.xml
:
这个文件定义了一个名为 "oldBean
" 的bean
。
然后,我们编写一个新的注解驱动的配置类:
在这个新的配置类中,我们使用 @ImportResource
注解来引入旧的XML
配置文件,并定义了一个新的bean
"newBean
"。@ImportResource("classpath:old-config.xml")
告诉Spring
在初始化AppConfig
配置类时,去类路径下寻找old-config.xml
文件,并加载其中的配置。
当我们启动应用程序时,Spring
会创建一个 ApplicationContext
,这个 ApplicationContext
会包含 old-config.xml
文件中定义的所有beans
(例如 "oldBean
"),以及 NewConfig
类中定义的所有beans
(例如 "newBean
")。
在以上的main
方法中,我们通过使用AnnotationConfigApplicationContext
并传入NewConfig.class
作为参数,初始化了一个Spring
上下文。在这个上下文中,既包含了从old-config.xml
导入的bean
,也包含了在NewConfig
配置类中使用@Bean
注解定义的bean
。
所以,通过使用 @ImportResource
,可以在新的注解配置中引入旧的XML
配置,这样就可以在不打断旧的XML
配置的基础上逐步迁移至新的注解配置。
上面我们说到类路径,什么是类路径?
resources
目录就是类路径(classpath
)的一部分。所以当我们说"类路径下"的时候,实际上也包含了"resources
"目录。JVM
在运行时,会把"src/main/resources
"目录下的所有文件和文件夹都添加到类路径中。
例如有一个XML
文件位于"src/main/resources/config/some-context.xml
",那么可以用以下方式来引用它:
这里可以描述为在类路径下的'config
'目录中查找'some-context.xml
'文件。
为什么说JVM
在运行时,会把"src/main/resources
"目录下的所有文件和文件夹都添加到类路径中?
当你编译并运行一个Java
项目时,JVM
需要知道去哪里查找.class
文件以及其他资源文件。这个查找的位置就是所谓的类路径(Classpath
)。类路径可以包含文件系统上的目录,也可以包含jar
文件。简单的说,类路径就是JVM
查找类和资源的地方。
在一个标准的Maven
项目结构中,Java
源代码通常在src/main/java
目录下,而像是配置文件、图片、静态网页等资源文件则放在src/main/resources
目录下。
当你构建项目时,Maven
(或者其他的构建工具,如Gradle
)会把src/main/java
目录下的.java
文件编译成.class
文件,并把它们和src/main/resources
目录下的资源文件一起复制到项目的输出目录(通常是target/classes
目录)。
然后当你运行程序时,JVM
会把target/classes
目录(即编译后的src/main/java
和src/main/resources
)添加到类路径中,这样JVM
就可以找到程序运行所需的类和资源了。
如果有一个名为application.properties
的文件在src/main/resources
目录下,就可以使用类路径来访问它,就像这样:classpath:application.properties
。在这里classpath:
前缀告诉JVM
这个路径是相对于类路径的,所以它会在类路径中查找application.properties
文件。因为src/main/resources
在运行时被添加到了类路径,所以JVM
能找到这个文件。
8. 思考总结
8.1 为什么我们需要注册组件,这与 Bean 注册有什么区别?
在Spring
框架中,Bean
对象是由Spring IoC
容器创建和管理的。通常Bean
对象是应用程序中的业务逻辑组件,如数据访问对象(DAO
)或其他服务类。
组件注册,或者说在Spring
中通过@Component
或者其派生注解(@Service
, @Controller
, @Repository
等)标记的类,是告诉Spring
框架这个类是一个组件,Spring
需要创建它的实例并管理它的生命周期。这样当使用到这个类的时候,Spring
就可以自动地创建这个类的实例并注入到需要的地方。
Bean
注册和组件注册其实是非常类似的,都是为了让Spring
知道它需要管理哪些类的实例。区别在于Bean
注册通常发生在配置类中,使用@Bean
注解来明确地定义每一个Bean
,而组件注册则是通过在类上使用@Component
或者其派生注解来告诉Spring
,这个类是一个组件,Spring
应该自动地为其创建实例。
8.2 什么是组件扫描,为什么我们需要它,它是如何工作的?
组件扫描是Spring
的一种机制,用于自动发现应用程序中的Spring
组件,并自动地为这些组件创建Bean
定义,然后将它们注册到Spring
的应用上下文中,我们可以通过使用@ComponentScan
注解来启动组件扫描。
我们需要组件扫描是因为它可以大大简化配置过程,我们不再需要为应用程序中的每个类都显式地创建Bean
。而是通过简单地在类上添加@Component
或者其派生注解,并启动组件扫描,就可以让Spring
自动地为我们的类创建Bean
并管理它们。
组件扫描的工作过程如下:使用@ComponentScan
注解并指定一个或多个包路径时,Spring
会扫描这些包路径及其子包中的所有类。对于标记了@Component
或者其派生注解的类,Spring
会在应用上下文启动时为它们创建Bean
,并将这些Bean
定义注册到Spring
的应用上下文中。当需要使用这些类的实例时,Spring
就可以自动注入这些实例。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
版权声明: 本文为 InfoQ 作者【砖业洋__】的原创文章。
原文链接:【http://xie.infoq.cn/article/6d82671bd1000d48903c77cda】。文章转载请联系作者。
评论