关联博文:
SpringMVC常见组件之HandlerAdapter分析
SpringMVC常见组件之HandlerMapping分析
SpringMVC常见组件之HandlerMethodArgumentResolver解析
SpringMVC常见组件之HandlerMethodReturnValueHandler解析
SpringMVC常见组件之DataBinder数据绑定器分析
SpringMVC常见组件之ViewResolver分析
SpringMVC常见组件之View分析
SpringMVC常见组件之HandlerExceptionResolver分析

什么是数据绑定?简单一句话就是把请求中参数信息绑定到目标方法的参数上。数据绑定是参数解析过程中的一部分。SpringMVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。如下图所示:

那么这里我们就要研究一下数据绑定相关的那些组件。

【1】绑定工厂WebDataBinderFactory

工厂嘛,使用了工厂方法设计模式,只有一个抽象方法用来让子类实现以创建一个WebDataBinder实例。

public interface WebDataBinderFactory {WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName)throws Exception;
}
  • NativeWebRequest:当前请求
  • target:数据绑定器的目标对象,如果是为简单类型创建,则target为null;
  • objectName:target的名字

WebDataBinderFactory家族图示(典型的工厂方法设计模式)

① DefaultDataBinderFactory

这里我们先看一下其createBinder方法,也是核心入口方法。

@Override
@SuppressWarnings("deprecation")
public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {// 提供了默认实现,也可以让子类覆盖
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
if (this.initializer != null) {this.initializer.initBinder(dataBinder, webRequest);
}
// 空方法,让子类实现
initBinder(dataBinder, webRequest);
return dataBinder;
}

如下所示,其创建了一个WebRequestDataBinder实例。WebRequestDataBinder主要是用来将web request parameters绑定到JavaBeans,支持 multipart files。

protected WebDataBinder createBinderInstance(@Nullable Object target, String objectName, NativeWebRequest webRequest) throws Exception {return new WebRequestDataBinder(target, objectName);
}

initBinder方法是个空方法,让子类InitBinderDataBinderFactory实现。

这里我们关注一下this.initializer.initBinder(dataBinder, webRequest);。也就是初始化器首先对数据绑定器进行初始化,这里最终会走到ConfigurableWebBindingInitializer#initBinder,如下所示,在后面使用databinder进行类型转换、格式校验时用的就是这里填充进去的“功能属性对象”

@Override
public void initBinder(WebDataBinder binder) {binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);if (this.directFieldAccess) {// 设置directFieldAccess=truebinder.initDirectFieldAccess();}// 设置messageCodesResolver ,默认是DefaultMessageCodesResolverif (this.messageCodesResolver != null) {binder.setMessageCodesResolver(this.messageCodesResolver);}// 设置BindingErrorProcessor,默认是DefaultBindingErrorProcessorif (this.bindingErrorProcessor != null) {binder.setBindingErrorProcessor(this.bindingErrorProcessor);}// 设置校验器if (this.validator != null && binder.getTarget() != null &&this.validator.supports(binder.getTarget().getClass())) {binder.setValidator(this.validator);}// 设置类型转换服务if (this.conversionService != null) {binder.setConversionService(this.conversionService);}// 设置属性编辑注册器,能够向binder注册一系列编辑器if (this.propertyEditorRegistrars != null) {for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {propertyEditorRegistrar.registerCustomEditors(binder);}}
}

② InitBinderDataBinderFactory

InitBinderDataBinderFactory是通过@InitBinder方法向WebDataBinder 添加初始化行为。其并没有重写父类的创建DataBinder方法,但是提供了初始化DataBinder方法。

public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {for (InvocableHandlerMethod binderMethod : this.binderMethods) {if (isBinderMethodApplicable(binderMethod, dataBinder)) {Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);if (returnValue != null) {throw new IllegalStateException("@InitBinder methods must not return a value (should be void): " + binderMethod);}}}
}

这里我们需要多说一点,在进行参数解析的时候,如果binderFactory不为null且是DefaultDataBinderFactory或子类InitBinderDataBinderFactory。那么其createBinder会进行初始化initBinder(dataBinder, webRequest);。而InitBinderDataBinderFactory的initBinder方法如上所示,会首先判断当前binderMethods是否有符合当前WebDataBinder@InitBinder方法binderMethod。如果有合适的binderMethod,那么将会在初始化绑定器的过程中反射调用该方法。

