SpringMVC源码分析5、聊HandlerAdapter以前必需要了解的前置知识点

HandlerAdapter做为SpringMVC中最复杂的一部分, 在真正的分析其源码以前, 咱们有必要对一些前置知识点进行了 解, 不然在看源码的过程当中会遇到盲点.....java

@ModelAttribute

先说说这个注解的做用吧, 被@ModelAttribute标注的方法, 在Controller方法执行前, 都会执行一次该注解标注的
方法, 下面先来看一个例子:
@Controller
public class TestController {
    @PostMapping( "/test" )
    public String test() {
        System.out.println( request );
        return "index";
    }

    @ModelAttribute
    public void testModelAttribute1 () {
        System.out.println( "Hello" );
    }
}

分析:
    当咱们每次请求这个/test的时候, 在SpringMVC调用test方法以前, 都会调用一次testModelAttribute1方法,
    固然, 这仅仅知识@ModelAttribute注解的一个小功能而已

@ModelAttribute: 
    对Model的属性进行定义、操做, Model是一个贯穿整个SpringMVC的东西, 相信你们接触该框架时必然少不了对
    Model的学习, 其实Model的本质就是一个Map而已, 在咱们进行参数绑定到Controller方法的时候, 数据的来源
    有好几个地方, 好比Request请求参数, session的Atrribute中, 以及这个Model(是一个Map), 
    @ModelAttribute的注解的做用有两个, 看成用在方法上的时候, 方法的返回值就会被放入
    到这个所谓的Model中, 若是指定了@ModelAttribute注解的value属性, 那么该value属性就做为Model中的key
    返回值做为值, 若是没有指定, 那么就会按照必定的生成规则生成key, 看成用在方法参数上的时候, 表示从
    Model中取出参数并赋值给该参数, 若是指定了@ModelAttribute注解的value属性, 那么就从Model中取得对应
    的key所在的value, 下面咱们举个例子来讲明:

例子:
    @GetMapping( "/test" )
    public String test (@ModelAttribute( "username" ) String username, @ModelAttribute List<String> list, Model model) {
        System.out.println( model );
        System.out.println( username );
        System.out.println( list );
        return "index";
    }

    @ModelAttribute( "username" )
    public String testModelAttribute1 () {
        return "zhangsan";
    }

    @ModelAttribute
    public List<String> testModelAttribute2 () {
        return Arrays.asList( "a", "b" );
    }

输出:
    {username=zhangsan, stringList=[a, b]}
    zhangsan
    [a, b]

分析:
    结合前面所说的, 被@ModelAttribute注解标注的方法, 在每次执行该Controller的方法(即test方法)前都会
    执行一次, 而且将返回值放入到Model对象中, 而咱们的hanler(以后请求映射方法如test都称为handler)在被
    执行的时候, @ModelAtrribute方法标注的参数就会从Model中获取, 能够看到, 输出Model的时候, 里面是保存
    了两个值的, 咱们返回的集合其对应的key即stringList, 在没有主动指定该注解的value的状况下, key是按照
    必定规则生成的, 当咱们参数绑定的时候, 若是没有主动指定该注解的值, 也会自动从Model中取出一个最合适的

小小的总结:
    @ModelAttribute的做用有两个, 被该注解标注的方法在每次其所在的Controller的hanler执行前都会执行一次
    而且将返回值放入到Model中, 被该注解标注的hanler的属性, 在获取值的时候就会从Model中获取, 那如何作到
    仅仅从Model中获取呢?这个就跟HandlerAdapter中的参数解析器ArgumentResolver有关系了, 若是是从下一篇
    文章过来的同窗, 笔者应该是先把参数解析器说明了才引入ModelAttribute的, 因此这里应该不会迷惑什么是参
    数解析器了, 固然, 不懂也不要紧, 这里仅仅须要了解这个注解的做用就行了, 其余的看到后面天然会豁然开朗
复制代码

@SessionAttributes

@SessionAttributes的做用其实跟上面咱们所说的Model有点关系, 咱们知道, Model实际上是一个Map, 
@ModelAttribute的做用就是往这个Map中放入key-value或者根据key从这个Map中取得值, 可是貌似只能做用在同一
个请求中, 由于@ModelAttribute中的Model仅仅是会在一个请求中有效而已, 若是想要在多个请求中实现参数的传递
你们应该能够想到, session就可以实现了, session中保存的属性是一次会话中会持续有效的, 因此若是有多个请求
的话, 就能利用session来传递数据, 而@SessionAttribute的做用就是往session中放入数据, 
SessionAttributes这个注解只能做用在一个类上, 他的做用其实很简单, 当请求结束的时候, 会将Model这个map中
的数据所有的保存到session中, 而在请求来的时候, Model中的数据除了会从@ModelAttribute中放入外, 还会从
session中取出@SessionAttributes规定的数据, 可能这样说比较抽象, 下面咱们利用两个handler来证实一下:

@Controller
@SessionAttributes( names={"username"} )
public class TestController1 {
    @GetMapping( "/request1" )
    public String request1 (Model model) {
        model.addAttribute( "username", "lisi" );
        return "index";
    }

    @GetMapping( "/request2" )
    public String request2 (@ModelAttribute( "username" ) String username) {
        System.out.println( username );
        return "index";
    }
}

分析:
    先请求/request1, 再请求/request2, 会发现, 第二次请求打印了lisi, 其实很简单, 当咱们执行第一次请求
    即/request1的时候, 往Model中放入了username->lisi, 因为@SessionAttributes中指定了name为username
    因此在hanlder执行完毕时, 就会将Model中key为username的键值对放入到session中, 当第二次请求过来的时
    候, 除了会将@ModelAttribute做用在方法的返回值放入Model外, 还会从session中将@SessionAttributes指
    定的key找到value, 而后放入到Model中, 当第二次请求执行到handler中时, Model中已经存在username这个
    键值对了

应用场景:
    须要注意的是, @SessionAtrributes的做用域但是一个类, 即仅仅会将该类中的请求中Model的指定数据放入
    session中, 同时, 也仅仅会从session中取出该类定义的@SessionAttributes中规定的参数而已, 而且能够看
    到, 因为保存到session中, 因此能够在多个请求中传递参数, 而咱们重定向redirect是无法传递参数的, 能够
    经过这个方式来实现参数的传递, 下面来看一个例子

@Controller
@SessionAttributes( names={"username"} )
public class TestController1 {
    @GetMapping( "/request1" )
    public String request1 (Model model) {
        model.addAttribute( "username", "lisi" );
        return "redirect:request2";
    }

    @GetMapping( "/request2" )
    public String request2 (String username, SessionStatus sessionStatus) {
        System.out.println( username );
        sessionStatus.setComplete();
        return "index";
    }
}

分析:
    和前面的例子相似, 不一样的是增长了一个SessionStatus类型的参数, 这个参数SpringMVC会自动帮咱们注入,
    其实仍是利用了参数解析器, 能够发现, /request1往model中放入了username->lisi, 同时
    @SessionAttributes指定了username会放入session中, 因而重定向到/request2时候, username的值就能从
    model中得到, 可是咱们调用sessionStatus.setComplete方法, 这个方法的意义是清空@SessionAttributes
    中保存到session中的数据

到此, @SessionAttributes的做用就说完了, 这样咱们就能够在以前的源码中看到, SpringMVC是如何完成这些操做
的
复制代码

@InitBinder

DataBinder

在上篇文章中, 咱们了解到了, BeanWrapper对属性的设置原理实际上是依赖于Java内省机制的PropertyDescriptor的
同时其提供了自定义解析器的功能, 经过registerCustomEditor方法往BeanWrapper中注入PropertyEditor的实现
类来完成的, 一般状况下是字符串转对象, 此时会采用PropertyEditorSupport这个类, 重写其setAsText或者
getAsText方法便可, 从而完成了转换, 本节讲解的DataBinder跟BeanWrapper相似, 由于DataBinder所作的任何事
情都是基于BeanWrapper来完成的, 先来看一个例子吧:
public static void main(String[] args) {
    Customer customer = new Customer();

    DataBinder binder = new DataBinder( customer );

    MutablePropertyValues propertyValues = new MutablePropertyValues();
    propertyValues.add( "birth", new Date() );

    binder.bind( propertyValues );
    System.out.println( customer );
}

分析:
    能够看到, 将MutablePropertyValues中的属性绑定到对象Customer中, 是否是跟BeanWrapper有点相似呢?因
    为Customer的birth属性是Date类型的, 因此直接绑定没有关系, 那么假设我提供的是字符串就绑定不了, 因而
    跟BeanWrapper相似, 能够经过registerCustomEditor方法来注册一个自定义的解析器, 好比字符串解析器:

    public static void main(String[] args) {
        Customer customer = new Customer();

        DataBinder binder = new DataBinder( customer );

        MutablePropertyValues propertyValues = new MutablePropertyValues();
        propertyValues.add( "birth", "2020-06-26 14:33:22" );

        binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                SimpleDateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
                setValue( dateFormat.parse( text ) );
            }
        });

        binder.bind( propertyValues );
        System.out.println( customer );
    }

    输出结果跟上面的同样, 可是咱们传入的确是字符串.........这个就是DataBinder的做用, 对一个对象提供
    PropertyValues来进行绑定, 同时能够提供验证器Validator, 这个就不进行分析了, 有兴趣能够研究下
