Spring MVC学习(01)入门

内容概览


    1. 介绍
    1. Spring MVC初步学习和配置
    1. 接受参数
    1. 处理表单
    1. 总结

1、 介绍


我们开发的很多应用实际上都是web应用,而现在最流行的框架当属Spring的MVC框架。Spring MVC框架基于模型-视图-控制器模式实现,能够帮助我们构建像spring那样灵活和松耦合的web应用程序。

2、Spring MVC初步学习和配置


每当用户在浏览器中点击链接或者提交表单时,请求就开始了。请求就像快递员一样,把信息从一个地方带到另一个地方,从离开浏览器开始到获取响应返回,中间会经历很多环节,每个环节都会留下一些信息,并带上其它信息。如图所示:

图中标了很多数字,我们来看一下:

  • 1、在请求离开浏览器时,会带有用户所请求内容的信息,会带有用户所请求内容的信息,至少会包含请求的URL,也可以包含用户提交的表单信息或者其他内容。第一站是Spring的DispatcherServlet,与大多数基于Java的web框架一样,Spring MVC的所有请求都会经过一个前端控制器Servlet。前端控制器是常用的web应用程序处理模式,在这里一个单实例的Servlet将请求委托给应用程序的其它组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。

  • 2、DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller),控制器是一个用于处理请求的spring组件,在应用程序中,一般会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet会查询一个或者多个处理器映射(handler mapping)来确定请求的下一站在哪里,处理器映射会根据请求所携带的URL信息来进行决策。

  • 3、一旦选择了合适的控制器,DispatcherServlet会将请求发送给选择的控制器进行处理,到了控制器,请求会交出其携带的信息,并等待处理结果。不过一般不建议处理器这一层处理业务逻辑,业务可以集中交给下一层来处理。

  • 4、控制器在完成逻辑处理后,会产生处理结果信息,这些信息需要返回给用户并展示在浏览器或者其他客户端,这些信息被称为模型-model,为了友好的展示一般会是一个html数据,所以信息需要发送一个视图-view,一般是JSP。控制器做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名,他接下来会将请求连同模型和视图名一起发送回DispatcherServlet。

  • 5、这样控制器就不会与特定的视图耦合在一起,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP,甚至并不能确定视图就是JSP格式,它仅仅传递了一个逻辑名称,这个名称将会用来查找产生结果的真正视图,DispatcherServlet将会使用视图解析器来将逻辑视图名匹配为一个特定的视图实现。JSP只是其中一个可能。

  • 6、DispatcherServlet已经知道由哪个视图渲染结果,那么请求也基本上完成了。最后一步就是视图的实现了,交付了模型数据,请求就完成了。

  • 7、视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会硬编码)。

可以看到一个请求要经过很多步骤,最终才能响应得到结果,大多数步骤都是在Spring框架内完成的,下面来看一下如何搭建Spring MVC基本组件。

根据上面的请求流程图,我们第一步先来配置DispatcherServlet,它是Spring MVC的核心,它主要负责将请求路由到其它组件中。因为目前的Spring 版本推荐的是JavaConfig的方式进行配置,下面我们主要用这种方式进行,至于以前使用最多的xml方式,会在后面讨论。我们将使用Java将DispatcherServlet配置在Servlet容器中,如下:

因为这个类放在了 包路径 web023.config 下,所以起名为 Web023WebAppInitializer。下面来了解这个类是如何工作的。扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类,都会自动配置DispatcherServlet和Spring应用上下文,Spring的应用上下文位于应用程序的Servlet上下文之中。

在Servlet 3.0 环境中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 接口的类,如果能发现的话,就会用它来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现 WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring 3.2 版本以后,引入了一个便利的 WebApplicationInitializer 基础实现,也就是 AbstractAnnotationConfigDispatcherServletInitializer ,因为我们的类 Web023WebAppInitializer 扩展了 AbstractAnnotationConfigDispatcherServletInitializer (同时也会实现WebApplicationInitializer ),因此当部署到Servlet 3.0 容器中的时候,容器会自动发现它,并用它来配置Servlet容器上下文。尽管它名字很长,但是 AbstractAnnotationConfigDispatcherServletInitializer 使用起来很方便。

在我们的代码 WebApplicationInitializer 中,重写了三个方法,第一个是 getServletMappings ,它会将一个或者多个路径映射到 DispatcherServlet 上,本例中映射的是 “/” ,这表示它会是应用的默认Servlet,它会处理进入应用的所有请求。

