在前面的文章我们学习过了Spring中的类型转换以及格式化,对于这两个功能一个很重要的应用场景就是应用于我们在XML中配置的Bean的属性值上,如下:

<bean class="com.dmz.official.converter.service.IndexService" name="indexService">
<property name="name" value="dmz"/>
<!-- age 为int类型-->
<property name="age" value="1"/>
</bean>

在上面这种情况下,我们从XML中解析出来的值类型肯定是String类型,而对象中的属性为int类型,当Spring将配置中的数据应用到Bean上时,就调用了我们的类型转换器完成了String类型的字面值到int类型的转换。

那么除了在上面这种情况中使用了类型转换,还有哪些地方用到了呢?对了,就是本文要介绍的数据绑定–DataBinder

DataBinder

UML类图

从上图我们可以看到,DataBinder实现了PropertyEditorRegistry以及TypeConverter,所以它拥有类型转换的能力。

我们通过下面两张图对比下BeanWrapperImplDataBinder

  1. DataBinder

  1. BeanWrapperImpl



可以发现跟BeanWrapperImpl不同的是,它并没有通过继承某一个类来实现类型转换,而是通过组合的方式(DataBinder持有一个SimpleTypeConverter的引用,通过这个SimpleTypeConverter完成了类型转换)

使用示例

public class Main {
public static void main(String[] args) throws BindException {
Person person = new Person();
DataBinder binder = new DataBinder(person, "person");
// 创建用于绑定到对象上的属性对(属性名称,属性值)
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "fsx");
pvs.add("age", 18);
binder.bind(pvs);
System.out.println(person);
// 程序打印:Person{name='dmz', age=18}
}
} class Person {
String name; int age; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} @Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

在上面的例子中要明确一点,Person中必须要提供setter方法(getter方法可以不提供,因为我们只是设置值),实际上DataBinder底层也是同样也是采用了Java的内省机制(关于Java的内省机制如果不了解的话,请参考《Spring官网阅读十四》),而内省只会根据setter方法以及getter来设置或者获取Bean中的属性。

源码分析

可能有细心的同学会发现,DataBinder是位于我们的org.springframework.validation包下的,也就是说它跟Spring中的校验也有关系,不过校验相关的内容不是我们本节要探讨的,本文我们只探讨DataBinder跟数据绑定相关的内容。

DataBinder所在的包结构如下:

OK,明确了要分析的点之后,接下来我们就看看到底数据是如何绑定到我们的对象上去的,核心代码如下:

bind方法

第一步,我们是直接调用了bind方法来完成,其代码如下:

public void bind(PropertyValues pvs) {
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ?
(MutablePropertyValues) pvs : new MutablePropertyValues(pvs));
// 最终调用了doBind方法,如果大家对Spring代码有所了解的话,会发现Spring中有很多doXXX的方法
// 形如doXXX这种命名方式的方法往往就是真正“干活”的代码,对于本例来说,肯定就是它来完成数据绑定的
doBind(mpvs);
}

doBind方法

protected void doBind(MutablePropertyValues mpvs) {
// 校验
checkAllowedFields(mpvs);
// 校验
checkRequiredFields(mpvs);
// 真正进行数据绑定
applyPropertyValues(mpvs);
}

跟校验相关的代码不在本文的探讨范围内,如果感兴趣的话可以关注我接下来的文章。我们现在把注意力放在applyPropertyValues这个方法,方法名直译过来的意思是--------应用属性值,就是将方法参数中的属性值应用到Bean上,也就是进行属性绑定。不知道大家看到这个方法名是否熟悉,如果对源码有一定了解的话,一定会知道Spring在完成属性注入的过程中调用了一个同名的方法,关于这个方法稍后我会带大家找一找然后做个比较,现在我们先看看doBind方法中applyPropertyValues干了什么

applyPropertyValues方法

protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// 逻辑非常简单,获取一个属性访问器,然后直接通过属性访问器将属性值设置上去
// IgnoreUnknownFields:忽略在Bean中找不到的属性
// IgnoreInvalidFields:忽略找到,但是没有访问权限的值
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// 省略部分代码.....
}
}

这段代码主要做了两件事

获取一个属性访问器

getPropertyAccessor(),获取一个属性访问器,关于属性访问器在《Spring官网阅读十四》也有介绍,这里我再做一些补充

可以看到,PropertyAccessor(也就是我们所说的属性访问器)只有两个实现类

  • 第一个,BeanWrapperImpl
  • 第二个,DirectFieldAccessor

那么这两个有什么区别呢?第一个我们已经知道了,它是基于内省来实现的,所以BeanWrapperImpl肯定是基于getter,setter方法来实现对属性的操作的。第二个从名字上我们可以猜测,它估计是直接通过反射来获取字段的,也就是说,不需要提供setter/getter方法。大家可以自行做个测试,这里我就直接给结论了

  • BeanWrapperImpl,基于内省,依赖getter/setter方法
  • DirectFieldAccessor,基于反射,不需要提供getter/setter方法

那么接下来,我们思考一个问题,DataBinder中的getPropertyAccessor()访问的是哪种类型的属性访问器呢?其实结合我们之前那个使用的示例就很容易知道,它肯定返回的是一个基于内省机制实现的属性访问器,并且它就是返回了一个BeanWrapperImpl。代码如下:

// 1.获取一个属性访问器,可以看到,是通过getInternalBindingResult()方法返回的一个对象来获取的
// 那么getInternalBindingResult()做了什么呢?
protected ConfigurablePropertyAccessor getPropertyAccessor() {
return getInternalBindingResult().getPropertyAccessor();
} // 2.getInternalBindingResult()又调用了一个initBeanPropertyAccess(),从名字上来看,就是用来初始化属性访问器的,再看看这个方法干了啥
protected AbstractPropertyBindingResult getInternalBindingResult() {
if (this.bindingResult == null) {
initBeanPropertyAccess();
}
return this.bindingResult;
} // 3.调用了一个createBeanPropertyBindingResult,创建了一个对象,也就是通过创建的这个对象返回了一个属性访问器,那么这个对象是什么呢?接着往下看
public void initBeanPropertyAccess() {
Assert.state(this.bindingResult == null,
"DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods");
this.bindingResult = createBeanPropertyBindingResult();
} // 4.可以发现创建的这个对象就是一个BeanPropertyBindingResult
protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {
BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(),
getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());
// .....
return result;
} // 5.跟踪这个对象的getPropertyAccessor()方法,发现就是返回了一个beanWrapper
// 现在明朗了吧,dataBinder最终也是依赖于beanWrapper
public final ConfigurablePropertyAccessor getPropertyAccessor() {
if (this.beanWrapper == null) {
this.beanWrapper = createBeanWrapper();
this.beanWrapper.setExtractOldValueForEditor(true);
this.beanWrapper.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
this.beanWrapper.setAutoGrowCollectionLimit(this.autoGrowCollectionLimit);
}
return this.beanWrapper;
}

我们可以思考一个问题,为什么Spring在实现数据绑定的时候不采用DirectFieldAccessor而是BeanWrapperImpl呢?换言之,为什么不直接使用反射而使用内省呢?

我个人的理解是:反射容易打破Bean的封装性,基于内省更安全。Spring在很多地方都不推荐使用反射的方式,比如我们在使用@Autowired注解进行字段注入的时候,编译器也会提示,”Field injection is not recommended “,不推荐我们使用字段注入,最好将@Autowired添加到setter方法上。

通过属性访问器直接set属性值

这段代码十分繁琐,如果不感兴趣可以直接跳过,整个核心就是获取到对象中的setter方法,然后反射调用。

1、setPropertyValues