复制代码

WebDataBinder如下的继承体系

DataBinder是Spring框架自己自带的数据绑定工具, 底层用的是BeanWrapper, 而BeanWrapper的底层又是
PropertyDesciptor以及PropertyEdtitor, 因而在SpringMVC中, 为了更加方便Request中数据到对象的绑定, 在
基于DataBinder上又进一步进行了实现, 以下图所示, WebDataBinder继承了DataBinder, 在其源码中, 其实就是
增长了一些判断而已,. 有一个doBind方法, 是WebDataBinder进行绑定的入口, 在该方法中, 作了一些判断, 最后
仍是调用了父类DataBinder的doBind方法, 再日后就是ServletRequestDataBinder以及
ExtendedServletRequestDataBinder, 这两个binder完成的任务就是将request对象中的param所有取出来并封装成
PropertyVlues, 而后调用父类的doBind的方法的时候传入从而进行绑定, 这里就不进行深刻分析了, 你们只须要清楚
当咱们在利用HttpServletRequest对象的getParameterValues获取到值后, 对handler进行绑定其实就是建立一个
ExtendedServletRequestDataBinder, 而后将handler中的参数对象以及HttpServletRequest对象传入进入就能够
完成绑定了, 下面是伪代码:

@RequestMapping( "/test" )
public String test (Customer customer) {
    System.out.println( customer );
    return "index"
}

当在执行参数绑定的时候, 伪代码以下:
    HttpServletRequest request = getHttpServletRequest();
    Customer customer = new Customer();

    ServletRequestDataBinder binder = new ServletRequestDataBinder(customer);
    binder.bind(request);
复制代码

@InitBinder

先来看一个例子吧:
@Controller
public class TestController1 {
    @InitBinder
    public void initBinder (WebDataBinder webDataBinder) {
        webDataBinder.registerCustomEditor( Date.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                SimpleDateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
                try {
                    setValue( dateFormat.parse( text ) );
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });

    }

    @GetMapping( "/request1" )
    public String request1 (Date date) {
        System.out.println( date );
        return "index";
    }
}

分析:
    咱们请求的url: 127.0.0.1/request1?date=1997-11-23 14:00:00
    当请求过来的时候, 正常状况下是无法将"1997-11-23 14:00:00"转为Date对象的, 可是咱们手动往
    WebDataBinder中注册了一个PropertyEditor, 同时指定类型为Date类型, 因此当其绑定Date类型的时候就会
    利用这个PropertyEditor进行转换了

    那么@InitBinder的意思就很明显了, 在每次请求执行前都会执行@InitBinder对应的方法, 咱们能够经过这种
    方式手动注册本身须要的参数解析器, 从总体来看, 貌似跟@ModelAttribute的做用时机是差很少的, 都是请求
    执行前会执行一次, 而@ModelAttribute做用在方法上的时候, 会将返回值放入Model中, 而@InitBinder则是
    容许咱们初始化WebDataBinder, 以后的源码分析也能够清晰的看到这两点
复制代码

@ControllerAdvice

在上面的分析中, 咱们主要引入了三个注解, 分别是@ModelAttribute、@SessionAttributes、@InitBinder, 这
三个注解中, @ModelAttribute注解能做用在方法和handler的方法参数上, @SessionAttributes只能做用在类上,
而且是以类为做用域的, 不一样类以前不能共享, @InitBinder只能做用在方法上, 以上三个注解都是对本身所在的
Controller生效, 那么问题来了, 若是有多个Controller, 每一个Controller都须要执行同样的@ModelAttribute
和@InitBinder标注的方法, 那么这两个注解标注的方法就会被复制多份并放到不一样的类中, 因而为了防止这样的状况
出现, SpringMVC提供了一个全局的方式, 只须要建立一个@ControllerAdvice注解标注的类, 而后将这两个注解标注
的方法放到该类中, 就实现了全局的操做了, 而不用每一个类放置一份, 同时, @ControllerAdvice还提供了卓多属性,
使得咱们可以灵活的配置做用的包、类等
复制代码
相关文章
相关标签/搜索