Keep going

Spring MVC中的二三事

HandlerMapping和HandlerAdapter

这个两个组件应该算是spring mvc中最重要的几个组件之一了,当一个请求到达DispatcherSerlvet后,spring mvc就全靠这各两个组件定位并调用我们定义的Controller函数。是的,他们的功能就分别对应了“定位”和“调用”。

HandlerMapping

先看看该接口的申明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface HandlerMapping {

  // ...
  // 其他常量定义

  /**
  * Return a handler and any interceptors for this request. The choice may be made
  * on request URL, session state, or any factor the implementing class chooses.
  * <p>The returned HandlerExecutionChain contains a handler Object, rather than
  * even a tag interface, so that handlers are not constrained in any way.
  * For example, a HandlerAdapter could be written to allow another framework's
  * handler objects to be used.
  * <p>Returns {@code null} if no match was found. This is not an error.
  * The DispatcherServlet will query all registered HandlerMapping beans to find
  * a match, and only decide there is an error if none can find a handler.
  * @param request current HTTP request
  * @return a HandlerExecutionChain instance containing handler object and
  * any interceptors, or {@code null} if no mapping found
  * @throws Exception if there is an internal error
  */
  HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

}

实际干事的就只有getHandler一个方法,根据http请求确定将要被执行的执行链HandlerExecutionChain。一个HandlerExecutionChain就是由目标handler和一组HandlerInterceptor组成。但是需要注意的是,HandlerExecutionChain并不负责真正的执行动作,它也不知道如何去执行目标handler,而仅仅是一个保存这些对象的容器罢了。

目标的handler是Object类型,换句话说spring没有提供任何接口来限定,可以是任何类型。因此真正的执行动作会发生在HandlerAdpater中,也就是说如果每个HandlerMapping(不管是spring提供的还是你自己写的)都需要有对应的HandlerAdpater,当然不一定是一一对应有些是可以复用的。

如何定义HandlerInterceptor

不像目标handler,handler执行链上的拦截器是有限定类型的,也就是上面提到的HandlerInterceptor。那么如何配置这些HandlerInterceptor呢?

首先需要明确的是interceptor最终都会被配置到容器中使用的HandlerMapping组件中去,因为HandlerMapping会产生HandlerExecutionChain,需要将所有的interceptor一并设置到返回的HandlerExecutionChain中。那么最直接的方式就是在定义HandlerMapping的地方将需要的interceptor直接注入到对应的HandlerMapping类中,实际上该字段是声明在AbstractHandlerMapping中,因此所有的HandlerMapping最好直接从AbstractHandlerMapping抽象类上继承,而不要直接实现HandlerMapping接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<beans>
    <bean id="handlerMapping"
          class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMap
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
      <property name="interceptors">
                    <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
    </bean>
    <bean id="officeHoursInterceptor"
          class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime" value="9"/>
        <property name="closingTime" value="18"/>
    </bean>
<beans>

实际上在spring中HandlerInterceptor有两类,一类是名符其实的实现了HandlerInterceptor接口的类;另外一类是MappedInterceptor,顾名思义它除了HandlerInterceptor的功能外还有了path match的能力,实际上它就是包含了一个真正的HandlerInterceptor外加一些路径匹配表达式。它的作用除了能够让spring调用其中包含的HandlerInterceptor之外,还具有路径匹配的功能,也就是说会告诉spring只有当指定request的请求路径复合要求的时候才会调用该interceptor。

OK,在回到配置HandlerInterceptor的第二种方法,就是使用<mvc:interceptors/>标签,如下:

1
2
3
4
5
6
7
8
<mvc:interceptors>
  <bean class="my.MyInterceptor"/>
  <ref bean="interceptorRef"/>
  <mvc:interceptor>
      <mvc:mapping path="/interceptor/*"/>
      <bean class="my.MyInterceptor1"/>
  </mvc:interceptor>
</mvc:interceptors>

这个例子就定义了三个interceptor,分别通过bean, ref, interceptor子元素。其中bean和ref定义的interceptor会匹配任何request(因为没有指定mapping path);使用interceptor子元素就可以指定mapping path了,那么它所表示的HandlerInterceptor就会根据request path来决定是否要执行。这些标签都会被转变为前面提到的MappedInterceptor

前面说了HandlerInterceptor会最终被应用到HandlerMapping中,那通过xml配置的interceptor呢?实际上他们会被同时自动配置到spring容器中定义的所有HandlerMapping中,这也是最合乎情理的,因为你并不需要同时考虑你根据path所配置的interceptor到底应该作用到那个HandlerMapping中。相反,所有的HandlerMapping在拥有了这些MappedInterceptor后,在准备HandlerExecutionChain时就会根据当前的request path来决定要把哪些MappedInterceptor放进去,当然所有直接定义的HandlerInterceptor都会被放入chain中。