此方法位于org.springframework.beans.AbstractPropertyAccessor#setPropertyValues(org.springframework.beans.PropertyValues, boolean, boolean)

public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
throws BeansException { List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// 核心代码就是这一句
setPropertyValue(pv);
}
// ......
}
2、setPropertyValue(String,Object)

此方法位于org.springframework.beans.AbstractNestablePropertyAccessor#setPropertyValue(java.lang.String, java.lang.Object)

public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
AbstractNestablePropertyAccessor nestedPa;
try {
// 这里是为了解决嵌套属性的情况,比如一个person对象中,包含一个dog对象,dog对象中有一个name属性
// 那么我们可以通过dog.name这种方式来将一个名字直接绑定到person中的dog上
// 与此同时,我们不能再使用person的属性访问器了,因为使用dog的属性访问器,这里就是返回dog的属性访问器
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
}
// ....... // PropertyTokenHolder是什么呢?例如我们的Person对象中有一个List<String> name的属性,
// 那么我们在绑定时,需要对List中的元素进行赋值,所有我们会使用name[0],name[1]这种方式来进行绑定,
// 而PropertyTokenHolder中有三个属性,其中actualName代表name,canonicalName代表整个表达式name[0],而key则代表0这个下标位置
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
// 最后通过属性访问器设置值
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
}

对上面的结论进行测试,测试代码如下:

public class Main {
public static void main(String[] args) throws BindException {
Person person = new Person();
DataBinder binder = new DataBinder(person, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("dog.dogName","dawang");
pvs.add("name[0]", "dmz0");
pvs.add("name[1]", "dmz1");
pvs.add("age", 18);
binder.bind(pvs);
System.out.println(person);
}
} class Dog {
// 省略getter/setter方法
String dogName;
} class Person {
// 省略getter/setter方法
List<String> name;
Dog dog;
int age;
}

在方法的如下位置添加条件断点(propertyName.equals("dog.dogName")):

启动main方法,并开始调试,程序进入如下结果:

我们关注红框标注的三个位置

  1. 第一个红框,标注了当前属性访问器所对应的对象为Dog
  2. 第二个红框,这是一个特殊的AbstractNestablePropertyAccessor,专门用于处理嵌套属性这种情况的,所以它包含了嵌套的路径
  3. 第三个红框,标注了这个嵌套的属性访问器的根对象是Person

同样的,按照这种方式我们也可以对Person中的List<String> name属性进行调试,可以发现PropertyTokenHolder就是按照上文所说的方式进行存储数据的,大家可以自行调试,我这里就不在演示了。

3、setPropertyValue(PropertyTokenHolder,PropertyValue)

这个方法是对上面方法的重载,其代码仍然位于org.springframework.beans.AbstractNestablePropertyAccessor中,代码如下:

protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
if (tokens.keys != null) {
// 前面已经说过了,keys其实就是下标数组,如果你能看到这里的话,肯定会有一个疑问,为什么需要一个数组呢?考虑这种属性List<List<String>> list,这个时候为了表示它,是不是就要list[0][0]这种方式了呢?这个时候就需要用数组存储了,因为一个属性需要多个下标表示
processKeyedProperty(tokens, pv);
}
else {
// 我们关注这个方法即可,解析完PropertyTokenHolder后,最终都要调用这个方法
processLocalProperty(tokens, pv);
}
}
4、processLocalProperty

代码位于:org.springframework.beans.AbstractNestablePropertyAccessor#processLocalProperty

	private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
// .... 省略部分代码
Object oldValue = null;
try {
Object originalValue = pv.getValue();
Object valueToApply = originalValue;
// 判断成立,代表需要进行类型转换,conversionNecessary为null或者为true都成立
if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
// 判断成立,代表已经转换过了
if (pv.isConverted()) {
valueToApply = pv.getConvertedValue();
}
else {
if (isExtractOldValueForEditor() && ph.isReadable()) {
try {
oldValue = ph.getValue();
}
// .... 省略部分代码
}
// 类型转换的部分,之前已经分析过了,这里就没什么好讲的了
valueToApply = convertForProperty(
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
}
pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
}
// 核心代码就这一句
ph.setValue(valueToApply);
}
// .... 省略部分代码
}
}
5、setValue