如下图所示,能够看到InvocableHandlerMethod#invokeForRequest方法被调用了两次,第二次的时候providedArgs不再为空

③ ServletRequestDataBinderFactory

其创建了一个ServletRequestDataBinder(子类ExtendedServletRequestDataBinder),ServletRequestDataBinder可以将请求的参数绑定到目标对象,如Query String Parameters、form-data的参数,并支持multipart files的绑定。

具体子类是ExtendedServletRequestDataBinderExtendedServletRequestDataBinderURI template variables(如路径变量/getUser/{id}中的id)解析提供给数据绑定使用。

@Override
protected ServletRequestDataBinder createBinderInstance(@Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {return new ExtendedServletRequestDataBinder(target, objectName);
}

【2】数据绑定器DataBinder

数据绑定器是为目标对象绑定属性值,同时支持属性校验与绑定结果分析。可以通过BindingResult拿到绑定结果。

① 校验器处理

DataBinder提供了一些校验器处理方法如addValidators(添加校验器)、replaceValidators(替换校验器)、getValidators(获取校验器)、getValidato(获取第一个校验器)r以及核心的validate方法。

public void validate(Object... validationHints) {Object target = getTarget();Assert.state(target != null, "No target to validate");BindingResult bindingResult = getBindingResult();// Call each validator with the same binding resultfor (Validator validator : getValidators()) {if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {((SmartValidator) validator).validate(target, bindingResult, validationHints);}else if (validator != null) {validator.validate(target, bindingResult);}}
}

代码主要含义就是首先获取taget(要绑定的目标对象)、bindingResultValidators,然后遍历Validators并进行校验,将校验结果放到bingdingResult中。

一个校验通过的bingdingResult如下所示

如果校验不通过,那么其errors属性将会保存错误信息,如下所示

② 转换器处理

数据绑定过程中,获取到请求中的数据后向目标对象进行绑定,那么这个阶段可能涉及到类型转换,如String转换为Integer。DataBinder实现了TypeConverter接口,提供了系列重载convertIfNecessary方法。

@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {return getTypeConverter().convertIfNecessary(value, requiredType);
}@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,@Nullable MethodParameter methodParam) throws TypeMismatchException {return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field)throws TypeMismatchException {return getTypeConverter().convertIfNecessary(value, requiredType, field);
}@Nullable
@Override
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {return getTypeConverter().convertIfNecessary(value, requiredType, typeDescriptor);
}

这里面我们可以看一下其getTypeConverter方法:

protected TypeConverter getTypeConverter() {if (getTarget() != null) {return getInternalBindingResult().getPropertyAccessor();}else {return getSimpleTypeConverter();}
}

也就是说如果target不为null(复杂类型比如SysUser),那么就获取一个BeanWrapperImpl实例。如果是一个简单类型如Integer age,那么就获取一个SimpleTypeConverter

额外说明的是,在类型转换的过程中,都离不开一个conversionService实例(默认是DefaultFormattingConversionService)。该实例内部拥有系统的converters,正是这些converts(有100多个)实现了类型的转换。从下图可以看到,二者都是TypeConverterSupport的子类,实现了TypeConverter、PropertyEditorRegistry接口。PropertyEditorRegistry接口给自定义属性编辑器注册提供了入口。


WebDataBinder 使用registerCustomEditor应用

@InitBinder
public void initBinderTest(WebDataBinder binder){SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");dateFormat.setLenient(false);binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}

③ 绑定结果BindingResult

数据绑定过程中很可能因为数据类型转换异常(或其他错误)绑定失败,这些错误信息就保存在了BindingResult中。我们可以通过该接口获取到错误信息进行对应处理。这里我们可以简单看一下其家族树,不展开分析。详情参考博文SpringMVC中使用JSR303进行数据校验实践详解


其创建BindingResult的两个方法

protected AbstractPropertyBindingResult createBeanPropertyBindingResult() {BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(),getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());if (this.conversionService != null) {result.initConversion(this.conversionService);}if (this.messageCodesResolver != null) {result.setMessageCodesResolver(this.messageCodesResolver);}return result;
}
protected AbstractPropertyBindingResult createDirectFieldBindingResult() {DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(),getObjectName(), isAutoGrowNestedPaths());if (this.conversionService != null) {result.initConversion(this.conversionService);}if (this.messageCodesResolver != null) {result.setMessageCodesResolver(this.messageCodesResolver);}return result;
}