那么spring是怎么把这些MappedInterceptor放入到HandlerMapping中的呢?实际上spring仅仅是把他们定义到容器中,在HandlerMapping初始化的时候通过调用AbstractHandlerMapping.detectMappedInterceptors方法来自动发现所有的MappedInterceptor,并做一些必要的初始化配置。

另外一点,如果你使用了<mvc:annotation-driven/>的话,默认是会添加一个MappedInterceptor到容器中,这个interceptor是ConversionServiceExposingInterceptor,它会把<mvc:annotation-driven/>检测或者创建的conversionService添加到HttpServletRequest的一个属性中,以便整个http request处理流程可以随时享用这个conversionService。因为并不是所有的组件都有享受spring ioc的能力,比如jsp tag,因此放在HttpServletRequest会比较方便。

HandlerAdapter

如何配置

HandlerAdapter是spring mvc中的独立组件,因此和其他核心组件一样可以通过一些三种方法获得:

  1. DispatcherServlet.peroperties默认提供
  2. <mvc:annotation-driven/>提供
  3. 自己配置在spring配置文件中

注意,2和3会disable掉1,但是2和3又会同时起作用。

应用流程

还是先看接口声明吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public interface HandlerAdapter {

  /**
  * Given a handler instance, return whether or not this {@code HandlerAdapter}
  * can support it. Typical HandlerAdapters will base the decision on the handler
  * type. HandlerAdapters will usually only support one handler type each.
  * <p>A typical implementation:
  * <p>{@code
  * return (handler instanceof MyHandler);
  * }
  * @param handler handler object to check
  * @return whether or not this object can use the given handler
  */
  boolean supports(Object handler);

  /**
  * Use the given handler to handle this request.
  * The workflow that is required may vary widely.
  * @param request current HTTP request
  * @param response current HTTP response
  * @param handler handler to use. This object must have previously been passed
  * to the {@code supports} method of this interface, which must have
  * returned {@code true}.
  * @throws Exception in case of errors
  * @return ModelAndView object with the name of the view and the required
  * model data, or {@code null} if the request has been handled directly
  */
  ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

  /**
  * Same contract as for HttpServlet's {@code getLastModified} method.
  * Can simply return -1 if there's no support in the handler class.
  * @param request current HTTP request
  * @param handler handler to use
  * @return the lastModified value for the given handler
  * @see javax.servlet.http.HttpServlet#getLastModified
  * @see org.springframework.web.servlet.mvc.LastModified#getLastModified
  */
  long getLastModified(HttpServletRequest request, Object handler);

}