代码位置:org.springframework.beans.BeanWrapperImpl.BeanPropertyHandler#setValue

最终进入到BeanWrapperImpl中的一个内部类BeanPropertyHandler中,方法代码如下:

public void setValue(final @Nullable Object value) throws Exception {
final Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ?
((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() :
this.pd.getWriteMethod());
// .... 省略部分代码
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(getWrappedInstance(), value);
}
}

代码就是这么的简单,内省获取这个属性的writeMethod,其实就是setter方法,然后直接反射调用


在了解了DataBinder之后,我们再来学习跟基于DataBinder实现的子类

DataBinder的子类

子类概览

可以看到DataBinder的直接子类只有一个WebDataBinder,从名字上我们就能知道,这个类主要作用于Web环境,从而也说明了数据绑定主要使用在Web环境中。

WebDataBinder

这个接口是为了Web环境而设计的,但是并不依赖任何的Servlet API。它主要的作用是作为一个基类让其它的类继承,例如ServletRequestDataBinder

代码分析

public class WebDataBinder extends DataBinder {

    // 这两个字段的详细作用见下面的两个方法checkFieldDefaults/checkFieldMarkers
public static final String DEFAULT_FIELD_MARKER_PREFIX = "_";
public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!";
@Nullable
private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX;
@Nullable
private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_PREFIX; // ......省略构造方法及一些getter/setter方法 @Override
protected void doBind(MutablePropertyValues mpvs) {
checkFieldDefaults(mpvs);
checkFieldMarkers(mpvs);
// 没有对数据绑定做什么扩展,只是单纯的调用了父类的方法,也就是DataBinder的方法
super.doBind(mpvs);
} // 若你给定的PropertyValue的属性名是以!开头的,例如,传入的属性名称为:!name,属性值为:dmz
// 那就做处理如下:
// 如果Bean中的name属性是可写的并且mpvs不存在name属性,那么向mpvs中添加一个属性对,其中属性名称为name,值为dmz
// 然后将!name这个属性值对从mpvs中移除
// 其实这里就是说你可以使用!来给个默认值。比如!name表示若找不到name这个属性的时,就取它的值,
// 也就是说你request里若有穿!name保底,也就不怕出现null值啦
protected void checkFieldDefaults(MutablePropertyValues mpvs) {
String fieldDefaultPrefix = getFieldDefaultPrefix();
if (fieldDefaultPrefix != null) {
PropertyValue[] pvArray = mpvs.getPropertyValues();
for (PropertyValue pv : pvArray) {
if (pv.getName().startsWith(fieldDefaultPrefix)) {
String field = pv.getName().substring(fieldDefaultPrefix.length());
// 属性可写,并且当前要绑定的属性值中不包含这个去除了“!”的属性名
if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
// 添加到要绑定到Bean中的属性值集合里
mpvs.add(field, pv.getValue());
}
mpvs.removePropertyValue(pv);
}
}
}
} // 处理_的步骤
// 若传入的字段以“_”开头,以属性名称:“_name”,属性值dmz为例
// 如果Bean中的name字段可写,并且mpvs没有这个值
// 那么对Bean中的name字段赋默认的空值,比如Boolean类型默认给false,数组给空数组[],集合给空集合,Map给空map
// 然后移除mpvs中的“_name”
// 相当于说,当我们进行数据绑定时,传入“_name”时,如果没有传入具体的属性值,Spring会为我们赋默认的空值
// 前提是必须以“_”开头
protected void checkFieldMarkers(MutablePropertyValues mpvs) {
String fieldMarkerPrefix = getFieldMarkerPrefix();
if (fieldMarkerPrefix != null) {
PropertyValue[] pvArray = mpvs.getPropertyValues();
for (PropertyValue pv : pvArray) {
if (pv.getName().startsWith(fieldMarkerPrefix)) {
String field = pv.getName().substring(fieldMarkerPrefix.length());
if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
Class<?> fieldType = getPropertyAccessor().getPropertyType(field);
mpvs.add(field, getEmptyValue(field, fieldType));
}
mpvs.removePropertyValue(pv);
}
}
}
} @Nullable
protected Object getEmptyValue(String field, @Nullable Class<?> fieldType) {
return (fieldType != null ? getEmptyValue(fieldType) : null);
} // 根据不同的类型给出空值
@Nullable
public Object getEmptyValue(Class<?> fieldType) {
try {
// 布尔值,默认false
if (boolean.class == fieldType || Boolean.class == fieldType) {
return Boolean.FALSE;
}
// 数组,默认给一个长度为0的符合要求的类型的数组
else if (fieldType.isArray()) {
return Array.newInstance(fieldType.getComponentType(), 0);
}
// 集合,也是给各种空集合,Set/List等等
else if (Collection.class.isAssignableFrom(fieldType)) {
return CollectionFactory.createCollection(fieldType, 0);
}
else if (Map.class.isAssignableFrom(fieldType)) {
return CollectionFactory.createMap(fieldType, 0);
}
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to create default value - falling back to null: " + ex.getMessage());
}
}
// Default value: null.
return null;
} // 这个方法表示,支持将文件作为属性绑定到对象的上
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
multipartFiles.forEach((key, values) -> {
if (values.size() == 1) {
MultipartFile value = values.get(0);
if (isBindEmptyMultipartFiles() || !value.isEmpty()) {
mpvs.add(key, value);
}
}
else {
mpvs.add(key, values);
}
});
} }

