Spring容器高级主题(精通Spring+4.x++企业应用开发实战 五)

15 篇文章 1 订阅

本章的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.");
、}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值