DispatherServlet在通过前面的HandlerMapping获得了当前请求的HandlerExecutionChain之后,就会哪些chain里面定义的目标handler遍历所有配置好的HandlerAdapter,并调用supports方法询问不同的adapter是否可以处理,如果可以就进入处理流程,如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
  for (HandlerAdapter ha : this.handlerAdapters) {
      if (logger.isTraceEnabled()) {
          logger.trace("Testing handler adapter [" + ha + "]");
      }
      if (ha.supports(handler)) {
          return ha;
      }
  }
  throw new ServletException("No adapter for handler [" + handler +
          "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

处理流程如下:

  1. 如果是GET或者HEAD请求,调用HandlerAdapter.getLastModified方法看看目标Controller方法在对于该请求有没有可用的lastModified逻辑,如果有的话就使用ServletWebRequest.checkNotModified逻辑判断当前lastModfied值和http header的上次缓存值,如果还没有过期就设置304头并且返回并结束整个请求流程。否则继续。
  2. 应用preHandle方法,调用所有的HandlerInterceptor.preHandle方法
  3. 调用HandlerAdapter.handle方法进行目标handler的调用(调用controller),得到ModelAndView返回值
  4. 应用interceptor.postHandle方法。
  5. 最后根据handle返回值的请求调用DispatcherServlet.processDispatchResult方法来根据返回值类型处理成最终的http response。

一个栗子

逻辑就是这么简单,没有什么好多说的,因为就像前面说的不同的HandlerAdapter是需要配合不同的HandlerMapping产生的目标handler,没有固定的规律和模式。就拿SimpleControllerHandlerAdapter这个例子来说明下把。可以和它配对的HandlerMapping有ControllerBeanNameHandlerMappingControllerClassNameHandlerMapping,或者说从AbstractControllerUrlHandlerMapping继承下来的类。

先看看SimpleControllerHandlerAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleControllerHandlerAdapter implements HandlerAdapter {

  @Override
  public boolean supports(Object handler) {
      return (handler instanceof Controller);
  }

  @Override
  public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
          throws Exception {

      return ((Controller) handler).handleRequest(request, response);
  }

  @Override
  public long getLastModified(HttpServletRequest request, Object handler) {
      if (handler instanceof LastModified) {
          return ((LastModified) handler).getLastModified(request);
      }
      return -1L;
  }

}

可以得出以下简单的结论:

  1. 它只能处理目标handler是Controller类型(实现了Controller接口)的调用
  2. 对于lastModified特性,如果目标handler(从1可知肯定是一个Controller类型)也实现了LastModifed接口,那么就调用该接口的getLastMofied函数来得到lastMofiy值,否则返回-1表示不支持。
  3. 调用过程非常简单,就是调用目标Controller的handleRequest方法。

从中我们可以断定和它配合的HandlerMapping返回的目标handler必须是Controller类型。好吧,我们来看看ControllerBeanNameHandlerMappingControllerClassNameHandlerMapping是干什么的。他们两个实际上是非常相似的,共同的父类都会扫描容器中所有定义的bean,如果该bean是Controller类型,那么就交给这两个不同的子类做处理来决定如何将这个Controller加入到mapping中。

  1. 对于ControllerBeanNameHandlerMapping,它会把这个bean的名字及其alias作为request path
  2. 对于ControllerClassNameHandlerMapping,它会把这个bean的类名作为request path,比如HelloController对应为"/hello"。

那么在收到请求后,这两个HandlerMapping会根据request path匹配已经保存的mapping数据,如果找到匹配的就会将之前存好的这个bean,也就是这个Controller对象当做目标handler返回出去。在后面的调用流程中自然就可以被SimpleControllerHandlerAdapter处理了。

当然,一个请求来了具体被那个HandlerMapping处理要看不同HandlerMapping的处理能力,还处理顺序,自己不能处理的旧交由下一个处理,其顺序是HandlerMapping的order值确定的。

这仅仅是一个例子,目前Controller类已经不推荐使用了,更多的请使用annotation的方法,当然其对应的处理组件是RequestMappingHandlerMapping和RequestMappingHandlerAdapter。

再谈Spring的Binding和Validation

之前小伙伴已经写过一篇关于spring mvc中validation的文章,其中还提到了与JSR-303集成和MessageCodeResolver的使用,非常详细。我想已经适用于大部分的情况了,最近也遇到了一些关于数据绑定和验证(实际上他们本来就是不可分割的)的问题,解决方案虽然有很多,但是还是希望对以下问题做一个总结以便形成一种处理该类问题的模式。

1.Spring是如何做数据绑定的

实际上数据绑定的过程就是一个找到目标字段,再把值设置进去的过程。目标字段的确定非常容易,最常用的类就是BeanWrapperImpl,我们可以在Spring中的很多地方见到它,再看看它所实现的接口就知道具有数据访问和类型转换的功能,实际上Spring的数据绑定很多时候也是通过BeanWrapperImpl来实现的。

真正比较复杂的部分是数据的转换,看过Spring文档的人都知道Spring在很早以前就是用了Java中的PropertyEditor机制来实现数据转换。但是在Spring新的版本中虽然还是支持PropertyEditor,但是更加标准的做法是是用ConverionService。ConversionService是什么?如同它的名字所说,就是提供了转换服务的对象。真正提供转换功能的是Converter类,不同的Converter能够实现的转换不一样。OK,到这里就可以想象一个最简单的模式:一个ConversionService包含很多各式各样的Converter,使得这个ConversionService成为一个无所不能的转换器!如果你想要一个这样的ConversionService,你可以直接是用Spring给我们准备好的GenericConversionService,它是一个空的ConversionService,但是你可以自己定制它所包含的Convert。除此之外,Spring还给我们准备好了一套足够全的Converter,甚至还准备好了一个配置好的ConversionService - DefaultConversionService,它实际上就是继承自GenericConversionService,只不过在它的构造函数中就帮你把Spring中得默认Converter注册进去罢了,如果你对Spring提供的Converter感兴趣,可以从这里开始看。这些ConversionService不仅仅被Spring内部使用,你甚至可以自己直接拿来在产品代码中使用。

2.如何给Spring配置类型转换器

提到类型转换,目前大多数情况你只需要考虑ConversionService,PropertyEditor就不要再考虑了。这里分三种情况:

2.1 产品代码使用

这个是最简单的,直接在配置文件中定义个DefaultConversionService或者GenericConversionService,然后再注入到你的产品代码中。

2.2 供Spring解析配置文件的类型转换器

Spring容器的核心实际上是BeanFactory,所有的Bean可以理解成BeanFactory通过读取配置文件然后在创建出来的。那么自然类型转换的过程也发生在其中,和类型转换相关的对象也由BeanFactory,实际上是在AbstractBeanFactroy中:

1
2
3
4
5
6
7
8
9
10
11
12
13
/** Spring 3.0 ConversionService to use instead of PropertyEditors */
private ConversionService conversionService;

/** Custom PropertyEditorRegistrars to apply to the beans of this factory */
private final Set<PropertyEditorRegistrar> propertyEditorRegistrars =
      new LinkedHashSet<PropertyEditorRegistrar>(4);

/** A custom TypeConverter to use, overriding the default PropertyEditor mechanism */
private TypeConverter typeConverter;

/** Custom PropertyEditors to apply to the beans of this factory */
private final Map<Class<?>, Class<? extends PropertyEditor>> customEditors =
      new HashMap<Class<?>, Class<? extends PropertyEditor>>(4);

那当我们在使用ApplicationContext的时候,它是怎样将ConversionService初始化进去的呢?在AbstractApplicationContext中找到了答案:

1
2
3
4
5
6
7
8
9
10
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
  // Initialize conversion service for this context.
  if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
          beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
      beanFactory.setConversionService(
              beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
  }

  ...
}

