5.1 Spring 容器技术内幕
5.1.1 内部工作机制
Spring 的 AbstractApplicationContext 是 ApplicationContext 抽象实现类,该抽象类的 refresh() 方法定义了 Spring 容器在加载配置文件后的各项处理过程
- 1.初始化 BeanFactory:根据配置文件实例化 BeanFactory,getBeanFactory() 方法由具体子类实现。在这一步里,Spring 将配置文件的信息装入到容器的 Bean 定义注册表(BeanDefinitionRegistry)中,但此时 Bean 还未初始化;
- 2.调用工厂后处理器:根据反射机制从 BeanDefinitionRegistry 中找出所有 BeanFactoryPostProcessor 类型的 Bean,并调用其 postProcessBeanFactory() 接口方法;
- 3.注册 Bean 后处理器:根据反射机制从 BeanDefinitionRegistry 中找出所有 BeanPostProcessor 类型的 Bean,并将他们注册到容器 Bean 后处理器的注册表中;
- 4.初始化消息源:初始化容器的国际化信息资源;
- 5.初始化应用上下文事件广播器;
- 6.初始化其他特殊的 Bean:这是一个钩子方法,子类可以借助这个方法执行一些特殊的操作:如 AbstractRefreshableWebApplicationContext 就使用该方法执行初始化 ThemeSource 的操作;
- 7.注册事件监听器;
- 8.初始化所有单实例的 Bean,使用懒初始化模式的 Bean 除外:初始化 Bean 后,将它们放入 Spring 容器的缓存中;
- 9.发布上下文刷新事件:创建上下文刷新事件,事件广播器负责将这些事件广播到每个注册的事件监听器中。
Bean 从创建到销毁的生命历程,Spring 协调多个组件共同完成这个复杂的工作流程
- 1.ResourceLoader 从存储介质中加载 Spring 配置信息,并使用 Resource 表示这个配置文件夹的资源;
- 2.BeanDefinitionReader 读取 Resource 所指向的配置文件资源,然后解析配置文件。配置文件中每一个< bean >解析成一个 BeanDefinition 对象,并保存到 BeanDefinitionRegistry中;
- 3.容器扫描 BeanDefinitionRegistry 中的BeanDefinittion,使用 Java的反射机制自动识别出 Bean 工厂后处理器(实现 BeanFactoryPostProcessor 接口)的 Bean,然后调用这些 Bean 工厂后处理器对 BeanDefinitionRegistry 中的 BeanDefinition 进行加工处理。主要完成两项工作:
- 对使用到的占位符的< bean > 元素标签进行解析,得到最终的配置值,这意味对一些半成品式的 BeanDefinition 对象进行加工处理并得到的成品的 BeanDefinittion 对象;
- 对 BeanDefinitionRegistry 中的 BeanDefinittion 进行扫描,通过 Java反射机制找出所有属性编辑器的 Bean (实现java.beans.PropertyEditor 接口的 Bean),并自动将他们注解到 Spring容器的属性编辑器注册表中(PropertyEditorRegistry)
- 4.Spring 容器从 BeanDefinitionRegistry 中取出加工后的 BeanDefinittion, 并调用 InstantiationStrategy 着手进行 Bean 实例化的工作;
- 5.在实例化 Bean 时,Spring 容器使用 BeanWrapper 对 Bean 进行封装,BeanWrapper提供了很多以 Java 反射机制操作的 Bean 方法,他将结合 Bean 的 BeanDefinittion 以及容器中属性编辑器,完成 Bean 属性的设置工作;
- 6.利用容器中注册的 Bean 后处理器(实现 BeanPostProcessor 接口的 Bean)对已经完成属性设置工作的 Bean 进行后续加工,直接装配出一个准备就绪的 Bean。
Spring 组件按其所承担的角色可以划分为两类
- 物料组件:Resource、BeanDefinition、PropertyEditor 以及最终的 Bean 等,他们是加工流程中被加工、被消费的组件;
- 加工设备组件:ReshourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy 以及 BeanWrapper 等组件像是流水线上不同环节的加工设备,对物料组件进行加工处理。
5.1.2 BeanDefinition
org.springframework.beans.factory.config.BeanDefinition 是配置文件< bean >元素标签在容器中的内部表示。< bean >元素标签拥有 class、scope、lazy-init 等配置属性,BeanDefinition则提供了相应的 BeanClass、scope、lazyInit 属性,BeanDefinition 就像是 < bean >的镜中人,两者是一一对应的
RootBeanDefinition 是最常用的实现类,他对应一般性的< bean >元素标签,我们知道在配置文件中可以定义父< bean >和子< bean >,父< bean >用RootBeanDefinition 表示,而子 < bean >用 ChildBeanDefinition 表示,而没有父< bean >的< bean >就使用RootBeanDefinition表示。
Spring 通过 BeanDefinition 将配置文件中的< bean >配置信息转化为容器的内部表示,并将这些 BeanDefinition 注册到 BeanDefinitionRegistry 中。Spring 容器的BeanDefinitionRegistry 就像是 Spring 配置信息的内存数据库,后续操作直接从 BeanDefinitionRegistry 中读取配置信息。一般情况下,BeanDefinition 只是在容器启动时加载并解析,除非容器刷新或重启,这些信息不会发生变化,当然如果用户由特殊的需求,也可以通过编程的方式在运行期间调整 BeanDefinition 的定义。
创建最终的 BeanDefinition 主要包含两个步骤:
- 利用BeandefinitionReader 对配置信息 Resource 进行读取,通过XML解析器解析配置信息的 DOM 对象,简单地为每个< bean >生成对应的BeanDefinition对象。但是这里生成的BeanDefinition可能时半成品,因为在配置文件中,我们可能通过占位符变量引用外部属性文件的属性,这些占位符变量在这一步还没有被解析出来。
- 利用容器中注册的 BeanFactoryPostProcessor 对半成品的 BeanDefinition 进行加工处理,将以占位符表示的配置解析为最终的实际值,这样半成品的 BeanDefinition 就成品的 BeanDefinition。
5.1.3 InstantiationStrategy
org.springframework.beans.factory.support.InstantiationStrategy 负责根据 BeanDefinition对象创建一个 Bean 实例。Spring之所以将实例化Bean 的工作通过一个策略接口进行描述,是为了方便采用不同的实例化策略,以便满足不同的应用需求:如通过CGLib类库为Bean动态创建子类再进行实例化。
SimpleInstantiationStrategy 策略是最常用的实现类,该策略利用 Bean 实现类的默认构造函数、带参构造函数或工厂方法创建Bean的实例。
CglibSubclassingInstantiationStrategy 扩展了 SimpleInstantiationStrategy,为需要进行方法注入的Bean提供了支持。它利用CGLib类库为 Bean 动态生成子类,在子类中生成方法注入的逻辑,然后使用这个动态生成的子类创建Bean的实例。
InstantiationStrategy 仅负责实例化 Bean的操作,相当于执行Java语言中new的功能,它并不会参与Bean属性的设置工作。所以由InstantiationStrategy返回的Bean实例实际上是一个半成品的Bean实例,属性填充的工作留待BeanWrapper来完成。
5.1.4 BeanWrapper
org.springframework.beans.BeanWrapper 是Spring框架中重要的组件类。BeanWrapper相当于一个代理器,Spring 通过 BeanWrapper 完成 Bean属性的填充工作。在 Bean实例被InstantiationStrategy创建出来之后,容器主控制程序将 Bean实例通过BeanWrapper 包装起来,这是通过调用BeanWrapper#setWrappedInstance(Object obj) 方法完成的。
我们可以看出BeanWrapper还有两个顶级类接口,分别是 PropertyAccessor 和 PropertyEditorRegistry。PropertyAccessor接口定义了各种访问Bean属性的方法,如setPropertyValue(String, Object),setPropertyValue(Property Value)等,而PropertyEditorRegistry是属性编辑器的注册表。所以BeanWrapper实现类BeanWrapperImpl具有三重身份:
- Bean包裹器;
- 属性访问器;
- 属性编辑器注册表。
一个BeanWrapperImpl实例内部封装了两类组件:被封装的目标Bean以及一套用于设置Bean属性的属性编辑器。
要顺利的填充Bean属性,除了目标Bean实例和属性编辑器以外,还需要获取Bean对应的BeanDefinition,它从Spring容器的BeanDefinitionRegistry中直接获取。Spring控程序从BeanDefinition获取Bean属性的配置信息PropertyValue,并使用属性编辑器对配置形式的PropertyValue 进行转换以得到Bean属性的值。对Bean其他属性重复这样的步骤,就可以完成Bean所有属性的注入工作。BeanWrapper在内部是使用到Spring的BeanUtils工具类对Bean进行反射操作,设置属性。
5.2 属性编辑器
- 属性编辑器的主要功能就是将外部的设置值转换为JVM内部的对应类型,所以属性编辑器其实就是一个类型转换器
5.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
- PropertyEditor是属性编辑器的接口,它规定了将外部设置值转化为内部JavaBean属性值的转换接口方法。主要方法说明:
- Object getValue():返回属性的当前值,基本类型被封装成对应的封装类实例;
- void setValue(Object new Value):设置属性的值,基本类型以装类传入;
- String getAsText():将属性对象用一个字符串表示,以便外部属性编辑器能以可视化的方式显示。缺省返回null,表示该属性不能以字符串表示;
- void setAsText(String text):用以字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入;
- String[] getTags():返回表示有效属性值的字符串数组(如Boolean属性对应的有效Tag为true和false),以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示属性没有匹配的字符值有限集合;
- String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值;
BeanInfo
- BeanInfo主要描述了JavaBean哪些属性可以编辑以及对应的属性编辑器,每一个属性对应一个属性描述器PropertyDescriptor。PropertyDescriptor 的构造函数有两个入参:PropertyDescriptor(String propertyName, Class beanClass),其中propertyName为属性名;而beanClass为JavaBean对应的Class。此外PropertyDescriptor还有一个BeanInfo接口最重要的方法就是:PropertyDescriptor[] getPropertyDescriptors(),该方法返回JavaBean的属性描述器数组。
5.2.2 Spring 默认属性编辑器
- BeanWrapperImpl类扩展了PropertyEditorRegistrySupport类,Spring在PropertyEditorRegistrySupport中为常见属性类型提供了默认的属性编辑器:可分为3大类:
类 别 | 说 明 |
---|---|
基础数据类型 | 分为几个小类: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、InputStream、Locale、Properties、Resource[] 和 Url |
- PropertyEditorRegistrySupport 中有两个用于保存属性编辑器的Map类型变量:
- defaultEditors:用于保存默认属性类型的编辑器,元素的键为属性类型,值为对应的属性编辑器实例;
- customEditors:用于保存用户自定义的属性编辑器,元素的键值和defaultEditors相同。
- PropertyEditorRegistrySupport 通过类似以下的代码定义默认属性编辑器:
- this.defaultEditors.pur(char.class, new CharacterEditor(false));
- this.defaultEditors.pur(Character.class, new CharacterEditor(false));
- this.defaultEditors.pur(Locale.class, new LocaleEditor(false));
- this.defaultEditors.pur(Properties.class, newPropertiesEditor(false));
5.2.3 自定义属性编辑器
package com.baobaotao.ditype;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Car {
private int maxSpeed;
private String brand;
private double price;
}
package com.baobaotao.ditype;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class Boss {
private String name;
private Car car = new Car();
@Override
public String toString() {
return "Boss [name=" + name + ", car=" + car + "]";
}
}
- 为Car类型提供一个自定义的属性编辑器,这样,我们就通过字面值为Boss 的car属性提供配置值。
package com.baobaotao.ditype;
import java.beans.PropertyEditorSupport;
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(car);
}
}
<!-- @1 配置自动注册属性编辑器的 CustomEditorConfigurer -->
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<!-- @2-1 属性编辑器对应的属性类型 -->
<entry key="com.baobaotao.ditype.Car">
<!-- @2-2 对应的属性编辑器Bean -->
<bean class="com.baobaotao.ditype.CustomCarEditor" />
</entry>
</map>
</property>
</bean>
<bean id="car" class="com.baobaotao.ditype.Car"/>
<!-- @3 改属性将使用@2 出的属性编辑器完成属性填充操作 -->
<bean id="boss" class="com.baobaotao.ditype.Boss">
<property name="name" value="John" />
<property name="car" value="红旗CA72,200,20000.00" />
</bean>
- 在@1处,我们定义了用于注册自定义属性编辑器的 CustomEditorConfigurer ,Spring 容器将通过反射机制自动调用这个 Bean。CustomEditorConfigurer 通过一个Map属性定义需要自动注册的自定义属性编辑器。在@2处,我们为Car类型指定了一对应属性编辑器的类名。
- @3处的配置,我们原来通过一个< bean >元素标签配置好car Bean,然后再boss的< bean > 中通过ref引用carBean,但是限制我们之间通过value为car属性提供配置。BeanWrapper 再设置boss的car属性时,他就检索自定义属性编辑器的注册表,当发现Car属性类型拥有对应的属性编辑器CustomEditorConfigurer 时,它就会利用 CustomEditorConfigurer 将"红旗CA72,200,20000.00"转换为Car对象。
5.3 使用外部属性文件
-
在进行数据源或邮件服务器等资源的配置时,用户可以直接在Spring配置文件中配置用户名/密码、链接地址等信息。但最好将这些配置信息独立到一个外部属性文件中,并在Spring配置文件中通过形如: u s e r 、 {user}、 user、{password}等占位符引用属性文件中的属性项:好处:
- 减少维护的工作量:在多个应用使用同一资源的情况下,资源的配置信息可以被多个应用共享;
- 使部署更简单:Spring配置文件主要描述应用工程中的Bean,这些配置信息在开发完成后就基本固定下来。但数据源、邮件服务器等资源配置信息却需要在部署时根据需要设定。
-
Spring 为我们提供了一个PropertyPlaceholderConfigurer,它能够使Bean在配置时引用外部属性文件。
5.3.1 使用外部属性文件
使用属性文件
- 原始配置数据源方式
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/hm_dream?characterEncoding=utf8"
p:username="root"
p:password="hm123"/>
- 推荐使用数据源配置方式
- jdbc.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/hm_dream?characterEncoding=utf8
username=root
password=hm123
<?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:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- @1引入jdbc.properties属性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
p:location="classpath:jdbc.properties"
p:fileEncoding="utf-8"/>
<!-- @2通过属性名引用属性值 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${username}"
p:password="${password}"/>
</beans>
PropertyPlaceholderConfigurer
- 除location属性外,PropertyPlaceholderConfigurer还有一些常用的属性,在一些高级应用中,可能会使用到:
- 如果只有一个属性文件,直接使用Location属性指定就可以了,如果是多个属性文件,则可以通过locations属性进行设置,可以像配置List一样配置locations属性;
- fileEncoding:指定属性文件的编码格式;
- order:如果有过个PropertyPlaceholderConfigurer,通过该属性指定优先顺序;
- placeholderPrefix:通过 属 性 名 引 用 属 性 , " {属性名}引用属性," 属性名引用属性,"{"则默认为占位符的前缀,改属性可以根据需求改为其他的前缀符
- palaceholderSuffix:占位符后最,默认为"}"。
使用< context:property-placeholder >引用属性文件
<?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:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<!-- @1引入jdbc.properties属性文件 -->
<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>
<!-- @2通过属性名引用属性值 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${username}"
p:password="${password}"/>
</beans>
- 可以使用context 命名空间定义属性文件,优雅的方式
在基于注解及基于Java类配置中引用属性
<context:component-scan base-package="com.baobaotao.domain"/>
package com.baobaotao.domain;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.Getter;
import lombok.Setter;
@Component
@Setter
@Getter
public class MyDataSource {
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Override
public String toString() {
return "MyDataSource [driverClassName=" + driverClassName + ", url=" + url + ", username=" + username
+ ", password=" + password + "]";
}
}
- @Value 注解可以为Bean注入一个字面值,也可以通过@Value("${propName}")的形式根据属性名注入属性值。由于标注@Configuration的类本身相当于标注了@Component,所以在标注@Configuration 类中引用属性的方式和基于注解配置的引用方式是完全一样的。
5.3.2 使用加密的属性文件
- 对于敏感信息需要加密处理,赋值时则将解密后的信息赋值于属性;加密分为对称加密于非对称加密(区别在于是否可还原)
- PropertyPlaceholderConfigurer继承于PropertyResourceConfigurer类,后者有几个protected 方法,用于属性文件中的属性进行转换处理
- void convertProperties(Properties propos):属性文件中的所有属性值都封装在props中,覆盖此方法,可以对所有的属性值进行转换处理。
- String convertProperty(String propertyName, Stirng propertyValue):在加载属性文件并读取文件中的每个属性时,都会调用此方法进行转换处理。
- String convertPropertyValue(String originalValue):和上一个方法类似;
DES 加密解密工具类
- DESUtils.java DES加密工具类
package com.baobaotao.placeholder.utils;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
public class DESUtils {
//@1对字符串进行DES加密,返回BASE64编码的加密字符串
public static String getEncryptString(String str) {
Encoder base64en = Base64.getEncoder();
try {
byte[] strBytes = str.getBytes("UTF8");
String result = base64en.encodeToString(strBytes);
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//@2 对BASE64编码的加密字符串进行解密,返回解密后的字符串
public static String getDecryptString(String str) {
Decoder base64en = Base64.getDecoder();
try {
byte[] strBytes = base64en.decode(str);
return new String(strBytes,"UTF8");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
System.err.println(getEncryptString("root"));
System.err.println(getEncryptString("hm123"));
System.err.println("root :" + getDecryptString("cm9vdA=="));
System.err.println("hm123 :" + getDecryptString("aG0xMjM="));
}
}
- jdbc.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/hm_dream?characterEncoding=utf8
username=cm9vdA==
password=aG0xMjM=
package com.baobaotao.placeholder.utils;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
//@1 继承PropertyPlaceholderConfigurer定义支持密文版属性的属性配置器
public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer{
private String[] encryptPropNames = {"username","password"};
//@2 对特定属性的属性值进行转换
protected String convertProperty(String propertyName, String propertyValue) {
if(isEncryptProp(propertyName)) {
String decryptValue = DESUtils.getDecryptString(propertyValue);
System.out.println(decryptValue);
return decryptValue;
}else {
return propertyValue;
}
}
//@3 判断是否需要进行解密的属性
private boolean isEncryptProp(String propertyName) {
for (String encryptPropName : encryptPropNames) {
if(encryptPropName.equals(propertyName)) {
return true;
}
}
return false;
}
}
- xml配置
<bean class="com.baobaotao.placeholder.utils.EncryptPropertyPlaceholderConfigurer"
p:location="jdbc.properties"
p:fileEncoding="utf-8"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${username}"
p:password="${password}" />
5.3.3 属性文件自身的引用
- Spring 即允许在Bean定义中通过 p r o p N a m e 引 用 属 性 值 , 还 允 许 在 属 性 文 件 中 使 用 {propName}引用属性值,还允许在属性文件中使用 propName引用属性值,还允许在属性文件中使用{propName},实现属性之间的引用:
dbName=hm_dream?characterEncoding=utf8
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/${dbName}
username=cm9vdA==
password=aG0xMjM=
- url属性通过 ${dbName} 引用另一个属性的值。
5.4 引用 Bean 的属性值
- 在@1 处的方法中,可以更改代码,从数据库中获取相应的属性信息。(这里直接赋值)
package com.baobaotao.placeholder.beanprop;
import javax.sql.DataSource;
public class SysConfig {
private int sessionTimeout;
private int maxTabPageNum;
private DataSource dataSource;
//@1 模拟从数据库中获取配置并设置相应的属性
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;
}
}
- 访问数据库获取配置属性的前提时链接到数据库,故,我们还得使用外部属性文件配置数据数据库的链接信息。然后,通过sysConfig的initFromDB()方法访问数据库,获取应用系统的配置信息,并将其保存在sysConfig的属性中。
- 其他需要访问应用系统配置信息的Bean即可通过#{beanName.propName}的表达式引用sysConfig Bean 的属性了,如@2所示
- 在基于注解基于Java类配置的Bean中,可以通过@Value("#{beanName.propName}")的注解形式引用Bean的属性值:
package com.baobaotao.placeholder.beanprop;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ApplicationManager {
@Value("#{sysConfig.sessionTimeout}")
private int sessionTimeout;
@Value("#{sysConfig.maxTabPageNum}")
private int maxTabPageNum;
public int getSessionTimeout() {
return sessionTimeout;
}
public void setSessionTimeout(int sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public int getMaxTabPageNum() {
return maxTabPageNum;
}
public void setMaxTabPageNum(int maxTabPageNum) {
this.maxTabPageNum = maxTabPageNum;
}
}
- 在XML配置文件,我们先将SysConfig定义为一个Bean,定义数据源时即可通过#{beanName.propName}的方式引用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:util="http://www.springframework.org/schema/util"
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.3.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.baobaotao" />
<!-- <context:property-placeholder
location="jdbc.properties"/> -->
<bean class="com.baobaotao.placeholder.utils.EncryptPropertyPlaceholderConfigurer"
p:location="jdbc.properties"
p:fileEncoding="utf-8"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${username}"
p:password="${password}" />
<!-- @1 通过initFromDB 方法从数据源中获取配置属性值 -->
<bean id="sysConfig" class="com.baobaotao.placeholder.beanprop.SysConfig"
init-method="initFromDB"
p:dataSource-ref="dataSource"/>
<!-- @2 引用Bean的属性值 -->
<bean class="com.baobaotao.placeholder.beanprop.ApplicationManager"
p:maxTabPageNum="#{sysConfig.maxTabPageNum}"
p:sessionTimeout="#{sysConfig.sessionTimeout}" />
</beans>
5.5 国际化信息
5.5.1 基础知识
- 常见一些语言和国家/地区的标准代码
语言 | 代码 | 国家/地区代码 | 代号 |
---|---|---|---|
中文 | zh | 中国大陆 | CN |
英文 | en | 中国台湾 | TW |
法语 | fr | 中国香港 | HK |
德语 | de | 英国 | EN |
日语 | ja | 美国 | US |
韩语 | ko | 加拿大 | CA |
Locale
- java.util.Locale 是表示语言和国家/地区信息的本地化类,它是创建国际化应用的基础。
- 创建本地化对象的示例:
//@1 带有语言和国家/地区信息的本地化对象
Locale locale1 = new Locale("zh", "CN");
//@2 只有语言信息的本地化对象
Locale locale2 = new Locale("zh");
//@3 等同于Locale("zh","CN")
Locale locale3 = Locale.CHINA;
//@4 等同于Locale("zh")
Locale locale4 = Locale.CHINESE;
//@5 获取本地系统默认的本地化对象
Locale locale5 = Locale.getDefault();
public static void main(String[] args) {
Locale locale = new Locale("zh", "CN");
NumberFormat currFmt = NumberFormat.getCurrencyInstance(locale);
double amt = 123456.78;
System.out.println(currFmt.format(amt));
Locale locale6 = new Locale("en", "US");
Date date = new Date();
DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM,locale6);
System.out.println(df.format(date));
}
- “¥123,456.78”,“Dec 25, 2019” 为输出结果,NumberFormat按本地化的方式对货币金额进行格式化操作;
ResourceBoundle
-
国际化资源文件的命名的规范规定资源名称采用以下的方式进行命名
- < 资源名 >< 语言代码 >< 国家/地区代码 >.properties
-
其中,语言代码和国家/地区代码都是可选的。< 资源名 >.properties命名的国际资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。< 资源名 >_< 语言代码 >.properties命名的国际化资源文件是某一语言默认的资源文件,即某个本地化类型在系统中找不到精确匹配的资源文件,将采用相应语言默认的资源文件。
-
Java为我们提供了用于加载本地资源文件的方便类java.util.ResourceBoundel。ResourceBoundel为加载及访问资源文件提供便捷的操作,如:
-
resource_en_US.properties
greeting.common=How are you!
greeting.morning=Good morning!
greeting.afternoon=Good Afternoon!
- resource_zh_CN.properties
greeting.common=\u60A8\u597D\uFF01
greeting.morning=\u65E9\u4E0A\u597D\uFF01
greeting.afternoon=\u4E0B\u5348\u597D\uFF01
- 测试代码
ResourceBundle rb1 = ResourceBundle.getBundle("resource", Locale.US);
ResourceBundle rb2 = ResourceBundle.getBundle("resource", Locale.CHINA);
System.out.println("us:" + rb1.getString("greeting.common"));
System.out.println("cn:" + rb2.getString("greeting.common"));
* us:How are you!
* cn:您好!
- ResourceBundle 在加载资源时,如果指定的本地化资源文件不存在,它按以下顺序尝试加载其他的资源:本地系统默认本地化对象对应的资源 -> 默认的资源。
在资源文件中使用格式化串
- resource_en_US.properties
greeting.common=How are you!{0}, today is {1}
greeting.morning=Good morning!{0}, now is {1 time shot}
greeting.afternoon=Good Afternoon!{0} new is {1 date long}
- resource_zh_CN.properties
greeting.common=\u60A8\u597D\uFF01{0}, \u73B0\u5728\u662F {1}
greeting.morning=\u65E9\u4E0A\u597D\uFF01
greeting.afternoon=\u4E0B\u5348\u597D\uFF01
- 测试代码
ResourceBundle rb1 = ResourceBundle.getBundle("resource", Locale.US);
ResourceBundle rb2 = ResourceBundle.getBundle("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.common"), Locale.CHINA).format(params);
System.out.println("us:" + str1);
System.out.println("cn:" + str2);
* us:How are you!John, today is 12/27/19 3:31 PM
* cn:您好!John, 现在是 19-12-27 下午3:31
5.5.2 MessageSource
- Spring 定义了访问国际化信息的MessageSource接口,并提供了几个易用的实现类。接口方法:
- String getMessage(String code, Object[] args, String defaultMessage, Locale locale) cod 表示国际化资源中的属性名,args用于传递格式化子字符串占位符所用的运行期参数;当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;locale表示本地化对象;
- String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException 于上面的方法类似,只不过在找不到的资源中对应的属性名时,直接抛出NoSuchMessageException 异常
- String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageExceptioin MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一接口方法相同。
MessageSource 的类结构
- MessageSource 分别被 HierarchicalMessageSource 和 ApplicationContext接口扩展。
- HierarchicalMessageSource 接口添加了两个方法,建立父子层级的MessageSource 结构,类似于我们前面所介绍的 HierarchicalBeanFactory。该接口的 setParentMessageSource(MessageSource parent)方法用于设置父MessageSource,而 getParentMessageSource() 方法用于返回父 MessageSource。
- HierarchicalMessageSource 接口最重要的两个实现类是 ResourceBundleMessageSource和ReloadResourceBundeleMessageSource。他们基于Java的ResourceBundle 基础类实现,允许仅通过资源名加载国际化资源。ReloadableResourceBundelMessageSource提供了定时刷新功能,允许在不重启系统的情况下,更新资源信息。StaticMessageSource 主要用于程序测试,它允许通过编程的方式提供国际化信息。而DelegationMessage Source 是为方便操作父MessageSource 而提供的代理类。
ResourceBundleMessageSource
- bean.xml配置
<bean id="myResource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>resource</value>
</list>
</property>
</bean>
- 测试
String[] configs = {"beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
MessageSource ms = (MessageSource)ctx.getBean("myResource");
Object[] params = {"John", new GregorianCalendar().getTime()};
String str1 = ms.getMessage("greeting.common", params, Locale.US);
String str2 = ms.getMessage("greeting.common", params, Locale.CHINA);
System.out.println("us:" + str1);
System.out.println("cn:" + str2);
- 控制台打印:us:How are you!John, today is 12/27/19 4:33 PM - cn:您好!John, 现在是 19-12-27 下午4:33
ReloadableResourceBundleMessageSource
<bean id="myResource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>resource</value>
</list>
</property>
<property name="cacheSeconds" value="5"></property>
</bean>
String[] configs = {"beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
MessageSource ms = (MessageSource)ctx.getBean("myResource");
Object[] params = {"John", new GregorianCalendar().getTime()};
for (int i = 0; i < 2; i++) {
String str1 = ms.getMessage("greeting.common", params, Locale.US);
System.out.println(str1);
Thread.currentThread().sleep(20000);
}
5.5.3 容器级国际化信息资源
<bean id="myResource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>resource</value>
</list>
</property>
</bean>
String[] configs = {"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.common", params, Locale.CHINA);
System.out.println(str1);
System.out.println(str2);
5.6 国际化信息
- Spring 的 ApplicationContext能够发布事件并且允许注册相应的事件监听器,因此它拥有一套完美的事件发布和监听机制。
- 事件源:事件的生产者,任何一个EventObject都必须拥有一个事件源;
- 事件监听器注册表:组件或框架的事件监听器不可能飘在水里悬在空中,而必须由所依存。也就是说组件或框架必须提供一个地方保存事件监听器,这便是事件监听器注册表。一个事件监听器注册到组件或框架中,其实就是保存在监听器注册表里,当组件和框架中的事件源产生事件是就会将事件通知这些位于注册表中的监听器;
- 事件广播器: 它是事件和事件监听器沟通桥梁,负责把事件通知给事件监听器
5.6.1 Spring 事件类结构
事件类
- ApplicationEvent 的唯一构造函数是 ApplicationEvent(Object source),通过source指定事件源,它的两个子类分别是:
- ApplicationContextEvent:容器事件,拥有4个子类分别表示容器启动、刷新、停止及关闭的事件;
- RequestHandlerEvent:这是一个与Web应用相关的事件,当一个HTTP请求被处理后,产生该事件。只有在web.xml中定义了DispatcherServlet时才会产生该事件。拥有两个子类,分别代表Servlet及Portlet的请求事件。
事件监听器接口
- ApplicationListener 接口只定义了一个方法:onApplicationEvent(E event),该方法接受ApplicationEvent事件对象,该方法中编写事件的响应处理逻辑。而SmartApplicationListener 它定义了两个方法:
- boolean supportsEventType(Class< ? extends ApplicationEvent > eventType): 指定监听器支持那种类型的容器事件,即它只会对该类型的事件做出响应;
- boolean supportsSourceType(Class< ? > sourceType):该方法指定监听器仅对何种事件源对象做出响应
事件广播器
- 当发生容器事件时,容器主控程序将调用事件广播器将事件通知给注册表中的事件监听器,监听器分别对事件进行响应。Spring为事件广播器定义了接口。
5.6.2 解构 Spring 事件体系的具体实现
- Spring 在 ApplicationContext 接口的抽象实现类 AbstractApplicationContext 中完成了事件体系的搭建。AbstractApplicationContext 拥有一个ApplicationEventMulticaster 成员变量,applicationEventMulticaster 提供了容器监听器的注册表。AbstractApplicationContext在refresh()这个容器启动方法种通过以下三个步骤搭建了事件的基础设施。
//@5 初始化应用上下文事件广播器
initApplicationEventMulticaster();
//@7 注册事件监听器
registerListeners();
//@9 完成刷新并发布容器刷新事件
finishRefresh();
- 首先,@5 处,Spring初始化事件的广播器。用户可以在配置文件中为容器定义一个自定义的广播器,只要实现ApplicationEventMulticaster就可以了,Spring会通过反射的机制将器注册成容器的事件广播器,如果没有找到配置的外部事件广播器,Spring自动使用SimpleApplicationEventMulticater作为事件广播器。
- 在@7处,Spring将根据反射机制,从BeanDefinitionRegistry中找出所有实现org.springframework.context.ApplicationListener的Bean,将他们注册为容器的事件监听器,实际的操作就是将其添加到实际广播器所提供的接听器注册表中
- 在@9处,容器启动完成,调用实际发布接口向容器中所有的监听器发布事件,在publishEvent()内部,我们可以道道Spring委托ApplicationEventMulticaster将事件通知给监听器。
5.6.3 一个实例
package com.baobaotao.event;
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 this.to;
}
}
- 它直接扩展ApplicationContextEvent,事件对象除source外,还具有一个代表发送目的地to属性。
package com.baobaotao.event;
import org.springframework.context.ApplicationListener;
//事件监听器 MailSendListener 负责监听MailSendEvent事件
public class MailSendListener implements ApplicationListener<MailSendEvent>{
//@1 对MailSendEvent事件进行处理
@Override
public void onApplicationEvent(MailSendEvent event) {
MailSendEvent mse = (MailSendEvent)event;
System.out.println("MailSendEvent:向" + mse.getTo() + "发送完一封邮件");
}
}
- MailSenderListener 直接实现ApplicationListener 接口,在接口方法通过instanceof操作符判断事件的类型,仅对MailSendEvent类型的事件进行处理
package com.baobaotao.event;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
//MailSender 要有有发布事件的能力,必须事件ApplicationContextAware接口
public class MailSender implements ApplicationContextAware{
private ApplicationContext ctx;
//@1 ApplicationContextAware 的接口方法,以便容器启动时注入示例
@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);
//@2 向容器中的所有事件监听器发送事件
ctx.publishEvent(mse);
}
}
- spring的配置文件中,仅需要如下配置:
<bean class="com.baobaotao.event.MailSendListener"/>
<bean id="mailSender" class="com.baobaotao.event.MailSender"/>
package com.baobaotao.anno;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.baobaotao.event.MailSender;
public class Test {
public static void main(String[] args) throws InterruptedException {
String[] configs = {"beans.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
MailSender mailSender = (MailSender)ctx.getBean("mailSender");
mailSender.sendMail("aaa@bbb.com");
}
}
- MailSender:模拟发送邮件 & MailSendEvent:向aaa@bbb.com发送完一封邮件