Spring容器是一部设计精妙的机器,其优异的外在表现是通过精细的内部设计实现的。本篇将对Spring容器进行解构,从内部探究Spring容器的体系结构和运行流程。
1、Spring容器技术内幕
1.1 内部工作机制
下图描述了Spring容器从加载配置文件到创建出一个完整Bean的作业流程以及参与的角色:
- ResourceLoader从存储介质中接收Spring配置文件,并使用
Resource
表示这个配置文件的资源; - BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个
<bean>
解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistery中; - 容器扫描BeanDefinitionRegistery中的BeanDefinition,使用Java的反射机制自动识别出Bean工厂后处理器(实现BeanFactoryPostProcessor接口)的Bean,然后调用这些Bean工厂后处理器对BeanDefinitionRegistery中的BeanDefinition进行加工处理。主要完成以下两项工作:
- 对使用到占位符的
<bean>
元素标签进行解析,得到最终的配置值,这意味对一些半成品的BeanDefinition对象进行加工处理并得到成品的BeanDefinition对象; - 对BeanDefinitionRegistery中的BeanDefinition进行扫描,通过Java反射机制找出所有属性编辑器的Bean(实现java.beans.PropertyEditor接口的Bean) ,并自动将它们注册到Spring容器的属性编辑器注册表中(PropertyEditorRegistry);
- Spring容器从BeanDefinitionRegistery中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;
- 在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作;
- 利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。
Spring组件按其所承担的角色可以划分为两类:
- 物料组件: Resource、BeanDefinition、PropertyEditor以及最终的Bean等,它们是加工流程中被加工、被消费的组件,就像流水线上被加工的物料;
- 加工设备组件: Resourceloader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy以及BeanWrapper等组件像是流水线上不同环节的加工设备,对物料组件尽心国家公处理。
1.2 BeanDefinition
org.springframework.beans.factory.config.BeanDefinition
是配置文件<bean>
元素标签的在容器中的内在表示。
RootBeanDefinition是最常用的实现类,它对应一般性的<bean>
元素标签。如果有父子关系,父<bean>
用RootBeanDefinition表示,子<bean>
用ChildBeanDefinition表示,若没有父子关系,<bean>
就使用RootBeanDefinition表示。
一般情况下,BeanDefinition只在容器启动时加载并解析,除非容器刷新或重启,这些信息不会发生变化。
1.3 InstantiationStrategy
org.springframework.beans.factory.support.InstantiationStrategy
负责根据BeanDefinition对象创建一个Bean实例。之所以将实例化Bean的工作通过一个策略接口进行描述,是为了方便可以采用不同的实例化策略,以满足不同的应用需求。
SimpleInstantiationStrategy策略利用Bean实现类的默认构造函数、带参构造函数或工厂方法创建Bean的实例。CglibSubclassingInstantiationStrategy利用CGLib类库为Bean动态生成子类,在子类中生成方法注入逻辑,然后用这个子类创建Bean实例。
InstantiationStrategy
仅负责实例化Bean的操作,它并不会参与Bean属性的设置工作。属性填充的工作将由下面的BeanWrapper来完成。
1.4 BeanWrapper
org.springframework.beans.BeanWrapper
是Spring容器中重要的组件类。BeanWrapper相当于一个代理器,Spring通过BeanWrapper完成Bean属性的填充工作。
PropertyAccessor接口定义了各种访问Bean属性的方法,而PropertyEditorRegistry是属性编辑器的注册表。所以BeanWrapper实现类BeanWrapperImpl具有了三重身份:
- Bean包裹器
- 属性访问器
- 属性编辑器注册表
2、属性编辑器
任何实现java.beans.PropertyEditor
接口的类都是属性编辑器。属性编辑器的主要功能就是将外部的设置值,转换为JVM内部的对应类型,所以属性编辑器其实就是一个类型转换器。
2.1 JavaBean的编辑器
JavaBean规范通过java.beans.PropertyEditor
定义了设置JavaBean属性的方法,通过BeanInfo描述了JavaBean哪些属性是可定制的,此外还描述了可定制属性与PropertyEditor的对应关系。
BeanInfo与JavaBean之间的对应关系,通过两者之间规范的命名确立:对应JavaBean的BeanInfo采用如下的命名规范:<Bean>BeanInfo
。如ChartBean
对应的BeanInfo为ChartBeanBeanInfo
。
JavaBean规范还提供了一个管理默认属性编辑器的管理器:PropertyEditorManager,该管理器内保存着一些常见类型的属性编辑器,如果某个JavaBean的常见类型属性没有通过BeanInfo显示指定属性编辑器,IDE将自动使用PropertyEditorManager中注册的对应默认属性编辑器。
PropertyEditor是属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的转换接口方法。PropertyEditor主要的接口方法说明如下:
- Object getValues():返回属性的当前值。基本类型被封装成对应的封装类实例;
- void setValue(Object newValue):设置属性的值,基本类型以封装类传入;
- String getAsText():将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化方式显示。缺省返回null,表示该属性不能以字符串表示;
- void setAsText(String text):用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入;
- String[] getTags():返回表示有效属性值的字符串数组,以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示该属性没有匹配的字符值有限集合。
- String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值。
BeanInfo主要描述了哪些属性可以编辑以及对应的属性编辑器,每一个属性对应一个属性描述器PropertyDescriptor。BeanInfo接口最重要的方法就是:PropertyDescriptor[] getPropertyDescriptors(),该方法返回JavaBean的属性描述器数组。
2.2 Spring默认属性编辑器
Spring环境下的属性编辑器功能非常单一,仅需要将配置文件中字面值转换为属性类型的对象即可,并不需要提供UI界面,Spring为常见的属性类型提供了默认的属性编辑器。如下表所示:
类别 | 说明 |
基础数据类型 | 分为几个小类: 1) 基本数据类型,如:boolean、byte、short、int等; 2) 基本数据类型封装类:如:Long、Character、Integer等; 3) 两个基本数据类型的数组,char[]和byte[]; 4) 大数类,BigDecimal和BigInteger |
集合类 | 为5种类型的集合类Collection、Set、SortedSet、List和SortedMap提供了编辑器 |
资源类 | 用于访问外部资源的8个常见类Class、Class[]、File、 InputSteam、Locale、Properties、Resource[]和URL。 |
这些默认的属性编辑器解决常见属性类型的注册问题,如果用户的应用包括一些特殊类型的属性,且希望在配置文件中以字面值提供配置值,那么就需要编写自定义属性编辑器并注册到Spring容器中。(在Spring环境下自定义属性编辑器仅需要覆盖PropertyEditorSupport的setAsText()方法就可以了。)下面看一代码片段:我们现在希望在配置Boss时,不通过引用Bean的方式注入Boss的car属性,而希望直接通过字符串字面值提供配置。
// Car类
public class Car {
private int maxSpeed;
public String brand;
pirvate double price;
// 省略get/setter方法
}
// Boss类
public class Boss {
private String name;
private Car car = new Car();
// 省略get/setter
}
// Car自定义属性编辑器
import java.beans.PropertyEditorSupport
public class CustomCarEditor extends PropertyEditorSupport {
// 1.将字面值转换为属性类型对象
public void setAsText(String text) {
if(text == null || text.indexOf(",") == 1) {
throw new IllegalArgumentException("设置的字符串格式不正确");
}
String[] infos = text.split(",");
Car car = new Car();
car.setBrand(infos[0]);
car.setMaxSpeed(Integer.parseInt(infos[1]));
car.setPrice(Double.parseDouble(infos[2]));
// 2.调用父类的setValue()方法设置转换后的属性对象
setValue(car);
}
}
注册自定义属性编辑器:如果使用BeanFactory,用户需要手工调用registerCustomEditor(Class requiredType,PropertyEditor propertyEditor)方法注册自定义属性编辑器;如果使用ApplicationContext,则只需要在配置文件通过CustomEditorConfigurer注册即可。CustomEditorConfigurer实现BeanFactoryPostProcessor接口,因此是一个Bean工厂后处理器。
<!-- 1.配置自动注册属性编辑器的CustomEditorConfigurer -->
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<!-- 2.1 属性编辑器对应的属性类型-->
<entry key="com.hhxs.editor.Car">
<!-- 2.2 对应的属性编辑器Bean-->
<bean class="com.hhxs.editor。CustomCarEditor"/>
</entry>
</map>
</property>
</bean>
<bean>
<property name="name" value=“John”/>
<!-- 3.该属性将使用2处的属性编辑器完成属性填充操作 -->
<property name="car" value="红旗CA72,200,20000.00"/>
</bean>
提示: 按照JavaBeans的规范,JavaBeans的基础设施会在JavaBean相同类包下查找是否存在<JavaBean>Editor
的类,如果存在,自动使用<JavaBean>Editor
作为该JavaBean的PropertyEditor。Spring也支持这个规范,这样就无需显示在CustomEditorConfigurer中注册了。
3、使用外部属性文件
在进行数据源以及文件服务器等资源配置时,我们可以直接在Spring配置文件中配置,但是这样会存在一个问题就是一旦相应的资源环境变了,我们就要去更改配置文件,这会增加我们的部署复杂度,增加维护工作量。还有一种更好的方式,是将这些经常变动的的配置信息独立到一个外部属性文件中,并在Spring配置文件中通过形如user、 u s e r 、 {password}等占位符引用属性文件中的属性项。
下面看一个配置数据源的例子:
// jdbc.properties文件
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
userName=root
password=1234
// Spring配置文件代码片段
<!-- 1.引入jdbc.properties属性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
p:location="classpath:com/hhxs/placeholder/jdbc.properties"
p:fileEncoding="utf-8"/>
<!-- 2.通过属性名引用属性值 -->
<bean id="dataSource" class=”org.apache。commons.dbcp.BasicDataSource”
destroy-method="close"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${userName}"
p:password="${password}" />
PropertyPlaceholderConfigurer其他属性
- locations:如果只有一个属性文件,直接使用location属性指定就可以了,如果是多个属性文件,则可以通过locations属性进行设置,可以像配置List一样配置locations属性。
<property name="locations">
<list>
<value>jdbc1.properties/value>
<value>jdbc2.properties </value>
<value>jdbc3.properties</value>
</list>
</property>
- fileEncoding: 属性文件的编码格式,Spring使用操作系统默认编码读取属性文件,如果属性文件采用了特殊编码,需要通过该属性显示指定。
- order:如果配置文件中定义了多个PropertyPlaceHolderConfigurer,则通过该属性指定优先顺序。
- placeholderPrefix:在上面的例子中,我们通过属性名引用属性文件中的属性项,其中” 属 性 名 引 用 属 性 文 件 中 的 属 性 项 , 其 中 ” {“为默认的占位符前缀,可以根据需要改为其他的前缀符。
- placeholderSuffix:占位符后缀,默认为”}”。
使用<context:property-placeholder>
引用属性文件
可以使用context命名空间定义属性文件,例如:
<context:property-placeholder location="classpath:com/hhxs/bbt/placeholder/jdbc.properties"/>
基于注解及基于Java类配置中引用属性
基于注解配置的Bean可以通过@Value的注解为Bean的成员变量或方法入参自动注入容器已有的属性。如下所示:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyDataSource {
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${userName}")
private String userName;
@Value("${password}")
private String password;
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
由于标注@Configuration的类本身相当于标注@Component,所以在标注@Configuration类中引用属性的方式和基于注解配置的引用方式是完全一样的。
5、容器事件
Spring的ApplicationContext能够发布事件并且允许注册相应的事件监听器。Java中通过java.util.EventObject
类和java.util.EventListener
接口描述事件和监听器,某个组件或框架要建立自己的事件发布和监听机制,一般都通过扩展它们进行定义。在事件体系中,除了事件和监听器以外,还有另外三个重要的概念:
- 事件源:事件的生产者,任何一个EventObject都必须拥有一个事件源;
- 事件监听注册表:一个事件监听器注册到组件或框架中,其实就是保存在监听器注册表里,当组件和框架中的事件源产生事件时就会将事件通知这些位于注册表中的监听器;
- 事件广播器:负责把事件通知给事件监听器。
图片 事件体系角色
5.1 Spring事件类结构
事件类
ApplicationEvent的唯一构造函数是ApplicationEvent(Object source)
,通过Source指定事件源,它的两个子类分别是:
- ApplicationContextEvent:容器事件,拥有4个子类分别表示容器启动、刷新、停止及关闭的时间;
- RequestHandleEvent:这是一个与Web应用相关的事件,当一个HTTP请求被处理后,产生该事件。只有在web.xml中定义了DispatcherServlet时才会产生该事件。
事件监听器接口
ApplicationListener接口之定义了一个方法:onApplicationEvent(E event)
,该方法接受ApplicationEvent事件对象,在该方法中编写事件的响应处理逻辑。SmartApplicationListener接口是Spring3.0新增的,它定义了两个方法:
- boolean supportsEventType(Class
5.2 解构Spring时间体系的具体实现
Spring在ApplicationContext接口的抽象实现类AbstractApplicationContext中完成了事件体系的搭建。AbstractApplicationContext拥有一个applicationEventMulticaster成员变量,applicationEventMulticaster提供了容器监听器的注册表。AbstractApplicationContext在refresh()
这个容器启动方法中通过以下三个步骤搭建了事件的基础设施,代码片段如下:
...
// 1 初始化应用上下文事件广播器
initApplicationEventMulticaster();
// 2 注册事件监听器
registerListeners();
// 3 完成刷新并发布容器刷新事件
finishRefresh();
...
1
处,Spring初始化事件的广播器。用户可以在配置文件中为容器定义一个自定义的事件广播器,Spring会通过反射的机制将其注册成容器的事件广播器。如果没有找到配置的外部事件广播器,Spring自动使用SimpleApplicationEventMulticaster作为事件广播器。2
处,Spring将根据反射机制,从BeanDefinitionRegistry中找出所有实现ApplicationListener的Bean,将它们注册为容器的事件监听器。3
处,容器启动完成,调用事件发布接口向容器中所有的监听器发布事件。
下面看一个例子,一个模拟的邮件发送器。
// MailSendEvent类
package com.hhxs.bbt.event;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class MailSender implements ApplicationContextAware {
private ApplicationContext ctx;
@Override
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);
}
}
// MailSendListener类
package com.hhxs.bbt.event;
import org.springframework.context.ApplicationListener;
public class MailSendListener implements ApplicationListener<MailSendEvent> {
@Override
public void onApplicationEvent(MailSendEvent event) {
MailSendEvent mse = (MailSendEvent) event;
System.out.println("MailSendListener:向" + mse.getTo() + "发送完一封邮件");
}
}
// MailSender类
package com.hhxs.bbt.event;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class MailSender implements ApplicationContextAware {
private ApplicationContext ctx;
@Override
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配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="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-3.1.xsd">
<bean class="com.hhxs.bbt.event.MailSendListener"/>
<bean id="mailSender" class="com.hhxs.bbt.event.MailSender"/>
</beans>
// 测试类ApplicatonEventTest
package com.hhxs.bbt.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ApplicatonEventTest {
public static void main(String[] args) {
String resourceFile = "com/hhxs/bbt/event/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(resourceFile);
MailSender mailSender = ctx.getBean(MailSender.class);
mailSender.sendMail("event@mail.com");
System.out.println("done.");
}
}
运行结果如下:
MailSender:模拟发送邮件...
MailSendListener:向event@mail.com发送完一封邮件
done.
————本文结束感谢您的阅读————