实际上它就是在容器初始化要完成的时候检查容器内是由有名字是conversionService的ConversionSrervice对象,如果有的话就初始化给BeanFactory,就是这么简单。因此如果你加入自定义的转换逻辑,那么自己去申明一个ConversionService对象就完了。

2.3 供Spring MVC对HttpRequest参数到Model对象的转换器

当发生一个http请求时,我们可以配置我们的Controller让其自动将一些http请求参数直接转换为我们的command/model对象中。如果目标字段不是String型,必然就需要类型转换,因为HttpServletRequest中拿到的参数都是String型。

实际上在你申明<mvc:annotation-driven/>的时候,Spring的AnnotationDrivenBeanDefinitionParser就会帮你自动注册一个ConversionService到容器中,并且会将这个ConversionService放到另外一个beanConfigurableWebBindingInitializer中(这个WebBindingInitializer可就非常重要啦,最后再讲)。这个默认的ConversionService是FormattingConversionService,它提供了比普通ConversionService更多的Formatter的功能,实际上你可以将Formatter理解成另外一种形式上的Converter(object <-> String)。

我们当然也可以通过直接在<mvc:annotation-driven/>显示指定来改变这个默认ConversionService,如下:

1
2
3
4
5
6
7
8
9
<mvc:annotation-driven conversion-service="conversionService"/>
<!-- conversion service -->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
  <property name="formatters">
      <set>
          <bean class="org.springframework.format.datetime.DateFormatter" p:pattern="MM/dd/yyyy"/>
      </set>
  </property>
</bean>

上面的代码就相当于扩充了原有的默认的实现。其实AnnotationDrivenBeanDefinitionParser会先检测是否有conversion-service属性,如果有就用属性指定的ConversionService,没有就自己提供一个默认的,很简单吧。

3.Spring中的Validation模型

在IDE中输入Validator,可以看到有很多叫Validator的类或者接口,在Spring中只需要考虑org.springframework.validation.Validator,你不需要考虑javax.validation.Validator,它最多只会被Spring的Validator所使用。

看看Validator的接口:

1
2
3
4
public interface Validator {
  boolean supports(Class<?> clazz);
  void validate(Object target, Errors errors);
}

估计和你想象中得出入不大,但是实际上独立起来看是有点别扭的,因为不你清楚Errors是什么。因此大多数时候Validator是DataBinder一起使用,我想这也是为什么DataBinder也在包org.springframework.validation下面。下面的代码就是基本的使用模式:

1
2
3
4
5
6
    DataBinder binder = new DataBinder(object);
    binder.addValidators(...);
    binder.setConversionService(...);  // If you want to convert and bind data
    binder.bind(...);  // If you want to convert and bind data
    binder.validate();
    BindingResult result = binder.getBindingResult();

拿到了最后的BindingResult是不是就觉得和MVC中的BindingResult很相似了,实际上他们就是一个东西。Spring MVC也是使用上面的模式来做HttpRequest的数据绑定和验证。

当然你可以独立使用上面的模板来在产品代码中做数据验证,但是大部分时候对它的了解还是更多的有助于理解Spring MVC的验证过程。从上面的模板来看只有Spring MVC中得Validator是如何进行配置的没有说了,那就先来讲讲它。

还记得Spring MVC是如何配置ConversionService的吗,Validator和它是一样的,也可以配置在<mvc:annotation-driven/>的validator属性上。AnnotationDrivenBeanDefinitionParser会检测该属性,如果存在该属性则使用显示配置的Validator,并且被放入到前面提到的ConfigurableWebBindingInitializer中,否则就会使用一个默认的OptionalValidatorFactoryBean实现。这个类就有点意思了,它实际上会检测是否有JSR303的实现在classpath中,如果有那么就会提供一个包装了jsr303实现的Spring Validator的适配器,用来适配jsr303的实现。从这里可以看出,如果想利用jsr303只需要两个条件:1.将一个jsr303的实现放入到classpath中;2.声明<mvc:annotation-driven/>

再回到前面的DataBinder模板,我们说Spring MVC也是使用类似的模板来做数据绑定和验证的,那么其DataBinder是怎么创建和配置的呢?我们首先需要看DefaultDataBinderFactory类,顾名思义该类就是专门用来创建WebDataBinder的工厂类,其核心方法是:

1
2
3
4
5
6
7
8
9
10
@Override
public final WebDataBinder createBinder(NativeWebRequest webRequest, 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;
}