获取BindingResult的核心方法

// 根据directFieldAccess 获取DirectFieldBindingResult或者BeanPropertyBindingResult
protected AbstractPropertyBindingResult getInternalBindingResult() {if (this.bindingResult == null) {this.bindingResult = (this.directFieldAccess ?createDirectFieldBindingResult(): createBeanPropertyBindingResult());}return this.bindingResult;
}

如下实例方法三个参数分别为pojo、Errors以及map

这里我们解析得到的参数实例为:

④ 核心绑定方法

也就是把给定的属性-值绑定到目标对象上,这个过程可能会抛出“required”或者类型转换异常等错误。

public void bind(PropertyValues pvs) {MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ?(MutablePropertyValues) pvs : new MutablePropertyValues(pvs));doBind(mpvs);
}
protected void doBind(MutablePropertyValues mpvs) {checkAllowedFields(mpvs);checkRequiredFields(mpvs);applyPropertyValues(mpvs);
}

可以看到doBind是一个模板方法:

  • checkAllowedFields方法是检测被允许的field,如果发现有不被允许的field,则移除掉。* checkRequiredFields方法是检测是那些声明了“required”的filed是否都存在,如果有不存在的将会把错误信息放到BindingResult中。
  • applyPropertyValues则是核心的属性-值绑定到目标对象的方法。

【3】WebDataBinder

WebDataBinder是DataBinder的子类,其覆盖了父类的doBind方法。

@Override
protected void doBind(MutablePropertyValues mpvs) {checkFieldDefaults(mpvs);checkFieldMarkers(mpvs);adaptEmptyArrayIndices(mpvs);super.doBind(mpvs);
}

其同样是个模板方法,在处理属性默认值(字段以“!”开头,提供默认值代替空值)、属性标记(字段以"_"开头,如果表单提交没有该字段对应的值,则重置该字段)、字段名称去掉"[]"(如果字段名称以[]结尾)后,调用了父类的doBind方法。

如下演示!的使用:

public static void main(String[] args){SysUser sysUser = new SysUser();WebDataBinder binder = new WebDataBinder(sysUser, "sysUser");// 设置属性(此处演示一下默认值)MutablePropertyValues pvs = new MutablePropertyValues();// 使用!来模拟各个字段手动指定默认值pvs.add("!name", "jane");pvs.add("age", 18);pvs.add("!age", 10); // 上面有确切的值了,默认值不会再生效binder.bind(pvs);System.out.println(sysUser);}

打印结果:SysUser{name='jane', sex='null', age=18}

值得一提的是,其提供了方法bindMultipart供子类调用,该方法用来处理multipart files

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);}});
}

【4】ServletRequestDataBinder

ServletRequestDataBinder继承自WebDataBinder,有唯一子类ExtendedServletRequestDataBinder。此类把web请求限定为了Servlet Request,和Servlet规范强绑定。

Special {@link org.springframework.validation.DataBinder} to perform data binding from servlet request parameters to JavaBeans, including support for multipart files.

ServletRequestDataBinder可以将请求的参数绑定到目标对象,如Query String Parameters、form-data的参数,并支持multipart files的绑定。

子类ExtendedServletRequestDataBinderURI template variables(如路径变量/getUser/{id}中的id)解析提供给数据绑定使用。

① ServletRequestDataBinder的bind方法

ServletRequestDataBinder提供了方法bind,其对MultipartRequest 请求做了支持可以绑定文件域(通过调用父类WebDataBinderbindMultipart方法)。

public void bind(ServletRequest request) {// 从request中获取属性-值MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);// 判断当前请求是否为MultipartRequest MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);if (multipartRequest != null) {// 调用父类webDataBinder方法bindMultipart(multipartRequest.getMultiFileMap(), mpvs);}else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) {HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);if (httpServletRequest != null) {StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());}}addBindValues(mpvs, request);// 调用父类方法doBind(mpvs);
}
  • ① 这里尝试获取mpvs,然后判断是否为MultipartRequest或者StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")&&httpServletRequest != null之后调用对应的bindMultipart或者bindParts方法。如http://localhost:8081/testUser/1?name=jane得到的mpvs如下图所示:
  • ② addBindValues是一步兼容,从request中获取属性HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE对应的值,然后放入mpvs。这时候的mpvs可能如下(可以看到多了一个URI Template Variable id=1):
  • ③ 调用父类核心的doBind方法,将mpvs中的属性-值绑定到目标target上

