本章的pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.smart</groupId>
<artifactId>chapter6</artifactId>
<version>1.0</version>
<name>Spring4.x第六章实例</name>
<dependencies>
<!-- spring 依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>${commons-dbcp.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7.2</version>
<configuration>
<forkMode>once</forkMode>
<threadCount>10</threadCount>
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<file.encoding>UTF-8</file.encoding>
<spring.version>4.2.2.RELEASE</spring.version>
<testng.version>6.8.7</testng.version>
<aopalliance.version>1.0</aopalliance.version>
<commons-codec.version>1.9</commons-codec.version>
<commons-lang.version>2.0</commons-lang.version>
<commons-dbcp.version>1.4</commons-dbcp.version>
<mysql.version>5.1.29</mysql.version>
</properties>
</project>
Spring容器技术内幕
内部工作机制
Spring 的AbstractApplicationContext是ApplicationContext的抽象实现类,该抽象类的refresh()方法定义了Spring容器在加载配置文件后的各项处理过程,这些处理过程清晰地刻画了Spring容器启动时的各项操作,下面是refresh()中的逻辑执行
代码清单6-1
下图描述Spring容器从加载配置文件到创建出一个完整Bean的作业流程及参与的角色
Spring组件按其所承担的角色可以划分为两类
(1)物料组件:Resource,BeanDefinition,PropertyEditor及最终的Bean等,像流水线上被加工的物料
(2)设备组件:ResourceLoader,BeanDefinitionReader,BeanFactoryPostProcessor,InstantiationStrategy及BeanWrapper等,像流水线上的加工设备
BeanDefinition
BeanDefinition是配置文件<bean>元素标签在容器内的表示。
<bean>拥有class,scope,lazy-init等配置属性,BeanDefinition对应的有beanClass,scope,lazyInit等属性
在配置文件中可以定义父<bean>和子<bean>,分别对应RootBeanDefinition和ChildBeanDefinition,没有父<bean>的<bean>则用RootBeanDefinition表示。AbstractBeanDefinition对二者共同的类信息进行抽象
Spring通过BeanDefinition将配置文件中的<bean>配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。
BeanDefinitionRegistry就像Spring配置信息的内存数据库,后续操作直接从BeanDefinitionRegistry中读取配置信息。
一般情况下,BeanDefinition只在容器启动时加载并解析,除非容器刷新或重启,这些信息不会发生变化。
创建最终的BeanDefinition主要包括两个步骤
①利用BeanDefinitionReader读取承载配置信息的Resource,通过XML解析器解析配置信息的DOM(Document Object Model)对象,简单地为每个<bean>生成对应的BeanDefinition。在配置文件中,可能通过占位符变量引用外部属性文件的属性,这些占位符变量在这一步还未被解析出来
②利用容器中注册的BeanFactoryPostProcessor对半成品的BeanDefinition进行加工处理,将以占位符表示的配置解析为最终的实际值
InstantiationStrategy
InstantiationStrategy负责根据BeanDefinition对象创建一个Bean实例。
Spring将实例化Bean的工作通过一个策略接口进行描述,是为了可以方便地采用不同的实例化策略,以满足不同需求。
SimpleInstantiationStrategy是最常用的实例化策略,该策略利用Bean实现类的默认构造函数,带参构造函数或工厂方法创建Bean的实例
CglibSubclassingInstantiationStrategy扩展了SimpleInstantiationStrategy,为需要进行方法注入的Bean提供了支持。利用CGLib类库为Bean动态生成子类,在子类中生成方法注入的逻辑,然后使用这个动态生成的子类创建Bean的实例
InstantiationStrategy仅负责实例化Bean的操作,相当于new的功能,不会参数Bean属性的设置。所以由InstantiationStrategy返回的Bean实例,实际上是半成品,属性填充的工作留待BeanWrapper来完成
BeanWrapper
Spring委托BeanWrapper完成Bean属性的填充工作。在Bean实例被InstantiationStrategy创建出来后容器主控程序将Bean通过BeanWrapper包装起来,这是通过调用BeanWrapper的setWrappedInstance(Object obj)方法完成的
可以看出,BeanWrapper有两个顶级类接口,PropertyAccessor和PropertyEditorRegistry。
PropertyAccessor接口定义了各种访问Bean 属性的方法,而PropertyEditorRegistry是属性编辑器的注册表
所以BeanWrapper实现类BeanWrapperImpl有三重身份:
①Bean包装器
②属性访问器
③属性编辑器注册表
一个BeanWrapperImpl实例内部封装了两类组件:
被封装的待处理的Bean和一套用于设置Bean属性的属性编辑器
要填充属性,除了目标Bean实例和属性编辑器外,还需要获取Bean对应的BeanDefinition,它从BeanDefinitionRegistry中直接获取。Spring主控程序从BeanDefinition中获取Bean属性的配置信息PropertyValue,并使用属性编辑器对PropertyValue进行转换以得到Bean的属性。
BeanWrapperImpl在内部使用Spring 的BeanUtils工具类对Bean进行反射操作,设置属性
属性编辑器
PropertyEditor是JavaBean规范定义的接口
JavaBean的编辑器
JavaBean通过java.beans.PropertyEditor定义了设置JavaBean属性的方法,通过BeanInfo描述了JavaBean的哪些属性是可定制的,还描述了可定制属性与PropertyEditor的对应关系
BeanInfo与JavaBean之间的对应关系通过二者之间的命名规范确立:<Bean>BeanInfo,如Car对应的BeanInfo为CarBeanInfo
当JavaBean连同其属性编辑器一同注册到IDE中,在开发界面对JavaBean进行定制时,IDE会根据JavaBean规范找到对应的BeanInfo,在根据BeanInfo中的描述信息找到JavaBean属性描述,进而为JavaBean生成特定的开发编辑界面
JavaBean规范提供了一个管理默认属性编辑器的管理器PropertyEditorManager,该管理器内保存着一些常见类型的属性编辑器
PropertyEditor
PropertyEditor是属性编辑器的接口,规定了将外部设置值转换为内部JavaBean属性值的转换接口方法,主要接口方法如下:
Java为PropertyEditor提供了一个方便类PropertyEditorSupport,该类实现了PropertyEditor接口并提供了默认实现。用户可以通过扩展这个类设置自己的属性编辑器
BeanInfo
BeanInfo描述了JavaBean的哪些属性可以编辑及对应的属性编辑器,每个属性对应一个属性描述器PropertyDescriptor。PropertyDescriptor的构造函数有两个入参:PropertyDescriptor(String propertyName,Class beanClass),propertyName为属性名,beanClass为JavaBean对应的Class。
PropertyDescriptor还有一个setPropertyEditorClass(Class propertyEditorClass)方法,用于为JavaBean属性指定编辑器
BeanInfo接口最重要的方法是PropertyDescriptor[] getPropertyDescriptor(),返回JavaBean的属性描述器数组
BeanInfo有一个常用的实现类SimpleBeanInfo,一般通过扩展它实现自己的功能
实例
ChartBean是一个可定制图标组件,允许通过属性设置定制图标的样式。继承了JPanel
public class ChartBean extends JPanel {
private int titlePostition=CENTER;
private boolean inverse;
//getter/setter
}
下面为titlePosition属性提供一个属性编辑器,我们通过扩展PropertyEditorSuppot这个方便类来定义属性编辑器
public class TitlePositionEditor extends PropertyEditorSupport {
private String[] options={"Left","Center","Right"};
//①代表可选属性值的字符串标识数组
public String[] getTags(){
return options;
}
//②代表属性初始值的字符串
public String getJavaInitializationString(){
return ""+getValue(); //getValue为PropertyEditior的接口方法,返回当前的属性值
}
//③将内部属性值转换为对应的字符串表现形式,供属性编辑器显示
public String getAsText(){
int value=(Integer)getValue();
return options[value];
}
//将外部设置的字符串转换为内部属性的值
public void setAsText(String s){
for (int i = 0; i <options.length ; i++) {
if(options[i].equals(s)){
setValue(i); //设置属性的值
return ;
}
}
}
}
①通过getTags()返回一个字符串数组,因此在IDE中该属性对应的编辑器将自动提供一个下拉框。③和④的两个方法分别完成属性值到字符串的双向转换功能。
下面编写ChartBean对应的BeanInfo,根据命名规范,应起名为ChartBeanBeanInfo,它负责将属性编辑器和ChartBean的属性联系起来
public class ChartBeanBeanInfo extends SimpleBeanInfo{
public PropertyDescriptor[] getPropertyDescriptors() {
try {
//①将TitlePositionEditor绑定到ChartBean的titlePosition属性中
PropertyDescriptor titlePositionDescriptor=
new PropertyDescriptor("titlePosition",ChartBean.class);
titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class);
//②将InverseEditor绑定到ChartBean的inverse属性中
PropertyDescriptor inverseDescriptor=
new PropertyDescriptor("inverse",ChartBean.class);
inverseDescriptor.setPropertyEditorClass(InverseEditor.class);
return new PropertyDescriptor[]{titlePositionDescriptor,inverseDescriptor};
}catch (IntrospectionException e){
e.getStackTrace();
return null;
}
}
}
将ChartBean连同属性编辑器及ChartBeanBeanInfo打成JAR包,使用IDE组件扩展管理功能注册到IDE中,这样就可以使用TextField等组件一样对ChartBean进行可视化的开发设计工作
Spring默认属性编辑器
Spring为常见的属性类型提供了默认的属性编辑器,从图6-4可知BeanWrapperImpl类扩展了PropertyEditorRegistrySupport类,Spring在PropertyEditorRegistrySupport中为常见属性类型提供了默认的属性编辑器
PropertyEditorRegistrySupport中有两个用于保存属性编辑器的Map类型变量
①defaultEditors:用于保存默认属性的编辑器,元素的键为属性类型,值为对应的属性编辑器实例
②customEditors:用于保存用于自定义的属性编辑器,元素的键值和defaultEditors相同
PropertyEditorRegistrySupport通过类似以下的代码定义默认属性编辑器
自定义属性编辑器
可以通过扩展PropertyEditorSupport实现自己的属性编辑器,Spring环境中仅需简单覆盖PropertyEditorSupport的setAsText()方法
实例
现在希望在配置Boss时不通过引用Bean的方式注入Boss的car属性,而是直接通过字面值提供配置
Boss有两个属性:name和Car。Spring拥有String类型的默认属性编辑器,Car类型是自定义的类型,要配置car属性,有两种方案:
①在配置文件中为car专门配置一个<bean>,然后在Boss<bean>中通过ref引用car Bean
②为Car类型提供一个自定义的属性编辑器,可以通过字面值为Boss的car属性提供配置值
第一种方案是常用,但在某些情况下,需要直接为属性提供一个对应的自定义属性编辑器
public class CustomCarEditor extends PropertyEditorSupport {
//将字面值转换为属性类型对象
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]));
//调用父类的setValue()方法设置转换后的属性对象
setValue(car);
}
}
仅覆盖了PropertyEditorSupport便利类的setAsText方法,该方法负责将配置文件以字符串提供的字面值转换为Car对象。由于不需要将car属性回显到属性编辑器的UI界面中,不需要覆盖getAsText()方法
注册自定义的属性编辑器
如果使用BeanFactory,需要手工调用registerCustomEditor(Class requiredType,PropertyEditor propertyEditor)方法注册自定义的属性编辑器
如果使用ApplicationContext,只需在配置文件中通过CustomEditorConfigurer注册。CustomEditorConfigurer实现了BeanFactoryPostProcessor接口,因而是一个Bean工厂后处理器。BeanFactoryPostProcessor在Spring容器加载配置文件并生成BeanDefinition半成品后就会被自动执行,CustomEditorConfigurer在容器启动时有机会注入自定义的属性编辑器
下面的定义片段定义了一个CustomEditorConfigurer
<!--①配置自动注册属性编辑器的CustomEditorConfigurer -->
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<!-- ②属性编辑器对应的属性类型-->
<entry key="com.smart.editor.Car"
value="com.smart.editor.CustomCarEditor"/>
</map>
</property>
</bean>
<bean id="boss" class="com.smart.editor.Boss">
<property name="name" value="John"/>
<!-- ③该属性使用上面的属性编辑器完成属性填充操作-->
<property name="car" value="红旗CA72,200,20000.00"/>
</bean>
在①处定义了用于注册自定义属性编辑器的CustomEditorConfigurer,Spring容器将通过反射机制自动调用这个Bean。CustomEditorConfigurer通过一个Map属性定义需要自动注册的自定义属性编辑器。在②为Car类型指定了对应的属性编辑器
在③直接通过value为car属性提供配置。BeanWrapper在设置Boss的car属性时,将检索自定义属性编辑器的注册表,当发现Car类型属性拥有对应的属性编辑器CustomCarEditor时,就会利用CustomCarEditor将”红旗CA72,200,20000.00”转换为Car对象
使用外部属性文件
将配置信息独立到一个外部属性文件中,并在Spring配置文件汇总通过占位符应用属性文件中的属性项,可以减少维护的工作量,使部署更简单
Spring提供了PropertyPlaceholderConfigurer,它能使Bean在配置时应用外部属性文件。它实现了BeanFactoryPostProcessor接口,也是一个Bean工厂后处理器
PropertyPlaceholderConfigurer属性文件
使用PropertyPlaceholderConfigurer属性文件
驱动类名,JDBC的URL地址及用户名/密码都在XML文件中,在部署应用中,必须先找出这个Bean配置XML文件,再找出数据源Bean定义的代码段进行调整,很不方便
可以将需要调整的配置信息抽取到一个配置文件中,这里使用名为jdbc.properties的配置文件
每个属性都由一个属性名和一个属性值组成,用”=”隔开。
通过PropertyPlaceholderConfigurer引入jdbc.properties属性文件,调整数据源Bean的配置
或
<!-- ①引入jdbc.properties属性文件-->
<context:property-placeholder
location="classpath:com/smart/placeholder/jdbc.properties"/>
<!--② 通过属性名引用属性值-->
<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的location属性引入属性文件,这样就可以在Bean定义的时候引用属性文件中的属性了
PropertyPlaceholderConfigurer的其他属性
包括locations,fileEncoding,order,placeholderPrefix和placeholderSuffix
使用<context:property-placeholder>引用属性文件
<context:property-placeholder
location="classpath:com/smart/placeholder/jdbc.properties"/>
在基于注解及基于Java类的配置中引用属性
在基于XML的配置文件中,通过”&{propNaem}”形式引用属性值
在基于注解配置的Bean可以通过@Value注解为Bean的成员变量或方法入参自动注入容器已有的属性
@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);
}
}
@Value注解可以为Bean注入一个字面值,亦可以通过@Value(“&{propName}”)的形式根据属性名注入属性值。由于标注@Configureration的类本身就相当于标注了@Component,所以标注了@Configuration的类中引用属性的方式和基于注解配置的引用方式是完全一样的
使用加密的属性文件
PropertyPlaceholderConfigurer继承自PropertyResourceConfigurer类,PropertyResourceConfigurer类有几个有用的protected方法,用于在属性使用之前对属性列表中的属性转换
可以扩展PropertyPlaceholderConfigurer,覆盖相应的属性转化方法,就可以支持加密版的属性文件了
DES加密解密工具类
DES属于对称加密。先用DES对属性值进行加密,在读取属性值时,再用DES进行解密
public class DESUtils {
//指定DES加密解密所用的密钥
private static Key key;
private static String KEY_STR="myKey";
static {
try{
KeyGenerator generator = KeyGenerator.getInstance("DES");
generator.init(new SecureRandom(KEY_STR.getBytes()));
key=generator.generateKey();
generator=null;
}catch (Exception e){
throw new RuntimeException(e);
}
}
//对字符串进行DES加密返回BASE64编码的加密字符串
public static String getEncryptString(String str){
BASE64Encoder base64Encoder=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 base64Encoder.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, "UTF8");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//对入参的字符串进行加密。打印出加密后的串
public static void main(String[] args) throws Exception {
if (args == null || args.length < 1) {
System.out.println("请输入要加密的字符,用空格分隔.");
} else {
for (String arg : args) {
System.out.println(arg + ":" + getEncryptString(arg));
}
}
}
}
BASE64编码是以大小写字母,数字及其他几个字符组成的编码串
使用密文版的属性文件
在DOS窗口下输入java com.smart.placeholder.DESUtils root 123456
PropertyPlaceholderConfigurer不支持密文版的属性,我们扩展它,覆盖String converProperty(String propertyName,String propertyValue)方法,对userName和password的属性值进行解密
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;
}
}
使用自定义的属性加载器后,就无法使用<context:property-placeholder>引用属性文件,必须通过传统的配置方式引用加密版的属性文件
<!--3.使用加密版的属性文件 -->
<bean class="com.smart.placeholder.EncryptPropertyPlaceholderConfigurer"
p:location="classpath:com/smart/placeholder/jdbc.properties"
p:fileEncoding="utf-8"/>
<context:component-scan base-package="com.smart.placeholder"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close" p:driverClassName="${driverClassName}" p:url="${url}"
p:username="${userName}" p:password="${password}" />
属性文件自身的引用
Spring既允许在Bean定义中通过
propName引用属性值,也允许在属性文件中使用
{propName}实现属性之间的相互引用
如果一个属性值太长,一行写不下,可以通过在行后添加”\”将属性值划分为多行,如:
引用Bean的属性值
可以通过类似#{beanName,beanProp}的方式方便地引用另一个Bean的值
public class SysConfig {
private int sessionTimeout;
private int maxTabPageNum;
private DataSource dataSource;
public void initFromDB(){
//从数据库中获取属性配置值
this.sessionTimeout = 30;
this.maxTabPageNum = 10;
}
public int getSessionTimeout() {
return sessionTimeout;
}
public int getMaxTabPageNum() {
return maxTabPageNum;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
为了简化代码,仅采用直接设置属性值的方式演示属性引用的过程
在XML配置文件中,先将SysConfig定义为一个Bean,这样在定义数据源时可以通过#{beanName,beanProp}的方式引用Bean的属性值
beans.xml 引用Bean的属性值
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--加载jdbc的属性文件 -->
<context:property-placeholder
location="classpath:com/smart/placeholder/jdbc.properties"/>
<!-- 连接数据库-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${userName}"
p:password="${password}"/>
<!-- ①通过initFromDB方法从数据源中获取配置属性值-->
<bean id="sysConfig" class="com.smart.beanprop.SysConfig"
init-method="initFromDB"
p:dataSource-ref="dataSource"/>
<!-- ②引用Bean的属性值-->
<bean class="com.smart.beanprop.ApplicationManager"
p:maxTabPageNum="#{sysConfig.maxTabPageNum}"
p:sessionTimeout="#{sysConfig.sessionTimeout}"/>
</beans>
在生产环境下,一般通过JNDI引用应用容器的数据源。然后通过sysConfig的initFromDB()方法访问数据库,获取应用系统的配置信息,并将其保存在sysConfig的属性中
入②其他需要访问应用系统配置信息的Bean可通过#{sysConfig.maxTabPageNum}表达式引用sysConfig Bean的属性值
在基于注解和基于Java类配置的Bean中,通过@Value(“#{beanName.propName}”)的注解形式引用Bean的属性值
@Component
public class ApplicationManager {
@Value("#{sysConfig.sessionTimeout}")
private int sessionTimeout;
@Value("#{sysConfig.maxTabPageNum}")
private int maxTabPageNum;
...
}
国际化信息
基础知识
“国际化信息”也称为“本地化消息”,需要“语言类型”和“国家/地区类型”这两个条件才可以确定一个特定类型的本地化信息。
Java通过java.util.Locale类表示一个本地化对象,允许通过语言参数和国家/地区参数创建一个确定的本地化对象
Locale
Locale是表示语言和国家/地区信息的本地化类,是创建国际化应用的基础
③和④直接通过引用常量返回本地化对象
本地化工具类
java.util包中提供了几个支持本地化的格式化操作工具类,如NumberFormat,DateFormat,MessageFormat
NumberFormat按本地化的方式对货币金额进行格式化操作
DateFormat.getDateInstance(int style,Locale locale)方法按本地化的方式对日期进行格式化操作。第一个入参为时间样式,第二个为本地化对象
输出为:
MessageFormat在这两者的基础上提供了占位符字符串的格式化功能,支持时间,货币,数字及对象属性的格式化操作
pattern1是简单形式的格式化信息串,通过{n}占位符指定动态参数的替换位置索引,{0}表示第一个参数,{1}表示第二个参数,以此类推
pattern2,除了参数位置索引索引,还指定了参数的类型和样式。{1,time,short}表示从第二个入参中获取时分秒部分的值,显示为短样式时间;{1,date,long}表示从第二个入参中获取日期部分的值,显示为长样式时间。
在②定义了用于替换格式化占位符的动态参数,用到了自动装包的语法。
在③通过messageFormat的format()方法格式化信息串,使用了系统默认的本地化对象,由于是中文平台,因此默认为Locale.CHINA。在④显示指定了MessageFormat的本地化对象
输出为:
ResourceBoundle
如果应用系统中的某些信息需要支持国际化功能,则必须为期望支持的不同本地化类型分别提供对应的资源文件,并以规范的方式进行命名:
其中,语言代码和国家/地区代码是可选的。
<资源名>.properties命名的国际化资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用它。
<资源名>_<语言代码>.properties命名的国际化资源文件是某一语言默认的资源文件,即在某个本地化类型在系统中找不到精确匹配的资源文件,就采用相应语言默认的资源文件
假设资源名为”resource”,语言为英语,国家/地区为美国,则与其对应的本地化资源文件名为resourece_en_US.properties
信息在资源文件以属性名/值的方式表示:
如果为中文,中国大陆的本地化资源文件则命名为resource_zh_CN.properties,资源内容为:
上面的内容采用了特殊的编码形式,这是因为资源文件对文件内容有严格要求:只能包含ASCII字符,所以必须将非ASCII字符的内容转换为Unicode代码的表示方式
用Unicode编码编辑资源文件是不方便的,通常情况下直接使用正常的方式编写资源文件,在测试或部署时再采用工具进行转换。
JDK在bin目录下提供了一个完成这个任务的native2ascii工具,可以将中文字符的资源文件转换为Unicode编码格式的文件,命令格式如下:
resource_zh_CN.properties包含中文字符并且以UTF-8进行编码,假设将该资源文件放到d:\目录下,通过下面的命令就可以转换为Unicode编码的形式:
前后对比
由于原资源采用UTF-8编码,必须显式地通过-encoding 指定编码格式
使用native2ascill命令操作不方便,在许多IDE中都有属性编辑器的插件,插件会自动将资源问价内容转换为ASCII形式的编码,以正常的方式阅读和编辑资源文件的内容。
IDEA支持这种透明化编辑资源文件的功能,默认未开启,可通过如下方式启动:
Setting→Editor→File Encoding→勾选“Transparent native-to-ascii conversion”复选框
Java提供了用于加载本地化资源文件的方便类java.util.ResourceBoundle.它为加载及访问资源文件提供了便捷的操作
下面语句从相对于类路径的目录中加载一个名为resource的本地化资源文件
ResourceBundle rb=ResourceBundle.getBundle("com/smart/i18n/resource",locale)
通过下面代码即可访问资源的属性值
rb.getString("greeting.common");
rb1加载了对应美国英语本地化的resource_en_US.properties资源文件,而rb2加载了对应中国大陆中文的resource_zh_CN.properties
输出为:
在加载资源文件时,如果不指定本地化对象,则使用本地系统默认的本地化对象。所以在中文系统中,ResourceBundle rb=ResourceBundle.getBundle(“com/smart/i18n/resource”)语句将返回和rb2相同的本地化资源
ResourceBundle在加载时,如指定的本地化资源文件不存在,则按以下顺序加载其他资源:本地系统默认本地化对象对应的资源→默认的资源
假设使用
“ResourceBundle rb=ResourceBundle.getBundle(“com/smart/i18n/resource”,locale.CANADA)”
由于不存在resource_en_CA.properties资源文件,将尝试加载resource_zh_CN.properties。
假设resource_zh_CN.properties也不存在,则继续尝试加载resource.properties资源文件,如果都不存在,则抛出MissingResourceException
在资源文件中使用格式化串
属性值要结合运行时的动态参数构造出灵活的信息,可以使用带占位符的格式化串作为资源文件并结合使用MessageFormat
代码6-17
//加载本地化资源
ResourceBundle rb1=ResourceBundle.getBundle("com/smart/i18n/fmt_resource", Locale.US);
ResourceBundle rb2=ResourceBundle.getBundle("com/smart/i18n/fmt_resource", Locale.CHINA);
Object[] params = {"John", new GregorianCalendar().getTime()};
//用本地化对象进行格式化
String str1=new MessageFormat(rb1.getString("greeting.common"),Locale.US).format(params);
String str2=new MessageFormat(rb2.getString("greeting.morning"),Locale.CHINA).format(params);
String str3=new MessageFormat(rb2.getString("greeting.afternoon"),Locale.CHINA).format(params);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
输出
MessageSource
Spring定义了访问国际化信息的MessageSource接口,并定义了几个实现类。
该接口有几个重要方法:
getMessage
MessageSource类结构
HierarchicalMessageSource接口添加了两个方法,建立父子层级的MessageSource结构,该接口的setParentMessageSource(MessageSource parent)方法用于设置父MessageSource,而
getParentMessageSource()方法用于返回父MessageSource。
HierarchicalMessageSource接口最终的两个实现类是ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。
它们基于ResourceBundle基础类实现,允许仅通过资源吗加载国际化信息。
ReloadableResourceBundleMessageSource提供了定时刷新功能,允许在不重启系统的情况更新资源的信息。
StaticMessageResource主要用于程序测试,允许通过编程的方式提供国际化信息。而DelegatingMessageSource是为了方便操作父MessageSource而提供的代理类
ResourceBundleMessageSource
该类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名,完成和6-17相同的任务
通过ResourceBundleMessageSource配置资源
<bean id="myResource1" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>com/smart/i18n/fmt_resource</value>
</list>
</property>
</bean>
启动Spring容器,并通过MessageSource访问配置的国际化信息
private static void rsrBdlMessageResource(){
String[] configs={"com/smart/i18n/beans.xml"} ;
//启动Spring容器,并加载配置文件
ApplicationContext ctx=new ClassPathXmlApplicationContext(configs);
//获取MessageSource的Bean
MessageSource ms=(MessageSource)ctx.getBean("myResource1");
Object[] params = {"John", new GregorianCalendar().getTime()};
//获取格式化的国际化信息
String str1 = ms.getMessage("greeting.common",params, Locale.US);
String str2 = ms.getMessage("greeting.morning",params,Locale.CHINA);
String str3 = ms.getMessage("greeting.afternoon",params,Locale.CHINA);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
}
无需分别加载不同语言,不同国家/地区的本地化资源文件,仅通过资源名就可以加载整套国际化信息资源文件。
无须显式使用MessageFormat操作国际化信息,仅通过MessageSource#getMessage()方法就可以完成操作
结果和上面一样。
ReloadableResourceBundleMessageSource
和ResourceBundleMessageSource的唯一区别在于它可以定时刷新资源文件,以便在应用程序不重启的情况下感知资源文件的变化。
通过ReloadableResourceBundleMessageSource配置资源
<bean id="myResource2" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>com/smart/i18n/fmt_resource</value>
</list>
</property>
<property name="cacheSeconds" value="5"/>
</bean>
通过cacheSeconds属性让ReloadableResourceBundleMessageSource每5秒刷新一次资源文件,默认值为-1,表示永不刷新
容器级的国际化信息资源
在上面的结构图中,发现ApplicationContext实现了MessageSource的接口,即ApplicationContext的实现类也是一个MessageSource对象。
回顾代码6-1,在容器启动阶段,④的initMessageSource()方法所执行的工作就是初始化容器中的国际化信息资源,根据反射机制从BeanDefinitionRegistry中找出名为messageSource且类型为org.springframework.context.MessageSource的这个Bean,将这个定义的信息加载为容器级的国际化信息资源
容器级资源的配置
<!-- 注册资源Bean,注意名字只能为messageSource-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>com/smart/i18n/fmt_resource</value>
</list>
</property>
</bean>
注意名字只能为messageSource
通过ApplicationContext直接访问国际化信息资源
private static void ctxMessageResource() throws Exception{
String[] configs = {"com/smart/i18n/beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
Object[] params = {"John", new GregorianCalendar().getTime()};
String str1 = ctx.getMessage("greeting.common",params,Locale.US);
String str2 = ctx.getMessage("greeting.morning",params,Locale.CHINA);
System.out.println(str1);
System.out.println(str2);
}
如果没有命名为messageSource,将抛出NoSuchMessageException
容器事件
ApplicationContext能发布事件并且允许注册相应的事件监听器,拥有一套完善的事件发布和监听机制。Java通过EventObject类和EventListener接口描述事件和监听器,某个组件或框架如需事件发布和监听机制,需要通过扩展它们进行定义。
在事件体系中,除了事件和监听器外,还有3个重要概念
①事件源:事件的产生者,任何一个EventObject都必须拥有一个事件源
②事件监听器注册表:组件或框架必须提供事件监听器注册表来保存事件监听器。当组件和框架中的事件源产生事件时,就会通知这些位于事件监听器注册表中的监听器。
③事件广播器:它是事件和事件监听器沟通的桥梁,负责把事件通知给事件监听器
这3个角色有时可以由同一个对象承担,如java.swing包中的JButton,JCheckBox等组件
Spring事件类结构
事件类
Spring仅定义了几个事件
Application的唯一构造函数是ApplicationEvent(Object source),通过source指定事件源,有两个子类:
①ApplicationContextEvent:容器事件,拥有4个子类,分别表示容器启动,刷新,停止及关闭的事件
②RequestHandleEvent:这是一个与Web应用相关的事件,当一个HTTP请求被处理后,产生该事件,只有在web.xml中定义了DispatcherServlet时才会产生该事件。拥有两个子类,分别代表Servlet和Portlet的请求事件
事件监听器接口
Spring 的事件监听器都继承自ApplicationListener接口
ApplicationListener接口只定义了一个方法:onApplicationEvent(E event),该方法接收ApplicationEvent事件对象,在该方法中编写事件的响应处理逻辑。
SmartApplicationListener定义了两个方法:
①boolean supportsEventType(Class
Spring事件体系的具体实现
Spring在ApplicationContext接口的抽象实现类AbstractApplicationContext中完成了事件体系的搭建。
AbstractApplicationContext拥有一个applicationEventMulticaster(应用上下文事件广播器)成员变量,它提供了容器监听器的注册表。
AbstractApplicationContext在refresh()这个容器启动启动方法中通过以下3个步骤搭建了事件的基础设施,在6-1中可以看到
在⑤,Spring初始化事件的广播器,可以在配置文件中为容器定义一个自定义的事件广播器,只要实现ApplicationEventMulticaster即可,Spring会通过反射机制将其注册容器的事件广播器。如果没有找到配置的外部事件广播器,则Spring自动使用SimpleApplicationEventMulticaster作为事件广播器
在⑦,Spring根据反射机制,从BeanDefinitionRegistry中找出所有实现ApplicationListener的Bean,将它们注册为容器的事件监听器,即将其添加到事件广播器所提供的事件监听器注册表中
在⑨,容器启动完成,调用事件发布接口向容器中所有的监听器发布事件
实例
模拟的邮件发送器MailSender,在向目的地发送邮件时,将产生一个MailSendEvent事件,容器中注册了监听该事件的监听器MailSendListener。
public class MailSendEvent extends ApplicationContextEvent {
private String to;
public MailSendEvent(ApplicationContext source,String to){
super(source);
this.to=to;
}
public String getTo() {
return this.to;
}
}
直接扩展ApplicationContextEvent,事件对象除source属性外,还具有一个代表发送目的地的to属性
事件监听器MailSenderListener负责监听MailSendEvent事件
public class MailSendListener implements ApplicationListener<MailSendEvent> {
//对MailSendEvent事件进行处理
public void onApplicationEvent(MailSendEvent event){
MailSendEvent mse=(MailSendEvent) event;
System.out.println("MailSendListener:向"+mse.getTo()+"发送一封邮件");
}
}
MailSendListener实现ApplicationListener接口,在接口方法中通过instanceof操作符判断事件的类型,仅对MailSendEvent类型的事件进行处理
MailSender要拥有发布事件的能力,必须实现ApplicationContextAware接口
public class MailSender implements ApplicationContextAware {
private ApplicationContext ctx;
//AppllicationContextAware的接口方法, 以便容器启动时注入容器实例
public void setApplicationContext(ApplicationContext ctx)throws BeansException{
this.ctx=ctx;
}
public void sendMail(String to){
System.out.println("MailSend模拟发送邮件");
MailSendEvent mse=new MailSendEvent(this.ctx,to);
//向容器中的所有事件监听器发送事件
ctx.publishEvent(mse);
}
}
在Spring 的配置文件中,仅需如下配置:
<bean class="com.smart.event.MailSendListener"/>
<bean id="mailSender" class="com.smart.event.MailSender"/>
下面代码启动容器并调用mailSender Bean发送一封邮件
public static void main(String[] args) {
String resourceFile = "com/smart/event/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(resourceFile);
MailSender mailSender = ctx.getBean(MailSender.class);
mailSender.sendMail("test mail.");
System.out.println("done.");
、}