基本上就分为3个步骤:

1.创建WebDataBinder。这个没什么好说的,基本上就是直接new出来

2.使用initializer来初始化。这个initializer就是前面一直提到的ConfigurableWebBindingInitializer,它就相当于把在配置过程中解析到的关于ConversionService和Validator先存起来,然后到需要用到DataBinder的时候再设置进去。看看它还干了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void initBinder(WebDataBinder binder, WebRequest request) {
  binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
  if (this.directFieldAccess) {
      binder.initDirectFieldAccess();
  }
  if (this.messageCodesResolver != null) {
      binder.setMessageCodesResolver(this.messageCodesResolver);
  }
  if (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);
  }
  if (this.propertyEditorRegistrars != null) {
      for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
          propertyEditorRegistrar.registerCustomEditors(binder);
      }
  }
}

从其中就可以看到熟悉的MessageCodesResolver和另外一个东西BindingErrorProcessor。这两个东西后面再讲。

3.最后一步还会调用initBinder来再做一些额外的初始化。反应快得同学可能已经想到了@InitBinder注解,是的,如果该Controller中有该注解,那么DefaultDataBinderFactory就会是一个子类InitBinderDataBinderFactory,该子类的initBinder方法就会调用Controller中得@InitBinder注解了的方法来对DataBinder进行额外的设置,也就是说可以覆盖默认配置做一些定制化的东西。

4.到底应该在前台做验证还是后台做验证?

对用户的输入数据进行验证是任何web应用都需要的,因为不但非法的用户输入不能够正常进行业务流程,甚至会破坏系统的正常运行。不管是前台还是后台,市面上都充斥着五花八门的框架,很多框架都提供了验证的功能,那么验证逻辑是放在前台还是后台呢?其实优缺点也是很明显的,将验证功能放在前台不但可以避免没必要的网络开销,使用灵活的js代码直接面对用户可以做出各种复杂的验证逻辑,并且验证结果也可以随心所欲的方便控制;将验证功能放在后台可以最大程度的保护应用,因为没有人能够保证后台收到的请求一定是来自正常的前台应用。因此我觉得关键部分的验证逻辑不管前台做不做,后台是一定要有的,并且从整个应用来看验证方式一定要统一,不要给用户造成困扰。

前面提到了前台的验证逻辑是可以随心所欲的,那么后台呢?当然,如果你说你直接操纵HttpServletRequest,那么你也可以根据自己的需要很容易的实现各种验证逻辑。但是在Spring MVC这种框架下怎么更加灵活的控制validation呢?

当request到来时经过DataBinder的两个阶段,第一是convert到command对象中;第二个是对command对象的字段进行验证,不管是使用jsr303也好,还是写自己的注解或者代码逻辑也好,只要是已经转换到了command对象中,其他的验证逻辑是非常好写的,这里往往更多的关注业务逻辑的合法性。但是如何验证第一个步骤中存在的潜在问题呢,举个最基本的例子,如何验证一个日期类型的输入参数是一个合法的日期格式,如何验证一个目标字段是int类型的参数真正是一个数字类型?如果你什么都不做,那么在前台的<form:errors/>标签中就会显示出一大堆java exception的信息,这显然是我们不希望看到的。

好吧,还是回到DataBinder吧。针对发生在第一阶段转换过程中得任何异常都会被转换为TypeMismatchException,顾名思义就是类型不匹配导致的转换出错。这种类型的异常会直接对应到message的errorCode:

  1. code + “.” + object name + “.” + field
  2. code + “.” + field
  3. code + “.” + field type
  4. code

其中code=typeMismatch。这个翻译过程实际上就是由默认的MessageCodesResolver生成的,该过程在最开头小伙伴的文章中有说明。

OK,拿到了Exception并且翻译成了error code,然后又怎么办呢?这个就是由DataBinder中得BindingErrorProcessor来决定的了,实际上该接口也是非常简单的,默认实现就是将刚才得到的error code封装成Error对象加入到BindingResult中。

有了MessageCodesResolver和BindingErrorProcessor,我想就应该可以非常容易的驾驭Spring MVC的验证逻辑了,难能可贵的是这些对象都可以很容易的配置到不同Controller对应的DataBinder中去。

Mvc:annotation-driven到底帮我们做了什么

大家都知道在使用Spring MVC的时候需要在spring mvc的配置文件中加上<mvc:annotation-driven/>这句话,但是如果不加这句话一切有是可以正常work的,那到底是加还是不加呢,针对哪些功能是必须要加的呢?

