文章很长,我相信你看完之后一定会有所收货
资源管理
资源管理是Spring的一个核心的基础功能,不过在说Spring的资源管理之前,先来简单说一下Java中的资源管理。
Java资源管理
Java中的资源管理主要是通过
java.net.URL
来实现的,通过URL的
openConnection
方法可以对资源打开一个连接,通过这个连接读取资源的内容。
资源不仅仅指的是网络资源,还可以是本地文件、一个jar包等等。
1、来个Demo
举个例子,比如你想到访问
www.baidu.com
这个百度首页网络资源,那么此时就可以这么写
public class JavaResourceDemo {
public static void main(String[] args) throws IOException {
//构建URL 指定资源的协议为http协议
URL url = new URL("http://www.baidu.com");
//打开资源连接
URLConnection urlConnection = url.openConnection();
//获取资源输入流
InputStream inputStream = urlConnection.getInputStream();
//通过hutool工具类读取流中数据
String content = IoUtil.read(new InputStreamReader(inputStream));
System.out.println(content);
}
}
解释一下上面代码的意思:
-
首先构建一个URL,指定资源的访问协议为http协议
-
通过URL打开一个资源访问连接,然后获取一个输入流,读取内容
运行结果
成功读取到百度首页的数据。
当然,也可以通过URL访问本地文件资源,在创建URL的时候只需要指定协议类型为
file://
和文件的路径就行了
URL url = new URL("file://" + "文件的路径");
这种方式这里我就不演示了。
其实这种方式实际上最终也是通过
FileInputStream
来读取文件数据的,不信你可以自己debug试试。
2、原理
每种协议的URL资源都需要一个对应的一个URLStreamHandler来处理。
URLStreamHandler
比如说,
http://
协议有对应的URLStreamHandler的实现,
file://
协议的有对应的URLStreamHandler的实现。
Java除了支持
http://
和
file://
协议之外,还支持其它的协议,如下图所示:
对于的URLStreamHandler如下图所示
当在构建URL的时候,会去解析资源的访问协议,根据访问协议找到对应的URLStreamHandler的实现。
当然,除了Java本身支持的协议之外,我们还可以自己去扩展这个协议,大致只需要两步即可:
-
实现URLConnection,可以通过这个连接读取资源的内容
-
实现URLStreamHandler,通过URLStreamHandler可以获取到URLConnection
不过需要注意的是,URLStreamHandler的实现需要放在
sun.net.www.protocol.协议名称
包下,类名必须是
Handler
,这也是为什么截图中的实现类名都叫
Handler
的原因。
当然如果不放在指定的包下也可以,但是需要实现
java.net.URLStreamHandlerFactory
接口。
对于扩展我就不演示了,如果你感兴趣可以自行谷歌一下。
Spring资源管理
虽然Java提供了标准的资源管理方式,但是Spring并没有用,而是自己搞了一套资源管理方式。
1、资源抽象
在Spring中,资源大致被抽象为两个接口
-
Resource:可读资源,可以获取到资源的输入流
-
WritableResource:读写资源,除了资源输入流之外,还可以获取到资源的输出流
Resource
Resource接口继承了InputStreamSource接口,而InputStreamSource接口可以获取定义了获取输入流的方法
WritableResource
WritableResource
继承了
Resource
接口,可以获取到资源的输出流,因为有的资源不仅可读,还可写,就比如一些本地文件的资源,往往都是可读可写的
Resource
的实现很多,这里我举几个常见的:
-
FileSystemResource:读取文件系统的资源
-
UrlResource:前面提到的Java的标准资源管理的封装,底层就是通过URL来访问资源
-
ClassPathResource:读取classpath路径下的资源
-
ByteArrayResource:读取静态字节数组的数据
比如,想要通过Spring的资源管理方式来访问前面提到百度首页网络资源,就可以这么写
//构建资源
Resource resource = new UrlResource("http://www.baidu.com");
//获取资源输入流
InputStream inputStream = resource.getInputStream();
如果是一个本地文件资源,那么除了可以使用UrlResource,也可以使用FileSystemResource,都是可以的。
2、资源加载
虽然
Resource
有很多实现,但是在实际使用中,可能无法判断使用具体的哪个实现,所以Spring提供了
ResourceLoader
资源加载器来根据资源的类型来加载资源。
ResourceLoader
通过
getResource
方法,传入一个路径就可以加载到对应的资源,而这个路径不一定是本地文件,可以是任何可加载的路径。
ResourceLoader
有个唯一的实现
DefaultResourceLoader
比如对于上面的例子,就可以通过
ResourceLoader
来加载资源,而不用直接new具体的实现了
//创建ResourceLoader
ResourceLoader resourceLoader = new DefaultResourceLoader();
//获取资源
Resource resource = resourceLoader.getResource("http://www.baidu.com");
除了
ResourceLoader
之外,还有一个
ResourcePatternResolver
可以加载资源
ResourcePatternResolver
继承了
ResourceLoader
通过
ResourcePatternResolver
提供的方法可以看出,他可以加载多个资源,支持使用通配符的方式,比如
classpath*:
,就可以加载所有classpath的资源。
ResourcePatternResolver
只有一个实现
PathMatchingResourcePatternResolver
PathMatchingResourcePatternResolver
3、小结
到这就讲完了Spring的资源管理,这里总结一下本节大致的内容
Java的标准资源管理:
Spring的资源管理:
-
资源抽象:Resource 、WritableResource
-
资源加载:ResourceLoader 、ResourcePatternResolver
Spring的资源管理在Spring中用的很多,比如在SpringBoot中,
application.yml
的文件就是通过ResourceLoader加载成Resource,之后再读取文件的内容的。
环境
上一节末尾举的例子中提到,SpringBoot配置文件是通过ResourceLoader来加载配置文件,读取文件的配置内容
那么当配置文件都加载完成之后,这个配置应该存到哪里,怎么能够读到呢?
这就引出了Spring框架中的一个关键概念,环境,它其实就是用于管理应用程序配置的。
1、Environment
Environment就是环境抽象出来的接口
Environment继承PropertyResolver
public interface PropertyResolver {
boolean containsProperty(String key);
String getProperty(String key);
T getProperty(String key, Class targetType);
T getRequiredProperty(String key, Class targetType) throws IllegalStateException;
String resolvePlaceholders(String text);
}
如上是PropertyResolver提供的部分方法,这里简单说一下上面方法的作用
-
getProperty(String key)
,很明显是通过配置的key获取对应的value值
-
getProperty(String key, Class
targetType)
,这是获取配置,并转换成对应的类型,比如你获取的是个字符串的
"true"
,这里就可以给你转换成布尔值的
true
,具体的底层实现留到下一节讲
-
resolvePlaceholders(String text)
,这类方法可以处理
${...}
占位符,也就是先取出
${...}
占位符中的key,然后再通过key获取到值
所以Environment主要有一下几种功能:
来个demo
先在
application.yml
的配置文件中加入配置
测试代码如下
@SpringBootApplication
public class EnvironmentDemo {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(EnvironmentDemo.class, args);
//从ApplicationContext中获取到ConfigurableEnvironment
ConfigurableEnvironment environment = applicationContext.getEnvironment();
//获取name属性对应的值
String name = environment.getProperty("name");
System.out.println("name = " + name);
}
}
启动应用,获取到ConfigurableEnvironment对象,再获取到值
ConfigurableEnvironment是Environment子接口,通过命名也可以知道,他可以对Environment进行一些功能的配置。
运行结果:
name = 三友的java日记
2、配置属性源PropertySource
PropertySource是真正存配置的地方,属于配置的来源,它提供了一个统一的访问接口,使得应用程序可以以统一的方式获取配置获取到属性。
PropertySource
来个简单demo
public class PropertySourceDemo {
public static void main(String[] args) {
Map source = new HashMap<>();
source.put("name", "三友的java日记");
PropertySource
简单说一下上面代码的意思
-
首先创建了一个map,就是配置来源,往里面添加了一个配置key-value
-
创建了一个PropertySource,使用的实现是MapPropertySource,需要传入配置map,所以最终获取到属性不用想就知道是从map中获取的
最后成获取到属性
除了MapPropertySource之外,还有非常多的实现
PropertySource实现
比如CommandLinePropertySource,它其实就封装了通过命令启动时的传递的配置参数
既然PropertySource才是真正存储配置的地方,那么Environment获取到的配置真正也就是从PropertySource获取的,并且他们其实是一对多的关系
其实很好理解一对多的关系,因为一个应用程序的配置可能来源很多地方,比如在SpringBoot环境底下,除了我们自定义的配置外,还有比如系统环境配置等等,这些都可以通过Environment获取到
当从Environment中获取配置的时候,会去遍历所有的PropertySource,一旦找到配置key对应的值,就会返回
所以,如果有多个PropertySource都含有同一个配置项的话,也就是配置key相同,那么获取到的配置是从排在前面的PropertySource的获取的
这就是为什么,当你在配置文件配置username属性时获取到的却是系统变量username对应的值,因为系统的PropertySource排在配置文件对应的PropertySource之前
3、SpringBoot是如何解析配置文件
SpringBoot是通过PropertySourceLoader来解析配置文件的
load方法的第二个参数就是我们前面提到的资源接口Resource
通过Resource就可以获取到配置文件的输入流,之后就可以读取到配置文件的内容,再把配置文件解析成多个PropertySource,之后把PropertySource放入到Environment中,这样我们就可以通过Environment获取到配置文件的内容了。
PropertySourceLoader默认有两个实现,分别用来解析
properties
和
yml
格式的配置文件
此时,上面的图就可以优化成这样
类型转换
在上一节介绍Environment时提到了它的
getProperty(String key, Class
targetType)
可以将配置的字符串转换成对应的类型,那么他是如何转换的呢?
这就跟本文要讲的Spring类型转换机制有关了
1、类型转换API
Spring类型转换主要涉及到以下几个api:
接下来我会来详细介绍这几个api的原理和他们之间的关系。
1.1、PropertyEditor
PropertyEditor并不是Spring提供的api,而是JDK提供的api,他的主要作用其实就是将String类型的字符串转换成Java对象属性值。
public interface PropertyEditor {
void setValue(Object value);
Object getValue();
String getAsText();
void setAsText(String text) throws java.lang.IllegalArgumentException;
}
就拿项目中常用的
@Value
来举例子,当我们通过
@Value
注解的方式将配置注入到字段时,大致步骤如下图所示:
-
-
根据
@Value
配置的key调用Environment的
resolvePlaceholders(String text)
方法,解析占位符,找到配置文件中对应的值
-
调用PropertyEditor将对应的值转换成注入的属性字段类型,比如注入的字段类型是数字,那么就会将字符串转换成数字
在转换的过程中,Spring会先调用PropertyEditor的setAsText方法将字符串传入,然后再调用getValue方法获取转换后的值。
Spring提供了很多PropertyEditor的实现,可以实现字符串到多种类型的转换
在这么多实现中,有一个跟我们前面提到的Resource有关的实现
ResourceEditor
,它是将字符串转换成Resource对象
ResourceEditor
也就是说,可以直接通过@Value的方式直接注入一个Resource对象,就像下面这样
@Value("http://www.baidu.com")
private Resource resource;
其实归根到底,底层也是通过ResourceLoader来加载的,这个结论是不变的。
所以,如果你想知道@Value到底支持注入哪些字段类型的时候,看看PropertyEditor的实现就可以了,当然如果Spring自带的都不满足你的要求,你可以自己实现PropertyEditor,比如把String转成Date类型,Spring就不支持。
1.2、Converter
由于PropertyEditor局限于字符串的转换,所以Spring在后续的版本中提供了叫Converter的接口,他也用于类型转换的,相比于PropertyEditor更加灵活、通用
Converter
Converter是个接口,泛型S是被转换的对象类型,泛型T是需要被转成的类型。
同样地,Spring也提供了很多Converter的实现
这些主要包括日期类型的转换和String类型转换成其它的类型
1.3、GenericConverter
GenericConverter也是类型转换的接口
这个接口的主要作用是可以处理带有泛型类型的转换,主要的就是面向集合数组转换操作,从Spring默认提供的实现就可以看出
那Converter跟GenericConverter有什么关系呢?
这里我举个例子,假设现在需要将将源集合
Collection
转换成目标集合
Collection
假设现在有个String转换成Date类型的Converter,咱就叫StringToDateConverter,那么整个转换过程如下:
-
首先会找到GenericConverter的一个实现CollectionToCollectionConverter,从名字也可以看出来,是将一个几个转换成另一个集合
-
-
根据目标集合泛型Date,找到StringToDateConverter,将String转换成Date,将转换的Date存到一个新的集合
-
所以通过这就可以看出Converter和GenericConverter其实是依赖关系
1.4、ConversionService
对于我们使用者来说,不论是Converter还是GenericConverter,其实都是类型转换的,并且类型转换的实现也很多,所以Spring为了方便我们使用Converter还是GenericConverter,提供了一个门面接口ConversionService
ConversionService
我们可以直接通过ConversionService来进行类型转换,而不需要面向具体的Converter或者是GenericConverter
ConversionService有一个基本的实现GenericConversionService
GenericConversionService
同时GenericConversionService还实现了ConverterRegistry的接口
ConverterRegistry提供了对Converter和GenericConverter进行增删改查的方法。
ConverterRegistry
这样就可以往ConversionService中添加Converter或者是GenericConverter了,因为最终还是通过Converter和GenericConverter来实现转换的
但是我们一般不直接用GenericConversionService,而是用DefaultConversionService或者是ApplicationConversionService(SpringBoot环境底下使用)
因为DefaultConversionService和ApplicationConversionService在创建的时候,会添加很多Spring自带的Converter和GenericConverter,就不需要我们手动添加了。
1.5、TypeConverter
TypeConverter其实也是算是一个门面接口,他也定义了转换方法
他是将PropertyEditor和ConversionService进行整合,方便我们同时使用PropertyEditor和ConversionService
convertIfNecessary方法会去调用PropertyEditor和ConversionService进行类型转换,值得注意的是,
优先使用PropertyEditor进行转换,如果没有找到对应的PropertyEditor,会使用ConversionService进行转换
TypeConverter有个简单的实现SimpleTypeConverter,这里来个简单的demo
public class TypeConverterDemo {
public static void main(String[] args) {
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
//设置ConversionService
typeConverter.setConversionService(DefaultConversionService.getSharedInstance());
//将字符串"true"转换成Boolean类型的true
Boolean b = typeConverter.convertIfNecessary("true", Boolean.class);
System.out.println("b = " + b);
}
}
这里需要注意,ConversionService需要我们手动设置,但是PropertyEditor不需要,因为SimpleTypeConverter默认会去添加PropertyEditor的实现。
小结
到这就讲完了类型转换的常见的几个api,这里再简单总结一下:
-
PropertyEditor:String转换成目标类型
-
-
GenericConverter:用于处理泛型的转换,主要用于集合
-
ConversionService:门面接口,内部会调用Converter和GenericConverter
-
TypeConverter:门面接口,内部会调用PropertyEditor和ConversionService
画张图来总结他们之间的关系
前面在举@Value的例子时说,类型转换是根据PropertyEditor来的,其实只说了一半,因为底层实际上是根据TypeConverter来转换的,所以@Value类型转换时也能使用ConversionService类转换,所以那张图实际上应该这么画才算对
2、Environment中到底是如何进行类型转换的?
这里我们回到开头提到的话题,Environment中到底是如何进行类型转换的,让我们看看Environment类的接口体系
Environment有个子接口ConfigurableEnvironment中,前面也提到过
它继承了ConfigurablePropertyResolver接口
而ConfigurablePropertyResolver有一个
setConversionService
方法
所以从这可以看出,Environment底层实际上是通过ConversionService实现类型转换的
这其实也就造成了一个问题,因为ConversionService和PropertyEditor属于并列关系,那么就会导致Environment无法使用PropertyEditor来进行类型转换,也就会丧失部分Spring提供的类型转换功能,就比如无法通过Environment将String转换成Resource对象,因为Spring没有实现String转换成Resource的Converter
当然你可以自己实现一个String转换成Resource的Converter,然后添加到ConversionService,之后Environment就支持String转换成Resource了。
数据绑定
上一节我们讲了类型转换,而既然提到了类型转换,那么就不得不提到数据绑定了,他们是密不可分的,因为在数据绑定时,往往都会伴随着类型转换,
数据绑定的意思就是将一些配置属性跟我们的Bean对象的属性进行绑定。
不知你是否记得,在远古的ssm时代,我们一般通过xml方式声明Bean的时候,可以通过
来设置Bean的属性
<bean class="com.sanyou.spring.core.basic.User">
<property name="username" value="三友的java日记"/>
bean>
@Data
public class User {
private String username;
}
然后Spring在创建User的过程中,就会给
username
属性设置为
三友的java日记
。
这就是数据绑定,将
三友的java日记
绑定到username这个属性上。
数据绑定的核心api主要包括以下几个:
1、PropertyValues
这里我们先来讲一下PropertyValue(注意没有s)
顾明思议,PropertyValue就是就是封装了属性名和对应的属性值,它就是数据绑定时属性值的来源。
以前面的提到的xml创建Bean为例,Spring在启动的时候会去解析xml中的
标签,然后将
name
和
value
封装成PropertyValue
当创建User这个Bean的时候,到了属性绑定的阶段的时候,就会取出PropertyValue,设置到User的username属性上。
而PropertyValues,比PropertyValue多了一个s,也就是复数的意思,所以其实PropertyValues本质上就是PropertyValue的一个集合
因为一个Bean可能有多个属性配置,所以就用PropertyValues来保存。
2、BeanWrapper
BeanWrapper其实就数据绑定的核心api了,因为在Spring中涉及到数据绑定都是通过BeanWrapper来完成的,比如前面提到的Bean的属性的绑定,就是通过BeanWrapper来的
BeanWrapper是一个接口,他有一个唯一的实现BeanWrapperImpl。
先来个demo
public class BeanWrapperDemo {
public static void main(String[] args) {
//创建user对象
User user = new User();
//创建BeanWrapper对象,把需要进行属性绑定的user对象放进去
BeanWrapper beanWrapper = new BeanWrapperImpl(user);
//进行数据绑定,将三友的java日记这个属性值赋值到username这个属性上
beanWrapper.setPropertyValue(new PropertyValue("username", "三友的java日记"));
System.out.println("username = " + user.getUsername());
}
}
结果
成功获取到,说明设置成功
BeanWrapperImpl也间接实现了TypeConverter接口
当然底层还是通过前面提到的ConversionService和PropertyEditor实现的
所以当配置的类型跟属性的类型不同时,就可以对配置的类型进行转换,然后再绑定到属性上
这里简单说一下数据绑定和@Value的异同,因为这两者看起来好像是一样的,但实际还是有点区别的
相同点:
-
两者都会涉及到类型转换,@Value和数据绑定都会将值转换成目标属性对应的类型,并且都是通过TypeConverter来转换的
不同点:
-
1、发生时机不同,@Value比数据绑定更早,当@Value都注入完成之后才会发生数据绑定(属性赋值)
-
2、属性赋值方式不同,@Value是通过反射来的,而是数据绑定是通过setter方法来的,如果没有setter方法,属性是没办法绑定的
3、DataBinder
DataBinder也是用来进行数据绑定的,它的底层也是间接通过BeanWrapper来实现的数据绑定的
但是他相比于BeanWrapper多了一些功能,比如在数据绑定之后,可以对数据校验,比如可以校验字段的长度等等
说到数据校验,是不是想到了SpringMVC中的参数校验,通过@Valid配合一些诸如@NotBlank、@NotNull等注解,实现优雅的参数校验。
其实SpringMVC的参数校验就是通过DataBinder来的,所以DataBinder其实在SpringMVC中用的比较多,但是在Spring中确用的很少。
如果你有兴趣,可以翻一下SpringMVC中关于请求参数处理的HandlerMethodArgumentResolver的实现,里面有的实现会用到DataBinder(WebDataBinder)来进行数据请求参数跟实体类的数据绑定、类型转换、数据校验等等。
不知道你有没有注意过,平时写接口的时候,前端传来的参数String类型的时间字符串无法通过Spring框架本身转换成Date类型,有部分原因就是前面提到的Spring没有相关的Converter实现
总的来说,数据绑定在xml配置和SpringMVC中用的比较多的,并且数据绑定也是Spring Bean生命周期中一个很重要的环节。
泛型处理
Spring为了方便操作和处理泛型类型,提供了一个强大的工具类——ResolvableType。
泛型处理其实是一块相对独立的东西,因为它就只是一个工具类,只还不过这个工具类在Spring中却是无处不在!
ResolvableType提供了有一套灵活的API,可以在运行时获取和处理泛型类型等信息。
ResolvableType
接下来就通过一个案例,来看一看如何通过ResolvableType快速简单的获取到泛型的
首先我声明了一个MyMap类,继承HashMap,第一个泛型参数是Integer类型,第二个泛型参数是List类型,List的泛型参数又是String
public class MyMap extends HashMap<Integer, List<String>> {
}
接下来就来演示一下如何获取到HashMap的泛型参数以及List的泛型参数
第一步,先来通过
ResolvableType#forClass
方法创建一个MyMap类型对应的ResolvableType
//创建MyMap对应的ResolvableType
ResolvableType myMapType = ResolvableType.forClass(MyMap.class);
因为泛型参数是在父类HashMap中,所以我们得获取到父类HashMap对应的ResolvableType,通过
ResolvableType#getSuperType()
方法获取
//获取父类HashMap对应的ResolvableType
ResolvableType hashMapType = myMapType.getSuperType();
接下来需要获取HashMap的泛型参数对应的ResolvableType类型,可以通过
ResolvableType#getGeneric(int... indexes)
就可以获取指定位置的泛型参数ResolvableType,方法参数就是指第几个位置的泛型参数,从0开始
比如获取第一个位置的对应的ResolvableType类型
//获取第一个泛型参数对应的ResolvableType
ResolvableType firstGenericType = hashMapType.getGeneric(0);
现在有了第一个泛型参数的ResolvableType类型,只需要通过
ResolvableType#resolve()
方法就可以获取到ResolvableType类型对应的class类型,这样就可以获取到一个泛型参数的class类型
//获取第一个泛型参数对应的ResolvableType对应的class类型,也就是Integer的class类型
Class> firstGenericClass = firstGenericType.resolve();
如果你想获取到HashMap第二个泛型参数的泛型类型,也就是List泛型类型就可以这么写
//HashMap第二个泛型参数的对应的ResolvableType,也就是List
ResolvableType secondGenericType = hashMapType.getGeneric(1);
//HashMap第二个泛型参数List的第一个泛型类型String对应的ResolvableType
ResolvableType secondFirstGenericType = secondGenericType.getGeneric(0);
//这样就获取到了List的泛型类型String
Class> secondFirstGenericClass = secondFirstGenericType.resolve();
从上面的演示下来可以发现,其实每变化一步,其实就是获取对应泛型或者是父类等等对应的ResolvableType,父类或者是泛型参数又可能有泛型之类的,只需要一步一步获取就可以了,当需要获取到具体的class类型的时候,通过
ResolvableType#resolve()
方法就行了。
除了上面提到的通过
ResolvableType#forClass
方法创建ResolvableType之外,还可以通过一下几个方法创建:
-
forField(Field field)
:获取字段类型对应的ResolvableType
-
forMethodReturnType(Method method)
:获取方法返回值类型对应的ResolvableType
-
forMethodParameter(Method method, int parameterIndex)
:获取方法某个位置方法参数对应的ResolvableType
-
forConstructorParameter(Constructor> constructor, int parameterIndex)
:获取构造方法某个构造参数对应的ResolvableType
通过上面解释可以看出,对于一个类方法参数,方法返回值,字段等等都可以获取到对应的ResolvableType
国际化
国际化(Internationalization,简称i18n)也是Spring提供的一个核心功能,它其实也是一块相对独立的功能。
所谓的国际化,其实理解简单点就是对于不同的地区国家,输出的文本内容语言不同。
Spring的国际化其实主要是依赖Java中的国际化和文本处理方式。
1、Java中的国际化
Locale
Locale是Java提供的一个类,它可以用来标识不同的语言和地区,如en_US表示美国英语,zh_CN表示中国大陆中文等。
目前Java已经穷举了很多国家的地区Locale。
我们可以使用Locale类获取系统默认的Locale,也可以手动设置Locale,以适应不同的语言环境。
ResourceBundle
ResourceBundle是一个加载本地资源的一个类,他可以根据传入的Locale不同,加载不同的资源。
来个demo
首先准备资源文件,资源文件通常是.properties文件,文件名命名规则如下:
basename_lang_country.properties
basename无所谓,叫什么都可以,而lang和country是从Locale中获取的。
举个例子,我们看看英语地区的Locale
从上图可以看出,英语Locale的lang为en,country为空字符串,那么此时英语地区对应资源文件就可以命名为:basename_en.properties,由于country为空字符串,可以省略
中国大陆Locale如下图
此时文件就可以命为:basename_zh_CN.properties
好了,现在既然知道了命名规则,我们就创建两个文件,basename就叫message,一个英语,一个中文,放在classpath路径下
中文资源文件:message_zh_CN.properties,内容为:
name=三友的java日记
英文资源文件:message_en.properties,内容为:
name=sanyou's java diary
有了文件之后,就可以通过
ResourceBundle#getBundle(String baseName,Locale locale)
方法来获取获取ResourceBundle
-
第一个参数baseName就是我们的文件名中的basename,对于我们的demo来说,就是message
-
第二个参数就是地区,根据地区的不同加载不同地区的文件
测试一下
public class ResourceBundleDemo {
public static void main(String[] args) {
//获取ResourceBundle,第一个参数baseName就是我们的文件名称,第二个参数就是地区
ResourceBundle chineseResourceBundle = ResourceBundle.getBundle("message", Locale.SIMPLIFIED_CHINESE);
//根据name键取值
String chineseName = chineseResourceBundle.getString("name");
System.out.println("chineseName = " + chineseName);
ResourceBundle englishResourceBundle = ResourceBundle.getBundle("message", Locale.ENGLISH);
String englishName = englishResourceBundle.getString("name");
System.out.println("englishName = " + englishName);
}
}
运行结果
其实运行结果可以看出,其实是成功获取了,只不过中文乱码了,这主要是因为ResourceBundle底层其实编码是
ISO-8859-1
,所以会导致乱码。
解决办法最简单就是把中文用Java Unicode序列来表示,之后就可以读出中文了了,比如
三友的java日记
用Java Unicode序列表示为
\u4e09\u53cb\u7684java\u65e5\u8bb0
除了这种方式之外,其实还可以继承ResourceBundle内部一个Control类
Control
重写newBundle方法
newBundle
newBundle是创建ResourceBundle对应核心方法,重写的时候你就可以随心所欲让它支持其它编码方式。
有了新的Control之后,获取ResourceBundle时只需要通过
ResourceBundle#getBundle(String baseName, Locale targetLocale,Control control)
方法指定Control就可以了。
Spring实际上就是通过这种方式扩展,支持不同编码的,后面也有提到。
MessageFormat
MessageFormat顾明思议就是把消息格式化。它可以接收一条包含占位符的消息模板,并根据
提供的参数
替换
占位符
,生成最终的消息。
MessageFormat对于将动态值插入到消息中非常有用,如欢迎消息、错误消息等。
先来个Demo
public class MessageFormatDemo {
public static void main(String[] args) {
String message = MessageFormat.format("你好:{0}", "张三"
);
System.out.println("message = " + message);
}
}
解释一下上面这段代码:
-
你好:{0}
其实就是前面提到的消息的模板,
{0}
就是
占位符
,中间的0代表消息格式化的时候将
提供的参数
第一个参数替换占位符的值
-
张三
就是提供的参数,你可以写很多个,但是我们的demo只会取第一个参数,因为是
{0}
所以输出结果为:
message = 你好:张三
成功格式化消息。
2、Spring国际化
Spring提供了一个国际化接口MessageSource
MessageSource
他有一个基于ResourceBundle + MessageFormat的实现ResourceBundleMessageSource
ResourceBundleMessageSource
他的本质可以在资源文件存储
消息的模板
,然后通过MessageFormat来替换
占位符
,MessageSource的getMessage方法就可以传递具体的参数
来个demo
现在模拟登录欢迎语句,对于不同的人肯定要有不同的名字,所以资源文件需要存模板,需要在不同的资源文件加不同的模板
中文资源文件:message_zh_CN.properties
welcome=您好:{0}
英文资源文件:message_en.properties
welcome=hello:{0}
占位符,就是不同人不同名字
测试代码
public class MessageSourceDemo {
public static void main(String[] args) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
//Spring已经扩展了ResourceBundle的Control,支持资源文件的不同编码方式,但是需要设置一下
messageSource.setDefaultEncoding("UTF-8");
//添加 baseName,就是前面提到的文件中的basename
messageSource.addBasenames("message");
//中文,传个中文名字
String chineseWelcome = messageSource.getMessage("welcome", new Object[]{"张三"}, Locale.SIMPLIFIED_CHINESE);
System.out.println("chineseWelcome = " + chineseWelcome);
//英文,英语国家肯定是英文名
String englishWelcome = messageSource.getMessage("welcome", new Object[]{"Bob"}, Locale.ENGLISH);
System.out.println("englishWelcome = " + englishWelcome);
}
}
运行结果
chineseWelcome = 您好:张三
englishWelcome = hello:Bob
成功根据完成不同国家资源的加载和模板消息的格式化。
小结
这里来简单总结一下这一小节说的内容
-
-
ResourceBundle:根据不同国家的Locale,加载对应的资源文件,这个资源文件的命名需要遵守
basename_lang_country.properties
命名规范
-
MessageFormat:其实就是一个文本处理的方式,他可以解析模板,根据参数替换模板的占位符
-
MessageSource:Spring提供的国际化接口,其实他底层主要是依赖Java的ResourceBundle和MessageFormat,资源文件存储模板信息,MessageFormat根据MessageSource方法的传参替换模板中的占位符
BeanFactory
我们知道Spring的核心就是IOC和AOP,而BeanFactory就是大名鼎鼎的IOC容器,他可以帮我们生产对象。
1、BeanFactory接口体系
BeanFactory本身是一个接口
BeanFactory
从上面的接口定义可以看出从可以从BeanFactory获取到Bean。
他也有很多子接口,不同的子接口有着不同的功能
-
-
-
-
AutowireCapableBeanFactory
ListableBeanFactory
ListableBeanFactory
从提供的方法可以看出,提供了一些获取集合的功能,比如有的接口可能有多个实现,通过这些方法就可以获取这些实现对象的集合。
HierarchicalBeanFactory
HierarchicalBeanFactory
从接口定义可以看出,可以获取到父容器,说明BeanFactory有子父容器的概念。
ConfigurableBeanFactory
ConfigurableBeanFactory
从命名可以看出,可配置BeanFactory,所以可以对BeanFactory进行配置,比如截图中的方法,可以设置我们前面提到的类型转换的东西,这样在生成Bean的时候就可以类型属性的类型转换了。
AutowireCapableBeanFactory
提供了自动装配Bean的实现、属性填充、初始化、处理获取依赖注入对象的功能。
比如@Autowired最终就会调用
AutowireCapableBeanFactory#resolveDependency
处理注入的依赖。
其实从这里也可以看出,Spring在BeanFactory的接口设计上面还是基于不同的职责进行接口的划分,其实不仅仅是在BeanFactory,前面提到的那些接口也基本符合这个原则。
2、BeanDefinition及其相关组件
BeanDefinition
BeanDefinition是Spring Bean创建环节中很重要的一个东西,它封装了Bean创建过程中所需要的元信息。
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
//设置Bean className
void setBeanClassName(@Nullable String beanClassName);
//获取Bean className
@Nullable
String getBeanClassName();
//设置是否是懒加载
void setLazyInit(boolean lazyInit);
//判断是否是懒加载
boolean isLazyInit();
//判断是否是单例
boolean isSingleton();
}
如上代码是BeanDefinition接口的部分方法,从这方法的定义名称可以看出,一个Bean所创建过程中所需要的一些信息都可以从BeanDefinition中获取,比如这个Bean的class类型,这个Bean是否是懒加载,这个Bean是否是单例的等等,因为有了这些信息,Spring才知道要创建一个什么样的Bean。
读取BeanDefinition
读取BeanDefinition大致分为以下几类
-
-
ClassPathBeanDefinitionScanner
BeanDefinitionReader
BeanDefinitionReader
BeanDefinitionReader可以通过
loadBeanDefinitions(Resource resource)
方法来加载BeanDefinition,方法参数就是我们前面说的资源,比如可以将Bean定义在xml文件中,这个xml文件就是一个资源
BeanDefinitionReader的相关实现:
-
XmlBeanDefinitionReader:读取xml配置的Bean
-
PropertiesBeanDefinitionReader:读取properties文件配置的Bean,是的,你没看错,Bean可以定义在properties文件配置中
-
AnnotatedBeanDefinitionReader:读取通过注解定义的Bean,比如@Lazy注解等等,AnnotatedBeanDefinitionReader不是BeanDefinitionReader的实现,但是作用是一样的
ClassPathBeanDefinitionScanner
这个作用就是扫描指定包下通过@Component及其派生注解(@Service等等)注解定义的Bean,其实就是@ComponentScan注解的底层实现
ClassPathBeanDefinitionScanner这个类其实在很多其它框架中都有使用到,因为这个类可以扫描指定包下,生成BeanDefinition,对于那些需要扫描包来生成BeanDefinition来说,用的很多
比如说常见的MyBatis框架,他的注解@MapperScan可以扫描指定包下的Mapper接口,其实他也是通过继承ClassPathBeanDefinitionScanner来扫描Mapper接口的
BeanDefinitionRegistry
这个从命名就可以看出,是BeanDefinition的注册中心,也就是用来保存BeanDefinition的。
提供了BeanDefinition的增删查的功能。
讲到这里,就可以用一张图来把前面提到东西关联起来
-
通过BeanDefinitionReader或者是ClassPathBeanDefinitionScanner为每一个Bean生成一个BeanDefinition
-
BeanDefinition生成之后,添加到BeanDefinitionRegistry中
-
当从BeanFactory中获取Bean时,会从BeanDefinitionRegistry中拿出需要创建的Bean对应的BeanDefinition,根据BeanDefinition的信息来生成Bean
-
当生成的Bean是单例的时候,Spring会将Bean保存到SingletonBeanRegistry中,也就是平时说的三级缓存中的第一级缓存中,以免重复创建,需要使用的时候直接从SingletonBeanRegistry中查找
3、BeanFactory核心实现
前面提到的BeanFactory体系都是一个接口,那么BeanFactory的实现类是哪个类呢?
BeanFactory真正底层的实现类,其实就只有一个,那就是DefaultListableBeanFactory这个类,这个类以及父类真正实现了BeanFactory及其子接口的所有的功能。
并且接口的实现上可以看出,他也实现了BeanDefinitionRegistry,也就是说,在底层的实现上,其实BeanFactory跟BeanDefinitionRegistry的实现是同一个实现类。