Spring容器就像一台构造精妙的机器,我们通过配置文件向机器传达控制信息,机器就能够按照设定的模式工作。如果将Spring容器比作一辆车,那么可以将BeanFactory看成汽车的发动机,而ApplicationContext则是一辆完整的汽车,它不但包括发动机,还包括离合器、变速器及底盘、车身、电气设备等其他组件。在ApplicationContext内,各个组件按部就班、有条不絮地完成汽车的各项功能。
一、 内部工作机制
Spring的AbstractApplicationContext是ApplicationContext的抽象实现类,该抽象类的refresh()方法定义了Spring容器在加载配置文件后的各项处理过程,这些处理过程清新地刻画了Spring容器启动时所执行地各项操作:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
//初始化BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
//调用工厂后处理器
invokeBeanFactoryPostProcessors(beanFactory);
//注册Bean后处理器
registerBeanPostProcessors(beanFactory);
//初始化消息来源
initMessageSource();
//初始化应用上下文时间广播器
initApplicationEventMulticaster();
//初始化其他特殊的Bean:由具体子类实现
onRefresh();
//注册事件监听器
registerListeners();
//初始化所有单实例Bean,使用懒初始化模式的Bean除外
finishBeanFactoryInitialization(beanFactory);
//完成刷新并发布容器的发布事件
finishRefresh();
}
catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
}
}
1.
初始化BeanFactory:根据配置文件实例化BeanFactory,在obtainFreshBeanFactory()方法中,首先调用refreshBeanFactory()方法刷新
BeanFactory,然后调用getBeanFactory()方法获取BeanFactory,这两个方法都是由具体子类实现。在这一步里,Spring将配置文件的信息装入
容器的Bean定义注册表(BeanDefinitionRegistry)中,但是此时Bean还没有初始化;
2.调用工厂后处理器:根据反射机制从BeanDefinitionRegistry中找出所有实现了BeanFactoryPostProcessor接口的Bean,并调用其postPr
ocessBeanFactory()接口方法;
3.注册Bean后处理器:根据反射机制从BeanDefintionRegistry中找出所有实现了BeanPostProcessor接口的Bean,并将它们注册到容器
Bean后处理的注册表;
4.初始化消息源:初始化容器的国际化消息资源
5.初始化应用上下文事件广播器
6.初始化其他特殊的Bean:这是一个钩子方法。子类可以借助这个方法执行一些特殊的操作,如AbstractRefreshableWebApplicationConte
xt就使用该方法执行初始化ThemeSource的操作
7.注册事件监听器
8.初始化所有单实例的Bean,使用懒加载模式的Bean初始化:初始化Bean后,将它们放入Spring容器的缓存池中
9.初始化上下文刷新事件:创建上下文刷新事件,事件广播器负责将这些事件广播到每个注册的事件监听器中。
二、使用外部属性文件
在进行数据源或邮件服务器等资源的配置时,用户可以直接在Spring配置文件中配置用户名/密码、链接地址等信息。并在Spri
ng配置文件中通过形如$(user)、$(passwood)等占位符引用属性文件中的属性。
Spring为我们提供了一个PropertyPlaceholderConfigurer,它能够使Bean在配置时引用外部属性文件。PropertyPlacehol
derConfigurer实现了BeanFactoryPostProcessorBean接口,因此它也是一个Bean工厂后处理器。
<!-- 引入jdbc.properties作为属性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
p:location="classpath:jdbc.properties"
p:fileEncoding="utf-8"/>
<!-- 通过属性名引用属性值 -->
<bean class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="$(driverClassName)"
p:url="$(url)"
p:username="$(userName)"
p:password="$(password)"/>
或者:
<!-- 使用context命名空间定义属性文件,file-encoding的值为字符串Bean -->
<context:property-placeholder location="classpath:jdbc.properties"
file-encoding="utf8"/>
<bean id="utf8" class="java.lang.String">
<constructor-arg value="utf-8"></constructor-arg>
</bean>
使用加密的属性文件
DES加密解密工具类
信息的加密可分为对称和非对称两种方式,前者表示加密后的信息可以解密成原值,而后者则不能根据加密后的信息还原为原值。MD5属于非对称加密,而DES属于对称加密。我们先使用DES对属性进行加密,再读取属性时,再用DES进行解密。下面是DES加密解密的工具类代码:
package com.baobao.jdbcsoutce.util;
import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
/*
* sun.misc是sun公司内部包,Eclipse无法直接使用解决办法:
* 项目右键Build Path,在JRE中找Access rules,点击Edit
* 在Resolution下拉框中选择Accessible,Rule Pattern输入**
*/
public class DESUtils {
//指定DES加密解密所用的秘钥
private static Key key;
private static String KEY_STR = "myKey";
static{
try{
//KeyGenerator秘钥生成器类,根据此类getInstance()方法构造此对象
KeyGenerator generator = KeyGenerator.getInstance("DES");
//提供随机源初始化算法
generator.init(new SecureRandom(KEY_STR.getBytes()));
//generateKey()生成一个秘钥
key = generator.generateKey();
generator = null;
}catch(Exception e){
throw new RuntimeException(e);
}
}
//对字符串进行DES加密,返回BASE64编码的加密字符串
public static String getEncryptString(String str){
BASE64Encoder base64en = new BASE64Encoder();
try{
byte[]strBytes = str.getBytes("UTF-8");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[]encryptStrBytes = cipher.doFinal(strBytes);
return base64en.encode(encryptStrBytes);
}catch(Exception e){
throw new RuntimeException(e);
}
}
//对BASE64编码的加密字符进行解密,返回解密后的字符串
public static String getDecryptString(String str){
BASE64Decoder base64De = new BASE64Decoder();
try{
byte[]strBytes = base64De.decodeBuffer(str);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[]decryptStrBytes = cipher.doFinal(strBytes);
return new String(decryptStrBytes,"UTF-8");
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
使用密文版的属性文件
由于PropertyPlaceholderConfigurer本身不支持密码本的属性文件,不过我们扩展该类,覆盖String convertProperty(String propertyName, String propertyValue)方法,对userName及password的熟悉经值进行解密转换。扩展类代码如下:
package com.baobao.placeholder;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import com.baobao.jdbcsoutce.util.DESUtils;
//继承PropertyPlaceholderConfigurer定义支持密文版属性的属性配置器
public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
private String[]encryptPropNames = {"userName","password"};
//对特定属性的属性值进行转换
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if(isEncryptProp(propertyName)){
String decryptValue = DESUtils.getDecryptString(propertyValue);
System.out.println(decryptValue);
return decryptValue;
}else{
return propertyValue;
}
}
//判断是否是需要进行解密的属性
private boolean isEncryptProp(String propertyName) {
for(String encryptPropName:encryptPropNames){
if(encryptPropName.equals(propertyName)){
return true;
}
}
return false;
}
}
最后使用传统的方式引入加密的属性文件
<!-- 引入jdbc.properties作为属性文件 -->
<bean class="com.baobao.placeholder.EncryptPropertyPlaceholderConfigurer"
p:location="classpath:jdbc.properties"
p:fileEncoding="utf-8"/>
<!-- 通过属性名引用属性值 -->
<bean class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="$(driverClassName)"
p:url="$(url)"
p:username="$(userName)"
p:password="$(password)"/>
属性文件的属性之间互相引用:
dbName=review_ssm
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/${dbName}
userName=WnplV/ietfQ=
password=QAHlVoUc49w=
访问Bean的属性值
采用#{beanName.propName}的表达式引用该Bean的属性值
三、容器事件
Spring的ApplicationContext能够发布事件并且允许注册相应的事件监听器,因此它拥有一套完善的事件发布和监听机制。Java通过java.util.EventObject类和java.util.EventListener接口描述事件和监听器,某个组件或框架建立自己的事件发布和监听机制,一般都通过扩展它们进行定义。在事件体系中,除了事件和监听器意外,还有另外三个重要的的概念。
事件源:事件的产生者,任何一个EventObject都必须拥有一个事件源;
事件监听器注册表:组件或框架的事件监听器不可能漂在水里悬在空中,而必须有所依存。也就是说组件或框架必须提供一个地方保存事件监听器,这便是事件监听器注册表。一个事件监听器注册到组件或框架中,其实就是存在在监听器注册表里,当组件和框架中的事件源产生事件时就会将事件通知这些位于注册表中的监听器;
事件广播器:它是事件和时间监听器沟通桥梁吗,负责把时间通知给事件监听器。
事件类结构图
ApplicationEvent的唯一构造函数是ApplicationEvent(Objectsource),通过source指定事件源,它的连个子类分别是:
① ApplicationContextEvent:容器事件,拥有4个子类分别表示容器启动、刷新、停止及关闭的事件;
② RequestHandleEvent:这是一个与Web应用相关的事件,当一个HTTP请求被处理后,产生该事件。只有在web.xml种定义了DispatcherServlet时才会产生该事件。
拥有两个子类,分别代表Servlet和Portlet的请求事件。
事件监听器接口
Spring只有一个事件监听器接口ApplicationListener,如图:
Applicationlistener接口只定义了一个方法:onApplicationEvent(Eevent),该方法接收ApplicationEvent事件对象,在该方法中编写事件的响应处理逻辑。而SmartApplicationListener接口是Spring 3.0新增的,它定义了两个方法:
① Boolean supportsEventType(Class<?Extends ApplicationEvent>eventType):指定监听器支持哪种类型的容器事件,即它只会对该类型的事件作出响应;
② Boolean supportsSourceType(Class<?>sourceType):该方法指定监听器仅对何种事件源对象作出响应。
事件广播器
当发生容器事件时,容器主控程序将调用事件广播器将事件通知给注册表中的事件监听器,监听器分别对事件进行响应。Spring为事件广播器定义了接口,并提供了实现类如下图:
Spring在ApplicationContext接口的抽象实现类AbstractApplicationContext中完成了事件体系的搭建。AbstactApplicationContext拥有一个applicationEventMulticaster成员变量,applicationEventMulticaster提供了容器监听器的注册表。AbstractApplicationContext在refresh()这个容器启动方法中通过以下三个步骤搭建了事件的基础设施。下面是有关代码:
在⑤,Spring初始化事件的广播器,可以在配置文件中为容器定义一个自定义的事件广播器,只要实现ApplicationEventMulticaster即可,Spring会通过反射机制将其注册容器的事件广播器。如果没有找到配置的外部事件广播器,则Spring自动使用SimpleApplicationEventMulticaster作为事件广播器
在⑦,Spring根据反射机制,从BeanDefinitionRegistry中找出所有实现ApplicationListener的Bean,将它们注册为容器的事件监听器,即将其添加到事件广播器所提供的事件监听器注册表中
在⑨,容器启动完成,调用事件发布接口向容器中所有的监听器发布事件
一个实例
模拟的邮件发送器MailSender,它向目的地发送邮件时,将产生一个MailSendEvent的事件,容器中注册了监听该事件的监听器MailSendListener。下面是代码:
①事件类:
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ApplicationContextEvent;
//事件类
public class MailSendEvent extends ApplicationContextEvent {
//发送的目的地
private String to;
public MailSendEvent(ApplicationContext source,String to) {
super(source);
this.to = to;
}
public String getTo() {
return to;
}
}
②监听器实现类:
import org.springframework.context.ApplicationListener;
public class MailSendListener implements ApplicationListener<MailSendEvent> {
//对MailSendEvent事件进行处理
public void onApplicationEvent(MailSendEvent event) {
MailSendEvent mes = (MailSendEvent)event;
System.out.println("MailSendListener:向"+mes.getTo()+"发送完一封邮件");
}
}
③发送者要拥有发布事件的能力,必须实现ApplicationContextAware接口:
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class MailSender implements ApplicationContextAware {
private ApplicationContext ctx;
//ApplicationContextAwre的接口方法,以便容器启动时注入容器实例
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = ctx;
}
public void sendMail(String to){
System.out.println("MailSender:模拟发送邮件...");
MailSendEvent mse = new MailSendEvent(this.ctx,to);
//向容器中的所有事件监听器发送事件
ctx.publishEvent(mse);
}
}
Spring配置文件中注册Bean
<bean class="com.baobao.jdbcsoutce.event.MailSendListener"/>
<bean id="mailSender" class="com.baobao.jdbcsoutce.event.MailSender"/>
最后在测试代码中启动容器并调用发送者:
@Test
public void test() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
MailSender mailSender = (MailSender) ctx.getBean("mailSender");
mailSender.sendMail("aaa@bbb.com");
}
运行完毕,系统输出:
MailSender:模拟发送邮件...
MailSendListener:向aaa@bbb.com发送完一封邮件