其实如果用一句话来描述<mvc:annotation-driven/>到底干了什么,实际上它就是一个spring的自定义标签,帮助我们自动配置一些bean到spring容器中,这些bean又会被进一步的被其他bean发现,最终实现一些预定义的功能。当然它也提供了一些属性(attribute)可以供我们做细微的调整。说到这里就不得不提DispatcherServlet.properties文件,如果你还不知道它在哪里,你可以在spring-webmvc-x.x.x.jar中的org.springframework.web.servlet包种找到它。就像它的名字所说,该文件会被Spring MVC的入口DispatcherServlet在初始化的使用作为默认配置使用。看看它里面的内容主要包括以下类:

  1. LocaleResolver
  2. ThemeResolver
  3. HandlerMapping
  4. HandlerAdapter
  5. HandlerExceptionResolver
  6. RequestToViewNameTranslator
  7. ViewResolver
  8. FlashMapManager

这些组件都是spring mvc中的核心组件,DispatcherServlet.properties中就定义这些组件的默认实现类(默认策略)。那么DispatcherServlet是怎么使用这些默认策略的呢?其中有如下函数来初始化各组件:

1
2
3
4
5
6
7
8
9
10
11
12
 protected void initStrategies(ApplicationContext context) {
      initMultipartResolver(context);
      initLocaleResolver(context);
      initThemeResolver(context);
      initHandlerMappings(context);
      initHandlerAdapters(context);
      initHandlerExceptionResolvers(context);
      initRequestToViewNameTranslator(context);
      initViewResolvers(context);
      initFlashMapManager(context);
  }
  

各种init函数,就拿initViewResolvers举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 private void initViewResolvers(ApplicationContext context) {
      this.viewResolvers = null;

      if (this.detectAllViewResolvers) {
          // Find all ViewResolvers in the ApplicationContext, including ancestor contexts.
          Map<String, ViewResolver> matchingBeans =
                  BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
          if (!matchingBeans.isEmpty()) {
              this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values());
              // We keep ViewResolvers in sorted order.
              OrderComparator.sort(this.viewResolvers);
          }
      }
      else {
          try {
              ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);
              this.viewResolvers = Collections.singletonList(vr);
          }
          catch (NoSuchBeanDefinitionException ex) {
              // Ignore, we'll add a default ViewResolver later.
          }
      }

      // Ensure we have at least one ViewResolver, by registering
      // a default ViewResolver if no other resolvers are found.
      if (this.viewResolvers == null) {
          this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
          if (logger.isDebugEnabled()) {
              logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default");
          }
      }
  }
  

实际上就是首先看看容器里有没有已经定义ViewResolver类,如果有就使用容器中定义的ViewResolver作为最终的ViewResolver,如果没有就使用DispatcherServlet.properties中定义的。其他的init函数也是类似的模式,首先看看是否有显示的定义,如果有就用定义好的,否则就用properties中的默认配置。好了,这下知道在零配置环境下spring mvc实际上默认的已经给我们提供了一套组件配置供我们正常使用了,如果代码出了什么问题这下知道从哪里下手查看配置了吧。

总算可以回到主题<mvc:annotation-driven/>了。如果要查看它到底干了什么事情,只需要看该元素对应的handler即可,也就是AnnotationDrivenBeanDefinitionParser。从它的javadoc中可以看出主要做了下面这些配置:

  1. HandlerMapping: 将RequestMappingHandlerMappingBeanNameUrlHandlerMapping配置到spring容器中。
  2. HandlerAdapter: 将RequestMappingHandlerAdapterHttpRequestHandlerAdapterSimpleControllerHandlerAdapter配置到spring容器中
  3. HandlerExceptionResolver: 这个组件是用来控制当出现异常的时候spring如何来处理。它将ExceptionHandlerExceptionResolverResponseStatusExceptionResolverDefaultHandlerExceptionResolver配置到spring容器中作为异常处理器。
  4. ContentNegotiationManager: 这个东西是用来做内容协商的,主要是用在RequestMappingHandlerMapping里面。<mvc:annotation-driven/>会首先检查自己有没有content-negotiation-manager属性,如果有的话就用属性指定的ContentNegotiationManager,否则就创建一个默认的并注册到容器中。但是最关键的还是该ContentNegotiationManager最终会被自动设置到前面定义的RequestMappingHandlerMapping中去。
  5. DefaultFormattingConversionService: 给Spring MVC配置的ConversionService,也是spring提供的默认FormattingConversionService。
  6. LocalValidatorFactoryBean: 提供自动检测jsr303实现的spring validator,主要会被用来spring mvc在收到请求,并在交给Controller的时候做数据绑定使用(数据绑定之后做数据验证)。
  7. HttpMessageConverters: 创建(发现)一组HttpMessageConverter,并把他们配置到RequestMappingHandlerAdapter中,供spring mvc使用。