可以看到相对于父类DataBinder,它主要做了以下三点增强

  1. 可以手动为Bean中的属性提供默认值(提供“!”开头的属性名称)
  2. 可以让容器对属性字段赋上某些空值(提供“_”开头的属性名称)
  3. 可以将文件绑定到Bean上

使用示例

public class WebDataBinderMain {
public static void main(String[] args) {
A a = new A();
WebDataBinder webDataBinder = new WebDataBinder(a);
MutablePropertyValues propertyValues = new MutablePropertyValues();
// propertyValues.add("name","I AM dmz");
propertyValues.add("!name","dmz");
propertyValues.add("_list","10");
webDataBinder.bind(propertyValues);
System.out.println(a);
// 程序打印:
// A{name='dmz', age=0, multipartFile=null, list=[], no_list=null}
// 如果注释打开,程序打印:A{name='I AM dmz', age=0, multipartFile=null, list=[], no_list=null}
}
} // 省略getter/setter方法
class A{
String name;
int age;
MultipartFile multipartFile;
List<String> list;
List<String> no_list;
}
}

ServletRequestDataBinder

相比于父类,明确的依赖了Servlet API,会从ServletRequest中解析出参数,然后绑定到对应的Bean上,同时还能将文件对象绑定到Bean上。

代码分析

public class ServletRequestDataBinder extends WebDataBinder {

	public void bind(ServletRequest request) {
// 从request中解析除MutablePropertyValues,用于后面的数据绑定
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); // 如果是一个MultipartRequest,返回一个MultipartRequest
// 上传文件时,都是使用MultipartRequest来封装请求
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); // 说明这个请求对象是一个MultipartRequest
if (multipartRequest != null) { // 调用父类方法绑定对应的文件
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
// 留给子类扩展使用
addBindValues(mpvs, request); // 调用WebDataBinder的doBind方法进行数据绑定
doBind(mpvs);
} protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
}
//....... 省略部分代码 }

ExtendedServletRequestDataBinder

代码分析