这里比较有意思的是addBindValues方法是一个空方法,且用protected修饰。很明显其是想让子类实现(ExtendedServletRequestDataBinder子类进行了实现)。

protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {}

如下所示,执行doBind(mpvs);方法的DataBinder对象。

如下所示,执行doBind(mpvs);方法的DataBinder对象。可以看到从mpvs中找到SysUser拥有的属性给sysUser实例对象赋值(通常是反射调用set方法,如setName)。

当然如果BindingResult不是BeanPropertyBindingResult而是DirectFieldBindingResult则是另一种方式。

② ExtendedServletRequestDataBinder

ExtendedServletRequestDataBinder实现/覆盖了父类的addBindValues方法,主要是从request中获取URL路径变量集合,然后找到符合当前请求的属性-值放入MutablePropertyValues mpvs中。

@Override
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {// HandlerMapping.class.getName() + ".uriTemplateVariables"String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;@SuppressWarnings("unchecked")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);}});}
}

此属性放入的第一个地方是:AbstractUrlHandlerMapping.lookupHandler() --> chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); --> preHandle()方法 -> exposeUriTemplateVariables(this.uriTemplateVariables, request); -> request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
第二个地方是:AbstractHandlerMethodMapping.lookupHandlerMethod()->RequestMappingInfoHandlerMapping.handleMatch()–>RequestMappingInfoHandlerMapping.extractMatchDetails()–>request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);

如下所示,一个请求对象的attributes中包含了一个org.springframework.web.servlet.HandlerMapping.uriTemplateVariables属性:

我们也顺便多看一眼当一个请求发起到找到对应的HandlerMethod时,请求属性里面放入了什么?

③ 表单提交绑定过程

这里描述的背景为填写表单对象,传递到后台(参数为SysUser sysUser,也就是常见的实体对象)。

① SpringMVC框架将ServletRequest对象以及目标方法的入参实例(objectName,attribute) 传递给WebDataBinderFactory实例,以创建DataBinder实例对象。

  • objectName为@ModelAttribute注解的value值或者根据参数类型自动创建的key
  • 其中attribute会根据目标方法参数类型从ModelAndViewContainer 获取或者创建-这是Value

② DataBinder调用装配在SpringMVC上下文中的ConversionService组件进行数据类型转换、数据格式化工作并将请求信息填充到创建的入参对象中。

③ 调用Validator 组件对已经绑定了请求消息的入参对象(创建的)进行数据合法性校验,并最终生成数据结果(BindingResult)

④ SpringMVC 抽取 BindingResult 中的入参对象(getTarget)和校验错误对象(BindingResult),将他们赋给处理方法的相应入参

【5】WebRequestDataBinder

这个与不同的是其是把web request parameters 绑定到 JavaBeans,同样支持multipart files。也就是说其跳出了Servlet规范,额外做了支持。其主要对org.springframework.web.context.request.WebRequest做处理。

如下代码所示,WebRequestDataBinder首先从request中获取参数值封装为MutablePropertyValues 。然后分别判断是否为MultipartRequest或者请求头Content-Type以multipart/开头两种情况进行处理,最后调用父类的doBind方法。

public void bind(WebRequest request) {MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap());if (request instanceof NativeWebRequest) {MultipartRequest multipartRequest = ((NativeWebRequest) request).getNativeRequest(MultipartRequest.class);if (multipartRequest != null) {bindMultipart(multipartRequest.getMultiFileMap(), mpvs);}else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) {HttpServletRequest servletRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class);if (servletRequest != null) {StandardServletPartUtils.bindParts(servletRequest, mpvs, isBindEmptyMultipartFiles());}}}doBind(mpvs);
}