对于HttpMessageConverter,它到底是干什么的呢?用spring mvc写过REST的人可能略知一二,它是用来将特定的对象转换成字符串并最终作为http response返回的工具。实际上spring mvc中面向开发人员的业务逻辑处理主要集中在各种Controller的方法中,基本模式是接受代表着HttpRequest的各种输入参数,在方法体中进行业务逻辑处理,最后得到输出结果,并以返回值的形式交给spring mvc,spring mvc根据返回值的不同调用不同的处理逻辑并最终以http response的形式返回给客户端。大家都知道Controller中的返回值可以有很多种,比如字符串,ModelAndView,普通对象,等等,甚至void类型都是可以的。那么很容易想到spring mvc会根据返回值的类型做很多的if else,不同的类型调用不同的处理逻辑。那么当函数受@ResponseBody声明时,spring就会尝试用配置好的各种HttpMessageConverter来将返回值进行序列化。不同HttpMessageConverter能够处理的对象以及处理方式都是不一样的,spring会遍历各converter,如果该converter能够处理该对象则交由其处理。因此,很多基于spring的REST风格的应用常常会返回一个model对象,那么你就应该配置好正确的HttpMessageConverter,以便spring能够正确的将这些对象序列化回客户端。

那么<mvc:annotation-driven/>是如何配置HttpMessageConverter的呢?

  1. 首先它会看<mvc:annotation-driven/>中有没有显示指定message-converters,如果指定了那么就用指定的配置。
  2. 如果没有显示指定,或者虽然显示指定了但是还是指定了register-defaults属性的话就会默认添加一些常用的converter,比如ByteArrayHttpMessageConverter,StringHttpMessageConverter,ResourceHttpMessageConverter,SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter。除此之外还会有一些自动发现的逻辑,比如自动发现jackson和jaxb的相关组件是否在classpath中,如果存在就会加入对应的converter。因此如果你想用jackson来序列化json或者使用jaxb序列化xml,你只需要将其实现类放入到classpath中,并且再声明<mvc:annotation-driven/>就自动配置好了。

意味着什么

前面介绍了DispatcherServlet.properties<mvc:annotation-driven/>,实际上DispatcherServlet.properties的逻辑是固然存在的,我们没有办法控制;但是<mvc:annotation-driven/>我们可以选择声明或者不声明,如果声明了还可以在一定程度上控制它的行为。但是很显然如果我们使用spring mvc,是推荐声明<mvc:annotation-driven/>的,并且如果声明了<mvc:annotation-driven/>那么就会自动对一些spring mvc的核心组件进行配置,也进而disable(覆盖)了很多DispatcherServlet.properties中的默认配置。

比如对于annotation风格的spring mvc,以前是可以使用DefaultAnnotationHandlerMapping作为handler mapping的,这个也是properties中的默认配置。但是现在spring已经在用RequestMappingHandlerMapping来替代它了,这也是<mvc:annotation-driven/>的默认配置。所以曾经一个同事和我讨论<mvc:annotation-driver/>是不是开启注解式spring mvc功能的必要条件,现在答案很清楚了,虽然不是必要条件,但是最好还是加上<mvc:annotation-driven/>,因为背后提供该服务的组件是不一样的。

还有一点是需要强调的, 要想让DispatchServlet.properties中的配置生效,比如其中定义的HandlerMapping,需要保证整个Spring Context中没有显示或隐式定义其他HandlerMapping,这种约束是很不灵活的。举个例子,你的spring配置中没有定义任何HandlerMapping,觉得DispatcherServlet.properties中提供的默认配置足够了,并且也想使用RequestMapping定义的Controller,在通常情况下这个是可以满足要求的。但是如果你又想将没有匹配成功的request交给应用服务器的默认Servlet来处理,就需要在spring-servlet.xml中配置<mvc:default-servlet-handler/>,这个时候你会发现RequestMapping不能工作了,为什么?原因是<mvc:default-servlet-handler/>会在context中隐式加入SimpleUrlHandlerMapping,导致spring在解析的时候发现有可用的HandlerMapping,就不会再去加载DispatcherServlet.properties中定义的配置。

总之,当你在使用spring mvc的时候,虽然它帮我们做了很多事情,一切看起来都是work的,但是还是要清楚你的系统中有哪些组件在起作用,这样出了问题才知道如何定位问题。

Java中的Reference对象[译]

原文链接

介绍

我从2000年开始用Java编程,在这之前使用了C和C++长达15年。我觉得我有能力在C类的语言中管理好内存,比如使用指针偏移,或者使用工具比如Purify。不记不清我遇到的最后一次内存泄露问题了。因此我确实对Java的自动内存管理有点不削,但是我很快爱上它了。我从来没有意识到在内存管理中需要花费多大的经历,因为它不需要我做任何事情。

随后我遇到了我的第一次OOM。它仅仅显示在控制台中,并且没有任何堆栈信息,因为堆栈跟踪信息需要内存开销。调试这个错误是非常痛苦的,因为没有任何工具可以使用,甚至是malloc日志。在2000年的Java调试的状况就是如此,非常原始。