public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
// ....省略构造方法 // 这个类在ServletRequestDataBinder复写了addBindValues方法,在上面我们说过了,本身这个方法也是ServletRequestDataBinder专门提供了用于子类复写的方法
@Override
@SuppressWarnings("unchecked")
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
// 它会从request获取名为HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE的属性
// 我们在使用@PathVariable的时候,解析出来的参数就放在request中的这个属性上,然后由ExtendedServletRequestDataBinder完成数据绑定
Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
if (uriVars != null) {
uriVars.forEach((name, value) -> {
if (mpvs.contains(name)) {
if (logger.isWarnEnabled()) {
logger.warn("Skipping URI variable '" + name +
"' because request contains bind value with same name.");
}
}
else {
mpvs.addPropertyValue(name, value);
}
});
}
} }

WebExchangeDataBinder

这个绑定器用于web-flux响应式编程中,用于完成Mono类型的数据的绑定,最终绑定的动作还是调用的父类的doBind方法

MapDataBinder

它位于org.springframework.data.web是和Spring-Data相关,专门用于处理targetMap类型的目标对象的绑定,它并非一个public类,Spring定义的用于内部使用的类

WebRequestDataBinder

它是用于处理Spring自己定义的org.springframework.web.context.request.WebRequest的,旨在处理和容器无关的web请求数据绑定

总结

上面关于Web相关的数据绑定我没有做详细的介绍,毕竟当前的学习阶段的重点是针对Spring-Framework,对于Web相关的东西目前主要以了解为主,后续在完成SpringMVC相关文章时会对这部分做详细的介绍。

本文主要介绍了DataBinder的整个体系,重点学习了它的数据绑定相关的知识,但是不要忘记了,它本身也可以实现类型转换的功能。实际上,我们也可以这样理解,之所以要让DataBinder具备类型转换的能力,正是为了更好的完成数据绑定。

前文我们也提到了,DataBinder位于org.springframework.validation,所以它必定跟校验有关,具体有什么关系呢?下篇文章将详细介绍及分析Spring中的数据校验,它也将是整个SpringFramwork官网阅读笔记的最后一篇文章!

Spring官网阅读(十六)Spring中的数据绑定的更多相关文章

  1. Spring官网阅读(六)容器的扩展点(一)BeanFactoryPostProcessor

    之前的文章我们已经学习完了BeanDefinition的基本概念跟合并,其中多次提到了容器的扩展点,这篇文章我们就开始学习这方面的知识.这部分内容主要涉及官网中的1.8小结.按照官网介绍来说,容器的扩 ...

  2. Spring官网阅读 | 总结篇

    接近用了4个多月的时间,完成了整个<Spring官网阅读>系列的文章,本文主要对本系列所有的文章做一个总结,同时也将所有的目录汇总成一篇文章方便各位读者来阅读. 下面这张图是我整个的写作大 ...

  3. Spring官网阅读(十八)Spring中的AOP

    文章目录 什么是AOP AOP中的核心概念 切面 连接点 通知 切点 引入 目标对象 代理对象 织入 Spring中如何使用AOP 1.开启AOP 2.申明切面 3.申明切点 切点表达式 excecu ...

  4. Spring官网阅读(十七)Spring中的数据校验

    文章目录 Java中的数据校验 Bean Validation(JSR 380) 使用示例 Spring对Bean Validation的支持 Spring中的Validator 接口定义 UML类图 ...

  5. Spring官网阅读(十一)ApplicationContext详细介绍(上)

    文章目录 ApplicationContext 1.ApplicationContext的继承关系 2.ApplicationContext的功能 Spring中的国际化(MessageSource) ...

  6. Spring官网阅读(三)自动注入

    上篇文章我们已经学习了1.4小结中关于依赖注入跟方法注入的内容.这篇文章我们继续学习这结中的其他内容,顺便解决下我们上篇文章留下来的一个问题-----注入模型. 文章目录 前言: 自动注入: 自动注入 ...

  7. Spring官网阅读(一)容器及实例化

    从今天开始,我们一起过一遍Spring的官网,一边读,一边结合在路神课堂上学习的知识,讲一讲自己的理解.不管是之前关于动态代理的文章,还是读Spring的官网,都是为了之后对Spring的源码做更全面 ...

  8. Spring官网阅读(二)(依赖注入及方法注入)

    上篇文章我们学习了官网中的1.2,1.3两小节,主要是涉及了容器,以及Spring实例化对象的一些知识.这篇文章我们继续学习Spring官网,主要是针对1.4小节,主要涉及到Spring的依赖注入.虽 ...

  9. Spring官网阅读(十五)Spring中的格式化(Formatter)

    文章目录 Formatter 接口定义 继承树 注解驱动的格式化 AnnotationFormatterFactory FormatterRegistry 接口定义 UML类图 FormattingC ...

