使用场景
说了这么多,为什么要有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提供的标准事件
这些标准事件会在特定的时间发布,我们可以监听这些事件,并在事件发布时做我们想做的工作。