我记不清造成这次OOM的原因是什么了,当然我并没有使用reference对象来解决这个问题。它们没有在我的常用工具箱中,直到1年以后,当我在写一个数据库缓存,并且打算尝试使用它们以减少缓存的内存开销。但是发现它们并不是那么有效,原因我会在后面分析。但是当它们进入到我的工具箱之后,我发现了很多关于这些reference对象的用法,并且可以让我更好的理解JVM。

深入解析OutOfMemoryError[译]

原文链接

在Java中,所有对象都存储在堆中。他们通过new关键字来进行分配,JVM会检查是否所有线程都无法在访问他们了,并且会将他们进行回收。在大多数时候程序员都不会有一丝一毫的察觉,这些工作都被静悄悄的执行。但是,有时候在发布前的最后一天,程序挂了。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

OutOfMemoryError是一个让人很郁闷的异常。它通常说明你干了写错误的事情:没必要的长时间保存一些没必要的数据,或者同一时间处理了过多的数据。有些时候,这些问题并不一定受你的控制,比如说一些第三方的库对一些字符串做了缓存,或者一些应用服务器在部署的时候并没有进行清理。并且,对于堆中已经存在的对象,我们往往拿他们没办法。

这篇文章分析了导致OutOfMemoryError的不同原因,以及你该怎样应对这种原因的方法。以下分析仅限于Sun Hotspot虚拟机,但是大多数结论都适用于其他任何的JVM实现。它们大多数基于网上的文章以及我自己的经验。我没有直接做JVM开发的工作,因此结论并不代表JVM的作者。但是我确实曾经遇到过并解决了很多内存相关的问题。

Jcl-over-slf4j

`jcl-over-slf4j'如同名字一样就是用来将java commons logging桥接到slf4j上。现在J2EE的一个项目通常会引用五花八门的类库,不同的类库又会使用不同的日志门面系统,有的是slf4j,有的是jcl。现在随着slf4j的越来越流行,那么将系统里的所有日志系统都统一到slf4j上也成了一个很平常的需求,jcl-over-slf4j也成了项目依赖中的常客。这周和同事也讨论了该桥接类的应用,尤其是spring本身也是直接以来jcl的,其官方文档也给出了如何替换成slf4j的说明。今天看了下代码,这里做下记录。

jcl的绑定流程

使用jcl的标准方法如下:

1
Log log = LogFactory.getLog(XXX.class);

LogFactory中给出了如下绑定流程:

  1. 检查系统属性org.apache.commons.logging.LogFactory,其中记录了具体的LogFactory的实现类。
  2. 通过java的service loading机制加载META-INF/services/org.apache.commons.logging.LogFactory配置文件,其中记录了LogFactory的实现类。
  3. 检查classpath中的commons-logging.properties配置文件,里面记录了具体的LogFactory的实现类。
  4. 使用jcl提供的默认实现类:org.apache.commons.logging.impl.LogFactoryImpl来创建Log。

如果不幸进入了最后一个步骤使用org.apache.commons.logging.impl.LogFactoryImpl来创建Log,那么它又会使用一套发现机制类查找合适的日志实现:

  1. 检查org.apache.commons.logging.impl.LogFactoryImpl是否已经配置了org.apache.commons.logging.Log属性,如果配置了该属性,则使用该属性指定的Log。
  2. 使用系统属性org.apache.commons.logging.Log中所定义的Log实现
  3. 检查是否存在log4j的实现类:org.apache.commons.logging.impl.Log4JLogger
  4. 检查org.apache.commons.logging.impl.Jdk14Logger
  5. 检查org.apache.commons.logging.impl.Jdk13LumberjackLogger
  6. 检查org.apache.commons.logging.impl.SimpleLog

jcl-over-slf4j桥接模式

其实仔细看看jcl-over-slf4j的实现,可以发现它提供了两种桥接方法。

1.引入jcl-over-slf4j并排除jcl

该方法也是spring官方推荐的方法,它的实现也是很巧妙也很直接。因为我们使用jcl都是通过LogFactory.getLog(XXX.class)来获得Log,jcl-over-slf4j中也就提供了名称完全一样的LogFactory,只不过它的getLog方法直接通过Slf4jLogFactory返回Slf4jLog。如果在dependencies中排除掉jcl,那么所有引用jcl的地方就偷天换日的直接使用了slf4j的日志。

2.引入jcl-over-slf4j并且没有排除jcl

有些人可能发现如果没有在dependencies中排除掉jcl也是可以工作的,这又是为什么呢?实际上jcl-over-slf4j提供了方法1外,还参考了jcl的绑定机制,并且参考上面提到的步骤2提供了META-INF/services/org.apache.commons.logging.LogFactory,其中说明了jcl需要绑定的LogFactory实现是org.apache.commons.logging.impl.SLF4JLogFactory。这不,也委托回slf4j上让其提供Log。

实际上,上面的两种方法都是可以的。但是推荐使用第一种方法,因为更加直接和清晰。第二种方法会让classpath中存在两个完全同名的jcl的LogFactory,但是不论jvm加载哪一个都是OK的。