随机推荐

  1. JS中的offsetWidth/offsetHeight/offsetTop/offsetLeft、clientWidth/clientHeight/clientTop/clientLeft、scrollWidth/scrollHeight/scrollTop/scrollLeft

    这是一组非常容易弄混的参数!都是描述某个盒子元素的宽度.高度以及上或左的距离偏移量. 1. offsetWidth / offsetHeight(不包括外边距) offsetWidth:返回元素的宽度 ...

  2. 如何使用python,才能像人民日报的“点亮”武汉景点

    如何使用python,才能像人民日报的“点亮”武汉景点 前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:Allen P ...

  3. 数据类型、运算符、Scanner的使用

              一.常见的基本数据类型      数值型  byte(最小,2字节)      short(4字节) int (默认 8字节)    long(16字节)      浮点型   f ...

  4. Java读源码之CountDownLatch

    前言 相信大家都挺熟悉 CountDownLatch 的,顾名思义就是一个栅栏,其主要作用是多线程环境下,让多个线程在栅栏门口等待,所有线程到齐后,栅栏打开程序继续执行. 案例 用一个最简单的案例引出 ...

  5. IN612 IN612L蓝牙5.0 SoC芯片替换NRF52832/NRF52840

    IN612L是美国公司INPLAY的SOC产品系列之一,具有多模协同2.4G无线协议栈,支持2.4G私有协议栈以及蓝牙5.0全协议栈的SOC芯片.如2mbps高数据速率模式,125kbps/500kb ...

  6. Suctf知识记录&&PHP代码审计,无字母数字webshell&&open_basedir绕过&&waf+idna+pythonssrf+nginx

    Checkin .user.ini构成php后门利用,设置auto_prepend_file=01.jpg,自动在文件前包含了01.jpg,利用.user.ini和图片马实现文件包含+图片马的利用. ...

  7. 《并发编程的艺术》阅读笔记之Lock与AQS

    Lock接口 在jdk1.5之后,并发包下新增了一个lock接口,lock接口定义了锁的获取,锁的释放,等方法,需要用户手动设置.与关键字不同的是,lock具有可操作性,比如,可以中断线程,设置超时时 ...

  8. Redis的三大问题

    一般我们对缓存读操作的时候有这么一个固定的套路: 如果我们的数据在缓存里边有,那么就直接取缓存的. 如果缓存里没有我们想要的数据,我们会先去查询数据库,然后将数据库查出来的数据写到缓存中. 最后将数据 ...

  9. TensorFlow keras vgg16net的使用

    from tensorflow.python.keras.applications.vgg16 import VGG16,preprocess_input,decode_predictions fro ...

  10. Docker 安装 Jenkins , 并解决初始安装插件失败

    安装 Jenkins 后,初始化下载插件总是失败,导致安装不成功,重试好几次都是卡在安装插件那. 这里记录下 Docker 下怎么安装 Jenkins ,并解决初始安装插件失败问题. 安装插件失败,其 ...