WebApplicationInitializer 中还重写了其它两个方法,为了理解这两个方法,首选要明白 DispatcherServlet 和一个Servlet监听器(也就是 ContextLoaderListener)的关系。当 DispatcherServlet 启动的时候,它会创建Spring 应用上下文,并加载配置文件和配置类中所声明的bean,在方法 getServletConfigClasses 中,我们要求DispatcherServlet 加载上下文时,使用定义在WebConfig中的配置类(Java配置类)中的bean,但是在Spring Web中还有另一个上下文,这个另外的上下文是由 ContextLoaderListener创建的。

正常情况下,DispatcherServlet 加载包含web组件的bean,如控制器,视图解析器以及处理器映射,而 ContextLoaderListener 加载应用中的其它bean,这些bean通常是驱动应用后端的中间层和数据层的组件。

实际上,AbstractAnnotationConfigDispatcherServletInitializer 会同时创建 DispatcherServlet 和 ContextLoaderListener ,getServletConfigClasses 方法返回的带有 @Configuration 注解的类将会用来定义DispatcherServlet 应用上下文中的bean,getRootConfigClasses 方法返回的带有 @Configuration 注解的类将会用来配置 ContextLoaderListener 创建的应用上下文中的bean。这两个类的内容稍后会看到。

需要注意的是,通过 AbstractAnnotationConfigDispatcherServletInitializer 来配置 DispatcherServlet 是传统 web.xml 方式的替代方案,其实也可以同时包含AbstractAnnotationConfigDispatcherServletInitializer 和web.xml,但是其实没必要。不过使用本例子中这种方式配置,唯一的问题在于它只能部署到支持Servlet 3.0 的服务器中才能正常工作(例如Tomcat 7或者更高的版本),Servlet 3.0规范在2009年12月份就发布了,因此目前应用比较多。但是如果项目中实际使用的是不支持 Servlet 3.0的服务器,那么就不能使用本例子中的方式配置DispatcherServlet ,只能使用web.xml了。

下面来看一下具体的 WebConfig 类,

先来看一下类上面的注解,@Configuration 和 @ComponentScan不用再详细说明,@ComponentScan扫描的包路径正是控制器类的路径,控制器类一般带有 @Controller 或者 @RestController 注解,这会使类成为组件扫描时的候选bean,因此不用在配置类中显示的配置任何bean。@EnableWebMvc注解表示启用Spring MVC。

下面来看 viewResolver 方法,这是一个 ViewResolver 的bean,更具体的讲是 InternalResourceViewResolver ,这是一个视图解析器(后面会详细讨论视图解析器),它会查找JSP文件,在查找的时候,会在视图名称上加一个特定的前缀和后缀,本例子中的前缀是 "/WEB-INF/views" ,后缀是 jsp,如果名为login的视图会解析为 /WEB-INF/views/login.jsp。

WebConfig 类还继承了 WebMvcConfigurerAdapter 类并重写了 configureDefaultServletHandling 方法,通过代码 configurer.enable(); ,我们要求 DispatcherServlet 对静态资源(css、js等)的请求转发到Servlet容器中默认的Servlet上,而不是使用 DispatcherServlet 本身来处理此类请求。

下面来看一下 RootConfig 类,在本例子中,我们只讨论Spring MVC的访问,么有访问数据库等操作,返回假数据即可,因此可以配置一个空类:

这样,一个简单的Spring MVC的配置基本就完成了,下面来看一下基本控制器的编写。在Spring MVC中,控制器只是方法上添加了 @RequestMapping 注解的类,这个注解声明了它们所要处理的请求,控制器的类上面通常会写@Controller 注解,下面来看一个简单的例子:

@Controller 是一个基于 @Component的注解,其实类上面还可以直接使用最简单的 @Component 注解,但是在意思表达上就不那么清晰了。 控制器类中写了两个方法,方法上面都有各自的注解,首先来看 @RequestMapping ,它的value属性指定了这个方法所要处理的请求的路径,method属性细化了它所处理的http的方法,代表了请求的类型(get,post等)。