SpringMVC常见组件之DataBinder数据绑定器分析相关推荐

  1. SpringMVC常见组件之View分析

    关联博文: SpringMVC中支持的那些视图解析技术 SpringMVC常见组件之ViewResolver分析 SpringMVC中重定向请求时传输参数原理分析与实践 前面SpringMVC常见组件 ...

  2. 【SSM框架系列】Spring-MVC的组件解析

    SpringMVC完整执行流程 用户发送请求至前端控制器DispatcherServlet. DispatcherServlet收到请求调用HandlerMapping处理器映射器. 处理器映射器找到 ...

  3. SpringMVC(一)视图解析器

    springMVC是一个基于spring的一个框架,实际上 就是spring的一个模块,专门做web开发. 是servlet的一个升级. web开发的底层是servlet,框架是再servlet基础上 ...

  4. 【详细】Android入门到放弃篇-YES OR NO-》各种UI组件,布局管理器,单元Activity

    问:达叔,你放弃了吗? 答:不,放弃是不可能的,丢了Android,你会心疼吗?如果别人把你丢掉,你是痛苦呢?还是痛苦呢?~ 引导语 有人说,爱上一个人是痛苦的,有人说,喜欢一个人是幸福的. 人与人之 ...

  5. vue双向数据绑定原理分析--Mr.Ember

    vue双向数据绑定原理分析 摘要 vue常用,但原理常常不理解,下面我们来具体分析下vue的双向数据绑定原理. (1)创建一个vue对象,实现一个数据监听器observer,对所有数据对象属性进行监听 ...

  6. Pytorch学习 - Task6 PyTorch常见的损失函数和优化器使用

    Pytorch学习 - Task6 PyTorch常见的损失函数和优化器使用 官方参考链接 1. 损失函数 (1)BCELoss 二分类 计算公式 小例子: (2) BCEWithLogitsLoss ...

  7. 一位微信小程序萌新的学渣笔记(三)基础语法之常见组件

    文章目录 常见组件 view text image swiper navigator rich-text 富文本标签 button icon radio checkbox 自定义组件 创建自定义组件 ...

  8. 微信小程序怎么在wxml中插入多个图片_22. 教你零基础搭建小程序:小程序的常见组件(2)- image

    大家好~今天讲小程序的常见组件-- image 图片标签 小程序中的图片标签相当于 web 中的图片标签 ,但也存在着不同之处. 例如:小程序在后期要打包上线时,对图片的大小是有要求的,图片要< ...

  9. Android中所有UI组件基类是,【详细】Android入门到放弃篇-YES OR NO-》各种UI组件,布局管理器,单元Activity...

    问:达叔,你放弃了吗? 答:不,放弃是不可能的,丢了Android,你会心疼吗?如果别人把你丢掉,你是痛苦呢?还是痛苦呢?~ 引导语 有人说,爱上一个人是痛苦的,有人说,喜欢一个人是幸福的. 人与人之 ...

最新文章

  1. qregexp限制数字范围_计算一列数字的平均值
  2. 【QwQ】乱七八糟的置顶
  3. 超详解析Flutter渲染引擎|业务想创新,不了解底层原理怎么行?
  4. 将应用打包为 Snaps
  5. android 判断webview加载成功,Android:如何检查使用webview.loadUrl时url的成功加载
  6. Callable 和 Future接口 学习
  7. java最新版怎么安装_Java JDK 最新版本安装与环境配置
  8. Linux 命令 find / -ctime +1 真的是查找1天前创建的文件咩?
  9. SQL Server插入geography、geometry和c_hierarchyid类型数据
  10. 大漠为什么不支持win10_Win10系统注册使用大漠插件的方法与设置!常见错误0x8002801...
  11. 机器人操作系统 ROS 大全
  12. 汉字录入到计算机的过程,如何快速把书中文字录入到电脑中
  13. HTML开心人人新浪微薄等
  14. 短视频素材怎么找?怎么做短视频运营?
  15. tecplot常用笔记
  16. (详细)《美国节日》:某月的第几个星期几
  17. 数论作业 —— 公约数公倍数问题
  18. php 招聘要求 转载
  19. 【Bug】WindowsPowerShell\profile.ps1
  20. 物联网卡拉开智能家居变革序幕

热门文章

  1. window XP安装
  2. 深度优先和广度优先的概念理解
  3. 一分钟学会python进制转换
  4. trello_如何构建Trello Chrome扩展程序-API身份验证
  5. java继承执行子类输出顺序_JAVA继承顺序
  6. 深度好文:阿里巴巴高级专家对组建技术团队的一些思考
  7. 软件测试方法汇总 - 从不同角度分析软件测试方法有哪些
  8. 做IAP远程升级时,APP程序地址修改了中断向量偏移地址不起效果的原因分析
  9. HTML——CSS样式优先级
  10. html5 本地文件系统,本地文件系统 客户端存储 WebAPI编程 [HTML5知典]