前言
本文为 【Spring】classpath扫描和组件管理 相关知识,下边将对@Component 和及其派生出的其他注解
,自动检测类和注册beanDifination
,组件命名
,为自动检测组件提供scope
,使用过滤器自定义扫描
,在组件中定义Bean元数据
,基于Java的容器配置
,BeanFactory和FactoryBean
,环境抽象
,事件机制
等进行详尽介绍~
📌博主主页:小新要变强 的主页
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
👉Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)
↩️本文上接:最新最全面的Spring详解(一)——Spring概述与IOC容器
目录
【Spring】classpath扫描和组件管理
- 前言
- 目录
- 1️⃣`@Component `和及其派生出的其他注解
- 2️⃣自动检测类和注册beanDifination
- 3️⃣组件命名
- 4️⃣为自动检测组件提供scope
- 5️⃣使用过滤器自定义扫描
- 6️⃣在组件中定义Bean元数据
- 7️⃣基于Java的容器配置
- 8️⃣BeanFactory和FactoryBean
- 9️⃣环境抽象
- 🔟事件机制
- 后记
本章中的大多数例子都使用【XML来指定配置元数据】,这些元数据在Spring容器启动时被扫描,每一个bean的元数据对应生成一个“BeanDefinition”。
本节我们可以通过【扫描类路径】隐式检测候选组件。 【候选组件】指的是通过扫描筛选并在容器中注册了相应beanDifination的类。 这样就不需要使用XML来执行bean注册。 相反,您可以使用注解(例如,【@Component 】)。
更多操作从Spring 3.0开始,Spring JavaConfig项目提供的许多特性都是核心Spring框架的一部分。 这允许您使用Java而不是使用传统的XML文件来定义bean。
1️⃣@Component
和及其派生出的其他注解
@Component
是任何spring管理组件的通用注解。@Repository
、@Service
和@Controller
是【@Component
】用于更具体用例的注解(分别在持久性、服务和表示层中)。这些注解对于我们后期对特定bean进行批量处理时是有帮助的。
2️⃣自动检测类和注册beanDifination
Spring可以自动检测类的信息,并将相应的【BeanDefinition】实例注册到【ApplicationContext】中。 例如,以下两个类适合这样的自动检测:
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
要自动检测这些类并注册相应的bean,您需要将【@ComponentScan
】添加到您的【 @Configuration
】类中,其中【basePackages
】属性是这两个类的公共父包。说人话就是:指定一个包名,自动扫描会检测这个包及其子包下的所有类信息。
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
为简单起见,前面的示例可能使用了注解的value属性 (即@ComponentScan ("org.example")
)。
当然我们可以使用以下XML代替,他们是等效的:
<?xml version="1.0" encoding="UTF-8"?>
<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
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example"/>
</beans>
注意: <context:component-scan>
的使用会隐式启用 <context:annotation-config>
,当使用 <context:component-scan>
时,通常不需要包含<context:annotation-config>
元素。
3️⃣组件命名
当组件作为扫描过程的一部分被自动检测时,它的bean名是由该扫描器所知道的“BeanNameGenerator”策略生成的。
默认情况下,会使用【@Component
】, 【@Repository
】,【@Service
】和【@Controller
】注解的value值,因此将该名称会提供给相应的beanDefination。 如果你的注解不包含任何名称属性,会有默认bean名称生成器将返回【非首字母大写的非全限定类名】。 例如,如果检测到以下组件类,则名称为【myMovieLister
】和【movieFinderImp
】,这个和xml自动生成的标识符名称不同:
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
4️⃣为自动检测组件提供scope
与spring管理的组件一样,自动检测组件的默认和最常见的作用域是“单例”。 然而,有时您需要一个不同的范围,可以由' @Scope '
注解指定。 您可以在注解中提供作用域的名称,如下面的示例所示:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
5️⃣使用过滤器自定义扫描
默认情况下,带有【@Component
】、【@Repository
】、【@Service
】、【@Controller
】、【@Configuration
】注解的类是一定能被筛选器选中并进行注册的候选组件。 但是,您可以通过应用自定义过滤器来修改和扩展此行为,自由定制筛选哪些或不包含那些组件。 将它们作为@ComponentScan
注解的includeFilters
或 excludeFilters
属性添加(或者作为XML配置中’ <context:include-filter />
‘或’ <context:exclude-filter />
‘元素的子元素)。 每个筛选器元素都需要’ type ‘和’ expression '属性。 下表描述了过滤选项:
过滤方式 | 示例表达式 | 描述 |
---|---|---|
annotation (默认) | org.example.SomeAnnotation | 要在目标组件的类型级别上“存在”或“元注解存在”的注解。 |
assignable | org.example.SomeClass | 指定要排除的bean的类 |
aspectj | org.example…*Service+ | 要被目标组件匹配的AspectJ类型表达式,后边会学习 |
regex | org.example.Default.* | 由目标组件的类名匹配的正则表达式 |
custom | org.example.MyTypeFilter | ’ org.springframework.core.type的自定义实现,TypeFilter”接口。 |
下面的示例显示了忽略所有【@Repository
】注解,而使用【stub】包下的类进行替换:
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
// ...
}
下面的例子显示了等效的XML:
<beans>
<context:component-scan base-package="org.example">
<context:include-filter type="regex"
expression=".*Stub.*Repository"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
</beans>
【小知识】: 您还可以通过在注解上设置useDefaultFilters=false
或通过提供use-default-filters="false"
作为<component-scan/>
元素的属性来禁用默认过滤器。 这将有效地禁用使用【@Component
】、【@Repository
】、【@Service
】、【 @Controller
】、【@Configuration
】注解或元注解的类的自动检测。
6️⃣在组件中定义Bean元数据
Spring组件还可以向容器提供beanDifination元数据。 可以使用 @Bean 注解来实现这一点。
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
前面的类是一个Spring组件,它的【doWork()
】方法中包含特定于应用程序的代码。 然而,它还提供了一个beanDifination,该beanDifination有一个引用方法【public Instance()
】的工厂方法。 【@Bean注解
】标识工厂方法,通过【@Qualifier
】注解标识一个限定符值。 其他可以指定的方法级注解有【@Scope
】, 【@Lazy
】等。
下面的例子展示了如何做到这一点:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
}
7️⃣基于Java的容器配置
🍀(1)@Bean和@Configuration
Spring新的java配置支持的中心组件是带注解的【@Configuration
】类和带注解的【@Bean
】方法。
@Bean
注解用于指示一个方法,该方法负责【实例化、配置和初始化】一个由Spring IoC容器管理的新对象。 对于那些熟悉Spring <beans/>
XML配置的人来说,@Bean
注解扮演着与<bean/>
元素相同的角色。 你可以在任何Spring @Component
中使用@Bean
注解方法。 但是,它们最常与@Configuration
一起使用。
用@Configuration
注解的一个类表明它的主要目的是作为beanDifination的源,我们通常称之为【配置类】。 此外,【@Configuration
】类允许通过调用同一类中的其他【@Bean
】方法来【定义bean间的依赖关系】。 最简单的【@Configuration
】类如下所示:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
前面的’ AppConfig '类等价于下面的Spring <beans/>
XML:
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
🍀(2)使用 AnnotationConfigApplicationContext
实例化Spring容器
下面的章节记录了Spring 3.0中引入的【AnnotationConfigApplicationContext
】。 这个通用的【ApplicationContext
】实现不仅能够接受【@Configuration
】类作为输入,还能够接受普通的【@Component
】类和用JSR-330元数据注解的类。
当提供【@Configuration
】类作为输入时,【@Configuration
】类本身被注册为一个beanDifination,并且类中所有声明的【@Bean
】方法也被注册为beanDifination。
当提供【@Component
】和JSR-330相关的注解类时,它们被注册为beanDifination。
a、结构简洁
就像Spring XML文件在实例化【ClassPathXmlApplicationContext
】时被用作输入一样,当实例化【AnnotationConfigApplicationContext
】时,你可以使用【@Configuration
】类作为输入。 这允许Spring容器完全不使用xml,如下例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
正如前面提到的,【AnnotationConfigApplicationContext
】并不局限于只与【@Configuration
】类一起工作。 任何【@Component
】或JSR-330注解类都可以作为输入提供给构造函数,如下面的例子所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
前面的例子假设【MyServiceImpl
】、【Dependency1
】和【Dependency2
】使用Spring依赖注入注解,比如【@Autowired
】。
b、通过使用’ register(Class<?>…)'以编程方式构建容器
你可以使用一个【没有参数的构造函数】来实例化一个【AnnotationConfigApplicationContext
】,然后使用【register()
】方法来配置它。 当以编程方式构建一个“AnnotationConfigApplicationContext
”时,这种方法特别有用。 下面的例子展示了如何做到这一点:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
c、使用 scan(String…)
启用组件扫描
要启用组件扫描,你可以像下面这样注解你的 @Configuration
类:
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="com.ydlclass" / >
</beans>
同时,AnnotationConfigApplicationContext也暴露了【 scan(String…)
】方法来允许相同的组件扫描功能,如下例所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
请记住,【@Configuration
】类是带有【@Component
】元注解的一个注解,因此它们是组件扫描的候选对象。 在前面的例子中,假设【AppConfig
】在"com.acme
"中声明。 在’ refresh()
‘之后,它的所有’ @Bean
'方法都被处理并注册为容器中的beanDifination。
🍀(3) @Bean注解
【@Bean
】是一个方法级注解,与XML<bean/>
元素具有相同的能力。 注解支持<bean/>
提供的一些属性,例如:
- init-method
- destroy-method
- autowiring
- name
你可以在带有【@Configuration
】注解的类或带有【@Component
】注解的类中使用【@Bean
】注解。
a、声明一个 Bean
使用【@Bean
】对方法进行注解可以帮助我们申明一个bean。 您可以使用此方法在【ApplicationContext
】中注册一个beanDifination,该bean的类型会被指定为【方法的返回值类型】,而具体的返回值则是交由spring管理的bean实例。 默认情况下,bean名与方法名相同。 下面的例子显示了一个【 @Bean
】方法声明:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
上面的配置与下面的Spring XML完全相同:
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
注意: 你也可以使用接口(或基类)作为返回类型来声明你的@Bean
方法,如下面的例子所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
b、Bean的依赖关系
带注解的【@Bean
】方法可以有任意数量的参数,这些参数描述构建该bean所需的依赖关系。 例如,如果我们的【TransferService
】需要一个【AccountRepository
】,我们可以用一个方法参数来实现这个依赖,如下例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
c、接受生命周期回调
- 任何用【
@Bean
】注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250的’@PostConstruct
‘和’@PreDestroy
'注解。 - 也完全支持常规的Spring lifecycle回调。 如果一个bean实现了’
InitializingBean
‘、’DisposableBean
‘或’Lifecycle
',则容器会调用它们各自的方法。 - 标准的【
Aware
】接口也完全支持。
【@Bean注解
】支持指定任意的初始化和销毁回调方法,就像Spring XML在’ bean ‘元素上的’ init-method
‘和’ destroy-method
'属性一样,如下面的示例所示:
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
小知识: 对于上面例子中的’ BeanOne
‘,在构造过程中直接调用’ init()
'方法同样有效,如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
当您直接在代码中进行配置时,您可以对您的对象做任何您想做的事情,而不总是需要依赖于容器生命周期。
d、指定Bean范围
Spring包含了【@Scope
】注解,以便您可以指定bean的范围。
默认的作用域是’ singleton ‘,但是你可以用’ @Scope
'注解来覆盖它,如下面的例子所示:
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
e、定制Bean命名
默认情况下,配置类使用【@Bean
】方法的名称作为结果bean的名称。 但是,可以使用’ name '属性覆盖该功能,如下例所示:
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}
有时需要为单个bean提供多个名称,或者称为bean别名。【@Bean
】注解的’ name '属性为此接受String数组。 下面的例子展示了如何为一个bean设置多个别名:
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
f、Bean 描述
有时,提供bean的更详细的文本描述是很有帮助的。 当bean被公开(可能通过JMX)用于监视目的时,这可能特别有用。
要向【@Bean
】添加描述,可以使用【@Description
】注解,如下面的示例所示:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
🍀(4)@Configuration
【@Configuration
】是一个类级注解,指示一个对象是beanDifination的源。【@Configuration
】类通过【@Bean
】带注解的方法声明bean。 【在“@Configuration
”类上调用“@Bean
”方法也可以用来定义bean间的依赖关系】。
注入bean之间的依赖
当@Bean
方法在没有标注@Configuration
的类中声明时,它们被认为是在【lite】模式下处理的。 在【@Component
】中声明的Bean方法甚至在一个普通的类中声明的Bean方法都被认为是【lite】。在这样的场景中,【@Bean
】方法是一种通用工厂方法机制。
与@Configuration
不同,【lite】模式下 【@Bean
】方法不能【声明bean】间的【依赖关系】。 因此,这样的【@Bean
】方法不应该调用其他【@Bean
】下的方法。 每个这样的方法实际上只是特定bean引用的工厂方法,没有任何特殊的运行时语义。
在一般情况下,@Bean
方法要在【@Configuration
】类中声明,这种功能情况下,会使用【full】模式,因此交叉方法引用会被重定向到容器的生命周期管理。 这可以防止通过常规Java调用意外调用相同的Bean,这有助于减少在【lite】模式下操作时难以跟踪的微妙错误。
@Bean
和@Configuration
注解将在下面几节中深入讨论。 不过,我们首先介绍通过使用基于java的配置创建spring容器的各种方法。
当bean相互依赖时,表示这种依赖就像让一个bean方法调用另一个bean方法一样简单,如下面的示例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
// full模式可以直接调用方法,这个调用过程由容器管理,lite模式这就是普通方法调用,多次调用会产生多个实例。
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
在前面的例子中,【beanOne】通过构造函数注入接收对【beanTwo】的引用。
考虑下面的例子,它显示了一个带注解的@Bean
方法被调用两次:
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()
在【clientService1()
】和【clientService2()
】中分别被调用一次。 由于该方法创建了一个新的【ClientDaoImpl
】实例并返回它,所以通常期望有两个实例(每个服务一个)。 这肯定会有问题。在Spring中,实例化的bean默认有一个【单例】作用域,在调用父方法并创建新实例之前,首先检查容器中是否有缓存的(有作用域的)bean。
我们目前学习的描述候选组件的注解很多,但是仔细意思考,其实很简单:
我们自己的写代码通常使用以下注解来标识一个组件:
- @Component 组件的通用注解
- @Repository,持久层
- @Service,业务层
- @Controller,控制层
- @Configuration + @Bean
配置类通常是我们不能修改源代码,但是需要注入别人写的类。例如向容器注入一个德鲁伊数据源的bean,我们是绝对不能给这个类加个【@Component
】注解的。
🍀(5) 使用 @Import
注解
就像在Spring XML文件中使用<import/>
元素来实现模块化配置一样,@Import
注解允许从另一个配置类加载【@Bean
】定义,如下面的示例所示:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
现在,在实例化上下文时不需要同时指定ConfigA.class
和ConfigB.class
,只需要显式地提供【ConfigB】,如下面的示例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
这种方法简化了容器实例化,因为只需要处理一个类,而不是要求您在构造过程中记住潜在的大量【@Configuration
】类。
【小知识】: 我们一样可以给该注解传入一个实现了ImportSelector
接口的类,返回的字符串数组的Bean都会被加载到容器当中:
public class ConfigSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"com.ydlclass.A","com.ydlclass.B"};
}
}
🍀(6)结合Java和XML配置
Spring的【@Configuration
】类支持的目标并不是100%完全替代Spring XML,有些场景xml仍然是配置容器的理想方式。
我们有如下选择:
- (1)容器实例化在一个“以XML为中心”的方式使用,例如“
ClassPathXmlApplicationContext
”。 - (2)"以java编程的方式为中心”的方式,实例化它通过使用【
@ImportResource
】注解导入XML。
以xml为中心使用“@Configuration
”类
最好从XML引导Spring容器,并以一种特别的方式包含【@Configuration
】类。将【@Configuration
】类声明为普通的Spring <bean/>
元素。记住,【@Configuration
】类最终是容器中的beanDifination。
下面的例子展示了Java中一个普通的配置类:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
下面的例子显示了一个’ system-test-config.xml '文件的一部分:
<beans>
<!-- enable processing of annotations such as @Autowired and @Configuration -->
<context:annotation-config/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="com.acme.AppConfig"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
下面的示例显示了一个可能的’ jdbc '。 属性的文件:
user=root
password=root
url=jdbc:mysql://127.0.0.1:3306/ydlclass?characterEncoding=utf8&serverTimezone=Asia/Shanghai
driverName=com.mysql.cj.jdbc.Driver
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
因为【@Configuration
】是用【@Component
】注解的,所以被【@Configuration
】注解的类会自动被组件扫描。 使用与前面示例中描述的相同的场景,我们可以重新定义system-test-config.xml来利用组件扫描。
下面的示例显示了修改后的system-test-config.xml文件:
<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
使用@ImportResource
以类为中心使用XML
在【@Configuration
】类是配置容器的主要机制的应用程序中,可能仍然需要使用至少一些XML。 在这些场景中,您可以使用【@ImportResource
】注解,并只定义所需的XML。 这样做可以实现一种“以java为中心”的方法来配置容器,并将XML最小化。
下面的例子说明了这一点:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
properties-config.xml:
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties::
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
启动容器:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
8️⃣BeanFactory和FactoryBean
🍀(1)BeanFactory
BeanFactory
是一个接口,它是Spring中工厂的顶层规范,是SpringIoc容器的核心接口,它定义了getBean()
、containsBean()
等管理Bean的通用方法。Spring的容器都是它的具体实现如:
- DefaultListableBeanFactory
- XmlBeanFactory
- ApplicationContext
这些实现类又从不同的维度分别有不同的扩展。
它的源码如下:
public interface BeanFactory {
//对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,
//如果需要得到工厂本身,需要转义
String FACTORY_BEAN_PREFIX = "&";
//根据bean的名字,获取在IOC容器中得到bean实例
Object getBean(String name) throws BeansException;
//根据bean的名字和Class类型来得到bean实例,增加了类型安全验证机制。
<T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
//提供对bean的检索,看看是否在IOC容器有这个名字的bean
boolean containsBean(String name);
//根据bean名字得到bean实例,并同时判断这个bean是不是单例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
//得到bean实例的Class类型
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
//得到bean的别名,如果根据别名检索,那么其原名也会被检索出来
String[] getAliases(String name);
}
使用场景:
- 从Ioc容器中获取Bean(byName or byType)
- 检索Ioc容器中是否包含指定的Bean
- 判断Bean是否为单例
🍀(2)FactoryBean
首先它是一个Bean,但又不仅仅是一个Bean。它是一个能生产或修饰对象生成的工厂Bean,类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候生产一个对象,且不仅仅限于它自身,它能返回任何Bean的实例。一般用于创建第三方或复杂对象。
源码如下:
public interface FactoryBean<T> {
//从工厂中获取bean
@Nullable
T getObject() throws Exception;
//获取Bean工厂创建的对象的类型
@Nullable
Class<?> getObjectType();
//Bean工厂创建的对象是否是单例模式
default boolean isSingleton() {
return true;
}
}
从它定义的接口可以看出,FactoryBean
表现的是一个工厂的职责。 即一个Bean A如果实现了FactoryBean
接口,那么A就变成了一个工厂,根据A的名称获取到的实际上是工厂调用getObject()
返回的对象,而不是A本身,如果要获取工厂A自身的实例,那么需要在名称前面加上'&'
符号。
- getObject(‘name’):返回工厂中的实例
- getObject(‘&name’):返回工厂本身的实例
通常情况下,bean 无须自己实现工厂模式,Spring 容器担任了工厂的 角色;但少数情况下,容器中的 bean 本身就是工厂,作用是产生其他 bean 实例。由工厂 bean 产生的其他 bean 实例,不再由 Spring 容器产生,因此与普通 bean 的配置不同,不再需要提供 class 元素。
下边的例子我们使用FactoryBean注入一个bean
@Component
public class MyBean implements FactoryBean {
private String message;
public MyBean() {
this.message = "通过构造方法初始化实例";
}
@Override
public Object getObject() throws Exception {
// 这里并不一定要返回MyBean自身的实例,可以是其他任何对象的实例。
//如return new Student()...
return new MyBean("通过FactoryBean.getObject()创建实例");
}
@Override
public Class<?> getObjectType() {
return MyBean.class;
}
public String getMessage() {
return message;
}
}
MyBean
实现了FactoryBean
接口的两个方法,getObject()
是可以返回任何对象的实例的,这里测试就返回MyBean
自身实例,且返回前给message
字段赋值。
同时在构造方法中也为message
赋值。然后测试代码中先通过名称获取Bean实例,打印message
的内容,再通过&+名称
获取实例并打印message
内容。
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class FactoryBeanTest {
@Autowired
private ApplicationContext context;
@Test
public void test() {
MyBean myBean1 = (MyBean) context.getBean("myBean");
System.out.println("myBean1 = " + myBean1.getMessage());
MyBean myBean2 = (MyBean) context.getBean("&myBean");
System.out.println("myBean2 = " + myBean2.getMessage());
System.out.println("myBean1.equals(myBean2) = " + myBean1.equals(myBean2));
}
}
结果:
myBean1 = 通过FactoryBean.getObject()初始化实例
myBean2 = 通过构造方法初始化实例
myBean1.equals(myBean2) = false
使用场景
说了这么多,为什么要有FactoryBean
这个东西呢,有什么具体的作用吗? FactoryBean
在Spring中最为典型的一个应用就是用来创建AOP的代理对象。
我们知道AOP实际上是Spring在运行时创建了一个代理对象,也就是说这个对象,是我们在运行时创建的,而不是一开始就定义好的,这很符合工厂方法模式。更形象地说,AOP代理对象通过Java的反射机制,在运行时创建了一个代理对象,在代理对象的目标方法中根据业务要求织入了相应的方法。这个对象在Spring中就是——ProxyFactoryBean
。
所以,FactoryBean
为我们实例化Bean提供了一个更为灵活的方式,我们可以通过FactoryBean
创建出更为复杂的Bean实例。
🍀(3)区别
FactoryBean
本质上还是一个Bean,也归BeanFactory
管理,他是用来构建bean,特别是复杂的bean的。BeanFactory
是Spring容器的顶层接口,FactoryBean
是用来管理bean的。
9️⃣环境抽象
- 接口是一个抽象,集成在容器中,它模拟了应用程序环境的两个关键方面:【
profiles
】 and 【properties
】。 - 一个
profile
是一个【给定名字】的,在【逻辑上分了组】的beanDifination
配置,只有在给定的profile
是激活的情况下才向容器注册。 properties
在几乎所有的应用程序中都扮演着重要的角色,并且可能源自各种来源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、特定的【Properties
】对象、“Map
”对象,等等。与属性相关的“Environment
”对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从那里解析属性。
🍀(1)Profiles
Profiles在核心容器中提供了一种机制,允许在不同环境中注册不同的Bean。 “环境”这个词对不同的用户有不同的含义,
- 在开发中使用内存中的数据源,还是在生产中从JNDI中查找的数据源。
- 为客户A和客户B部署注册定制的bean实现。
考虑一个实际应用程序中的第一个用例,它需要一个“数据源”。 在测试环境中,配置可能类似如下:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
现在考虑如何将该应用程序部署到生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的JNDI目录中。 我们的’ dataSource
’ bean现在看起来如下所示:
@Bean
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
重点:问题是如何根据当前环境在使用这两种数据源之间进行切换?
当然,我们可以使用 @Profile
。
【@Profile
】注解允许您指出,当一个或多个bean在哪一种Profile
被激活时被注入。 使用前面的例子,我们可以将dataSource
配置重写如下:
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Profile
也可以在方法级别声明,只包含一个配置类的一个特定bean(例如,对于一个特定bean的替代变体),如下面的示例所示:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
🍀(2)XML Bean 定义环境
XML对应的是<beans>
元素的’ profile
'属性。 前面的示例配置可以在两个XML文件中重写,如下所示:
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
也可以避免在同一个文件中分割和嵌套<beans/>
元素,如下例所示:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
🍀(3)激活一个环境
现在我们已经更新了配置,我们仍然需要指示Spring哪个配置文件是活动的。 如果我们现在启动我们的样例应用程序,我们会看到抛出一个NoSuchBeanDefinitionException
,因为容器无法找到名为dataSource
的Spring bean。
激活配置文件有几种方式,但最直接的方式是通过【ApplicationContext】可用的【Environment】API以编程方式执行。 下面的例子展示了如何做到这一点:
@Test
public void testProfile(){
// 创建容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 激活环境
context.getEnvironment().setActiveProfiles("development");
// 扫包
context.scan("com.ydlclass.datasource");
// 刷新
context.refresh();
// 使用
DataSource bean = context.getBean(DataSource.class);
logger.info("{}",bean);
}
此外,你还可以通过spring.profiles来声明性地激活环境【active
】属性,它可以通过系统环境变量、JVM系统属性、servlet上下文参数在’ web.xml '中指定。
请注意,配置文件不是一个“非此即彼”的命题。 您可以一次激活多个配置文件。 通过编程方式,您可以向’ setActiveProfiles()
‘方法提供多个配置文件名,该方法接受’ String…
'可变参数。 下面的示例激活多个配置文件:
加入启动参数:
-Dspring.profiles.active="profile1,profile2"
编程的方式:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
🍀(4)porperties
Spring的【环境抽象】提供了对【属性】的搜索操作。 考虑以下例子:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
在前面的代码片段中,我们看到了查询Spring是否为当前环境定义了【my-property
】属性的方法。 为了回答这个问题,“Environment
”对象对一组【PropertySource
】对象执行搜索。 “PropertySource
”是对任何【键值对源】的一个简单抽象, spring的【StandardEnvironment
】配置了两个PropertySource
对象——一个代表JVM系统属性的集合(“System.getProperties()
”)和一个代表系统环境变量的设置(System.getenv()
”)。
Map<String,String> getenv = System.getenv();
Properties properties = System.getProperties();
具体地说,当你使用【StandardEnvironment
】时,如果【my-property
】系统属性或【my-property
】环境变量在运行时存在,对env.containsProperty("my-property")
的调用将返回true
。
最重要的是,整个机制都是可配置的。 也许您有一个自定义的属性源,希望将其集成到此搜索中。 为此,我们可以实例化自己的【PropertySource
】,并将它添加到当前’ Environment
‘的’ propertyssources
'集合中。 下面的示例显示了如何这样做:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
使用@PropertySource
【@PropertySource
】注解提供了一种方便的声明性机制,用于向Spring的【Environment
】中添加【 PropertySource
】。
给定一个名为app的文件。 下面的【@Configuration
】类使用了【@PropertySource
】,从而调用“testBean.getName()
”返回“myTestBean
”:
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
🔟事件机制
为了以更面向框架的风格增强【BeanFactory
】功能,ApplicationContext
还提供了以下功能:
- 通过
MessageSource
接口访问i18n风格的消息,实现国际化。 - 通过
ResourceLoader
接口访问资源,例如url和文件。 - 事件发布,即通过使用’
ApplicationEventPublisher
‘接口发布实现’ApplicationListener
’接口的bean。 - 通过“
HierarchicalBeanFactory
”接口,加载多个(分层的)上下文,让每个上下文都集中在一个特定的层上,比如应用程序的web层。
🍀(1)自定义事件
ApplicationContext
中的事件处理是通过【ApplicationEvent
】类和【ApplicationListener
】接口提供的。 如果将实现“ApplicationListener
”接口的bean部署到上下文中,那么每次将【ApplicationEvent
】发布到【ApplicationContext
】时,都会通知该bean。 本质上,这是标准的Observer设计模式。
从spring4.2开始,事件基础设施得到了显著的改进,并提供了一个【基于注解的事件模型】以及发布任意事件的能力 。
您可以使用spring创建和发布自己的自定义事件。 下面的例子展示了一个简单的类,它扩展了Spring的【ApplicationEvent
】基类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
要发布自定义的【ApplicationEvent
】,需要调用【ApplicationEventPublisher
】上的【publishEvent()
】方法。 通常,这是通过创建一个实现’ ApplicationEventPublisherAware
'的类并将其注册为Spring bean来实现的。 下面的例子展示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
在配置时,Spring容器检测到【EmailService
】实现了【 ApplicationEventPublisherAware
】并自动调用【setApplicationEventPublisher()
】。 实际上,传入的参数是Spring容器本身。 你通过它的【ApplicationEventPublisher
】接口与应用上下文交互。
要接收自定义的【 ApplicationEvent
】,您可以创建一个类来实现【ApplicationListener
】并将其注册为Spring bean。 下面的例子展示了这样一个类:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
将来容器只要发布这个事件,这个监听者就可以感知。
基于注解的事件监听器
您可以使用【@EventListener
】注解在托管bean的任何方法上注册一个事件侦听器。 【BlockedListNotifier
】可以重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
方法签名再次声明它侦听的事件类型,但这一次使用了灵活的名称,而没有实现特定的侦听器接口。 只要实际事件类型在其实现层次结构中解析泛型参数,就可以通过泛型缩小事件类型。
如果您的方法应该侦听多个事件,或者您想在不带参数的情况下定义它,也可以在注解本身上指定事件类型。 下面的例子展示了如何做到这一点:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
🍀(2)Spring提供的标准事件
事件 | 说明 |
---|---|
ContextRefreshedEvent | 在“ApplicationContext ”被初始化或刷新时发布(例如,通过使用“ConfigurableApplicationContext ”接口上的“refresh() ”方法)。 这里,“初始化”意味着加载了所有bean,检测并激活了后处理器bean,预实例化了单例,并且“ApplicationContext ”对象已经准备好使用了。 只要上下文还没有被关闭,一个刷新可以被触发多次,只要选择的’ ApplicationContext ‘实际上支持这种“热”刷新。 例如,’ XmlWebApplicationContext ‘支持热刷新,但’ GenericApplicationContext '不支持。 |
ContextStartedEvent | 在’ ConfigurableApplicationContext ‘接口上使用’ start() ‘方法启动’ ApplicationContext '时发布。 在这里,“started ”意味着所有的“生命周期”bean都接收一个显式的开始信号。 通常,此信号用于在显式停止之后重新启动bean,但它也可用于启动尚未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。 |
ContextStoppedEvent | 在’ ConfigurableApplicationContext ‘接口上使用’ stop() ‘方法停止’ ApplicationContext ‘时发布。 这里,“stopped ”意味着所有“Lifecycle ”bean都接收一个显式的停止信号。 一个停止的上下文可以通过’ start() '调用重新启动。 |
ContextClosedEvent | 在’ ConfigurableApplicationContext ‘接口上的’ close() ‘方法或通过JVM关闭钩子关闭’ ApplicationContext '时发布。 这里,“closed ”意味着将销毁所有单例bean。 一旦关闭上下文,它将到达其生命周期的结束,不能刷新或重新启动。 |
RequestHandledEvent | 一个特定于web的事件,告诉所有bean一个HTTP请求已经得到了服务。 此事件在请求完成后发布。 这个事件只适用于使用Spring ’ DispatcherServlet '的web应用程序。 |
ServletRequestHandledEvent | 'requestthandledevent ’的子类,用于添加特定于servlet的上下文信息。 |
这些标准事件会在特定的时间发布,我们可以监听这些事件,并在事件发布时做我们想做的工作。
后记
↪️本文下接:XXXX
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~