类中有两个方法,其中 hi() 方法上面还有一个 @ResponseBody 注解,表示返回的不是一个页面视图,而是纯数据。下面的hello方法没有 @ResponseBody 注解,虽然也返回了一个字符串 hello ,但是会被Spring MVC解读为要渲染的视图名称,DispatcherServlet 会要求视图解析器将这个逻辑名称解析为实际的视图路径,根据前面的配置:

这个视图的实际结果是 /WEB-INF/views/hello.jsp ,下面我们在这个路径下创建一个简单的jsp文件:

这个jsp没有特殊的地方,只是展示了 hello jsp 的文字。现在我们的简单的例子就基本完成了,下面开始测试,这里需要使用到tomcat,因为使用的是maven结构的项目,所以直接加一个插件即可:

然后双击下面的run进行启动:

image.png

可以看到启动成功:

下面我们分别访问控制器中定义的两个链接,/hello 结果如下:

/hi 的结果如下:

在控制器中,我们的路径是完全定义在方法上面的,不过同一个类中往往定义了相同业务的链接,大部分情况下会有重复的部分,所以可以把部分路径定义在类级别上:

这样两个路径就变成了 /web/hi 和 /web/hello。不仅如此,类级别的路径还可以定义多个:

这种情况下,方法 hi() 对应的全路径包括两个,是 /web/hi 和 /mvc/hi ,方法 hello() 也是同样。

上面的例子我们已经编写了一个非常简单的控制器,但是实际业务当中,控制器的功能是复杂的,例如会在处理完业务逻辑后,向视图页面中传递一些数据模型,来展示数据。下面来实现一个这样的例子,首先,我们来定义一个实体数据模型类:

上面其实就是一个简单的POJO类,下一步,写一个业务方法获取所有的数据,这里没有配置数据库访问,所以我们来写一个静态数据的业务方法:

数据准备好了,下面来看控制器方法的编写:

这里控制器方法中有个Model的参数,通过这个参数就能把获取的users数据填充到模型中,Model实际上就是一个Map,它会传递给视图,这样数据就能渲染到客户端了,当调用 addAttribute() 方法并且不指定key的时候,那么key就会根据值得对象类型推断确定,例如List<User> 类型会推断为 userList ,但是通常情况下,我们会像代码中那样指定一个key。

数据已经放到模型中了,在jsp中应该如何访问呢?实际上,当视图是jsp的时候,模型数据会作为属性放到请求(request)中,因此,jsp文件中可以使用JSTL(JavaServer Pages Standard Tag Library)的<c:forEach> 标签渲染List列表,也即是users。当输出单个字段值的时候,可以使用 <c:out> 标签,具体写法如下:

<c:forEach> 中,items属性写的是传递过来的列表的key,需要放在{}中,var属性定义的是列表中每个元素,标签内容是循环体,<c:out value="{user.id}"/> 写法是输出列表中元素的单个字段值。

为了使用JSTL标签,需要在jsp页面的顶部定义 taglib :

下面来看一下jsp页面的内容:

现在来看一下实现效果:

这样我们就实现了向视图模型中传递数据。

3、 接受参数


控制器中的方法在大多数时候其实是要接收参数的,Spring MVC允许以多种方式将客户端中的数据传递到控制器的处理方法中,参数类型包括查询参数,表单参数,路径变量三种。先来看最简单的查询参数。

现在我们向方法中传递一个name参数,这样查询List列表的时候,就可以代替 “Jack” 参数,如下:

参数前面使用了 @RequestParam("name") 注解,其中的 “name” 表示接受名称为name的参数,除了定义参数的名字,还可以定义默认值和是否可以为空,如下:

defaultValue = "blues" ,表示客户端不传递这个参数时,默认值是blues,required = false,表示这个参数不是必须传递的。来看一下效果:

下面来看一下通过路径参数接受输入,我们可以直接把参数的位置放到路径当中:

在路径中要包含参数,需要在路径中添加占位符,使用{} 括起占位符的名字,参数的注解也变成了 @PathVariable("name") ,这表明不管占位符是什么值都会传递到name参数中,如果占位符的名字和参数名字一样,可以直接写 @PathVariable String name ,来看一下路径参数的效果:

4、处理表单


如果传递少量参数,查询参数和路径参数都是很合适的,但是遇到表单提交的情况,需要传递很多的参数,那这两种就不合适了。Spring MVC为表单的处理提供了良好的支持。

使用表达通常分为两个方面:展现表单,以及处理用户通过表单提交的数据。来看第一种情况,展现表单。我们平时操作软件时会有新增数据的功能,或者注册功能,下面我们来跳转到新增页面,来看一下方法:

方法很简单,没有任何参数,也没有传递任何参数到视图页面,除了跳转没有任何逻辑,下面来看一下form表单:

这个表单有两个输入框,和一个保存按钮,同时form标签中显示,保存提交的路径是 /web/save ,类型是post,是一个非常基础的带有form标签的html页面。它的样子大概如下图所示,来看一下效果:

上面的表单提交的路径是 /web/save ,方法类型是post,下面我们来实现一个这样的方法,并接收页面传递过来的参数,同时保存成功以后,为了防止重复提交,这里最好进行 一次重定向,我们来重定向到新增页面,下面是具体方法:

这个保存方法接收一个User对象作为参数,这个对象有id和name两个属性,这两个属性的值将使用form表单中name属性同名的输入作为赋值。保存方法最后一件事也是返回一个字符串,但是这里不是一个视图页面的名称,而是一个重定向的链接,这个链接的前缀是 redirect: ,有这个前缀就表示是一个重定向,后面的 /web/to/add 是目标地址,表示让浏览器重新访问这个链接,自然也就会跳转到这个链接方法所返回的视图页面。

像这种返回结果的跳转,除了 redirect: 前缀,还有一种 forward: 前缀,这种就不是重定向了,就是一种跳转的方式,不同的是写视图的名字就是直接指向视图页面,写 forward: 这种前缀就是,要通过一个链接地址,走一遍地址对应的方法,然后再跳转。

代码写完了,我们直接输入id为123,名称为Jack,可以看到重定向到了新增页面,但是访问了保存方法:

除了新增之外,还有修改功能,需要跳转到form表单,不同的是,修改功能同时要带着修改前的原始数据:

同时为了展示数据,我们需要修改一下视图:

这里使用value="${user.id}" 将携带的值赋值到了输入框当中,如果user或者user.id为空,那么属性就会展示为 value="" ,是一个空字符串,所以这个页面可以共用于新增和修改。下面来看一下效果:

在我们提交和保存数据的时候,一般会有一些基本的逻辑验证操作,比如用户名不能为空,密码不能为空,邮箱地址不能为空,并且邮箱地址要合理合法等等。有的字段还需要控制长度,比如用户名,电话号码等等。

这些验证逻辑我们可以在控制器的方法中进行,在接收到参数以后,写多个 if 语句来校验非空,长度,格式等等。但是这种方式非常的初级,也比较繁琐,所扰乱我们的控制器方法,还有一种选择,就是使用Spring对Java的校验API(Java Validation API,又称 JSR-303)的支持。Spring MVC中也提供了这种支持,并且不需要什么额外的配置,只要保证在类路径下包含这个API的实现即可,比如 Hibernate Validator。

Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值,所有的注解都位于 javax.validation.constraints 包中,如下图:

除了上表中的注解,Java校验API的实现可能还会提供额外的校验注解,同时也可以定义自己的限制条件,下面我们重点来使用 @NotNull 和@Size 这两个注解。我们要做的就是将注解添加到属性上面,如下:

对每个字段的校验逻辑都写在了注解上面,接下来需要修改一下控制器的方法,来验证和返回参数,保证校验逻辑:

首先,参数user增加了 @Valid注解,这会告知Spring,需要确保这个对象满足校验逻辑限制。同时方法还增加了Errors参数,如果校验出现了错误,可以通过Errors对象进行获取,注意,Errors对象要紧紧跟在带有 @Valid注解的参数后面,判断是否有参数逻辑错误,可以通过 errors.hasErrors() 方法获取,返回true表示有问题。同时因为验证错误以后,我们需要做的就是提示错误信息,首先要获取所有的错误对象, errors.getAllErrors() ,这会返回一个List列表,有几条记录就表示验证发现了几处错误,要获取我们自定义的友好的中文提示,可以通过 getDefaultMessage() 方法获取,代码中我们直接打印了出来。下面来测试一下,我们直接提交,不输入任何消息,可以看到以下打印信息:

5、总结


在这里我们讨论了如何使用Spring 4版本推荐的JavaConfig方式创建一个简单的Spring MVC应用实例,写了一个简单的JSP视图页面,后面我们会详细讨论关于视图的技术。

代码地址:https://gitee.com/blueses/spring-framework-demo 23

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335