文章目录
1、初识SpringMVC
SpringMVC是Spring的一个Web框架,它帮助了我们在开发JavaWeb项目中会遇到的状态管理、工作流以及验证等等这些问题,SpringMVC就是为了解决这些关注点而设计的。SpringMVC基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮我们构建像Spring框架那样灵活和松耦合的Web应用程序。
2、SpringMVC执行流程
每当用户在Web浏览器中点击链接或提交表单的时候,请求就开始工作了。下图从发起请求开始:
在请求离开浏览器时,会带有用户所请求内容的信息,至少会包含请求的URL。
- 请求旅程的第一站是Spring的 DispatcherServlet,SpringMVC所有的请求都会通过一个前端控制器Servlet。DispatcherServlet的任务是将请求发送给SpringMVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个处理器映射(handler mapping)。
- 处理器映射(handler mapping)寻找相应的控制器。处理器映射会根据请求所携带的URL信息来进行决策。
- 找到了就返回给 DispatcherServlet,如果没找到就会报错了,在前端界面看到的就是404。
- DispatcherServlet会将请求发送给选中的控制器
- 到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理。)控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的,这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view)。控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名(逻辑视图名)。它接下来会将请求连同模型和视图名发送回DispatcherServlet。
- DispatcherServlet 将会使用视图解析器(view
resolver)来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。 - 告知 DispatcherServlet 产生结果的真正视图。
- 既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现。在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出
- 输出会通过响应对象传递给客户端
DispatcherServlet 就像个分发器一样,将请求分发给不同的控制器去处理。
可以看到,请求要经过很多的步骤,最终才能形成返回给客户端的响应。但是大多数的步骤都是在Spring框架内部完成的,不用我们手动去实现,了解这些可以帮助我们对SpringMVC有个更深入的认识。
3、搭建SpringMVC
在这里我将会按照最简单的方式搭建一个SpringMVC,它尽可能功能不全,但是它用来我们快速上手已经绰绰有余了。
3.1、配置DispatcherServlet
按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中。
先在WEB-INF目录下创建Springmvc.xml文件,然后在web.xml中加入如下配置:
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
我们在学习Servlet的时候就学习过这些标签是什么意思,所以这里就不过多的阐述。
注意,在中我们设置的是“/”,它会处理进入应用的所有请求。
3.1.1、配置一个首页设置:
<welcome-file-list>
<welcome-file>view/index.html</welcome-file>
</welcome-file-list>
指定应用的首页
在往下进行之前,我们先来看看 ContextLoaderListener。
我们在使用Spring框架来开发JavaWeb项目时,通常会在Web.xml中加上如下配置:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
这项配置就是配置了 ContextLoaderListener 监听器,它的作用是在启动web容器时,自动装配Spring的applicationContext.xml的配置信息。
换句话说就是,servlet容器启动后,会调用web.xml中配置的contextLoaderListener,初始化WebApplicationContext上下文环境(即IOC容器),并加载context-param指定的配置文件信息到IOC容器中。
在Spring Web项目中一般会有两个应用上下文,一个是Spring(ContextLoaderListener)应用上下文,也就是IOC容器,另外一个就是 SpringMVC(DispatcherServlet)应用上下文。
一般SpringMVC(DispatcherServlet)应用上下文,负责加载控制器、视图解析器以及处理器映射这些Bean。而Spring(ContextLoaderListener)应用上下文负责加载应用中的其他bean,如service、dao这些Bean。
3.2、启用Spring MVC
因为我们在后面写控制器的时候会用到@Controller注解,所以我们还在在springmvc.xml文件中加入如下配置:
<!-- 启动SpringMVC注解 -->
<mvc:annotation-driven/>
要使用spring mvc中的@Controller注解,就必须要配置<mvc:annotation-driven />,否则org.springframework.web.servlet.DispatcherServlet无法找到控制器并把请求分发到控制器。
最后的配置如果没有mvc:annotation-driven/,那么所有的Controller可能就没有解析,所有当有请求时候都没有匹配的处理请求类,就都去default servlet处理了。添加上mvc:annotation-driven/后,相应的请求被Controller处理,而静态资源因为没有相应的Controller就会被default servlet处理。总之没有相应的Controller就会被default servlet处理。
现在我们的程序其实是可以运行起来了,但是还有一些问题,这些问题会导致我们的应用不可用。
- 没有启用组件扫描
- 没有配置视图解析器
- 没有处理静态资源
接下来的配置项将会在springmvc.xml文件中进行配置
3.2.1、配置启用组件扫描:
<!-- 配置组件扫描 -->
<context:component-scan base-package="com.SpringMVC.controller" />
3.2.2、配置视图解析器:
<!--配视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/view/"/>
<property name="suffix" value=".html"/>
</bean>
如果不配置视图解析器,Spring默认会使用BeanNameView-Resolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,它以这样的方式来解析视图。
3.2.3、配置静态资源处理方案
<mvc:default-servlet-handler />
如果不配置静态资源会怎样呢?因为我们在前面配置 DispatcherServlet 的时候,配置的“/” 拦截所有请求,包括css、js、html和图片这些静态资源,这就导致前端访问不到页面的内容,就会出现404错误。
在springmvc.xml中配置<mvc:default-servlet-handler />后,会在Spring MVC上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,它会像一个检查员,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,就将该请求转由Web应用服务器默认的Servlet处理,如果不是静态资源的请求,才由DispatcherServlet继续处理。
3.2.5、全部配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置组件扫描 -->
<context:component-scan base-package="com.SpringMVC.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 启动SpringMVC注解 -->
<mvc:annotation-driven/>
<!--配视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/view/"/>
<property name="suffix" value=".html"/>
</bean>
<!-- 配置处理静态资源,交由default servlet去处理 -->
<mvc:default-servlet-handler/>
</beans>
现在,我们基本上已经可以开始使用Spring MVC构建Web应用了。此时,最大的问题在于,我们要构建的应用到底是什么。
3.3、登陆注册应用
现在我们模拟一个简单的登陆注册应用,用户在主页上可以点击登陆或者注册,并且进入到相应的界面,填写信息进行登陆或注册。
3.3.1、业务场景
- 首先程序启动后我们位于主页,主页上有两个按钮,登陆和注册按钮
- 点击登陆或注册按钮进入对应页面
- 填写登陆信息,并点击登陆按钮进行登陆
- 填写注册信息,并点击注册按钮进行注册
其实前端的部分代码,例如页面跳转,完全可以使用前端的技术,例如超链接啊之类的或者框架技术来实现。但是呢这篇文章主要以Java后端的SpringMVC框架为主。所以前端的东西一切从简。
接下来就该实现下面业务了,先来写后端的控制器。
4、编写控制器
首先创建 UserController 类作为控制器
@Controller
public class UserController {}
这里我们使用了@Controller注解,很显然这个注解是用来声明控制器的。但是声明控制器不一定非要用它,我们还可以使用 @Component 注解。在这里,它俩的目的就是辅助实现组件扫描并将其声明为Spring应用上下文中的一个bean。
但是使用 @Component 注解在表意性上可能会差一些,无法确定 UserController 是什么组件类型。我这里就选择使用 @Controller
现在来实现点击登陆按钮跳转到登陆页面的业务场景。
在UserController类中添加如下方法:
@RequestMapping(value = "goLoginPage", method = RequestMethod.POST)
public String goLogin(){
return "login";
}
goLogin()这个方法带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求路径,method属性细化了它所处理的HTTP方法。在本例中,当收到对“goLoginPage”的HTTP POST 请求时,就会调用goLogin()方法。
goLogin()方法其实并没有做太多的事情,它返回了一个String类型的 “login”。 这个String将会被Spring MVC解读为要渲染的视图名称,也叫做逻辑视图名。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。
根据我们配置视图解析器(InternalResourceViewResolver)的方式,视图名“login”将会解析为“/view/login.html”路径的一个HTML。
那么该如何在首页点击登陆按钮的时候,发送请求到后端指定的控制器呢?接下来将编写应用首页的前端代码。
4.1、首页的前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
h1{
text-align: center;
line-height: 100%;
}
.myForm{
display: inline-block;
margin-left: 20px;
margin-top: 20px;
}
.myForm input{
width:100px;
height:35px;
font-size: 18px;
}
#loginForm {
margin-left: 40%;
}
</style>
<body>
<h1>Welcome to Spring Leaning Demo</h1>
<form id="loginForm" class="myForm" action="goLoginPage" method="post">
<input type="submit" value="Login">
</form>
<form id="registForm" class="myForm" action="goRegistPage" method="post">
<input type="submit" value="Regist">
</form>
</body>
</html>
非常简单的首页界面,它应该是这样的:
可以注意到,在html中是使用表单标签form来是实现提交的。其中action指定了请求路径,这路径和前面的控制器的goLogin()方法上的@RequestMapping的value值一样。method表示请求的方式,这都和控制器保持了一致,只有保持一致才能正确的请求到对应的控制器
现在讲程序跑起来就可以在首页点击登陆按钮,并请求到我们写的控制器了。但是点击后报一个404的错误:
HTTP Status 404 - /view/login.html
为什么呢?因为我们在控制器里面的goLogin()方法中返回了一个逻辑视图名,视图解析器将会去寻找 "/view/login.html“ 文件,但是现在还没有呢,所以我们马上就要写一个登陆界面。
4.2、登陆界面的前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
h1{
text-align: center;
line-height: 100%;
}
.myForm div{
margin-top: 15px;
}
.myForm div input{
margin-left: 10px;
}
#username{
margin-left: 9px;
}
#password{
margin-left: 12px;
}
#confirm{
margin-left: -51px;
}
#phone{
margin-left: -29px;
}
#loginButton{
margin-top: 20px;
width:100px;
height:35px;
font-size: 18px;
margin-left: 9%;
}
#loginForm {
margin-left: 40%;
}
</style>
<body>
<h1>Welcome to user login page</h1>
<form id="loginForm" class="myForm" action="userLogin" method="post">
<div id="username">username:<input type="text" name="username"></div>
<div id="password">password:<input type="password" name="password"></div>
<input id="loginButton" type="submit" value="Login">
</form>
</body>
</html>
登陆界面展现的界面是这样的:
这时第一个业务场景就完成了,即在首页点击登陆按钮跳转到登陆界面。
注册的场景和登陆是一样的,首先在Java后端编写跳转到注册界面的代码:
@RequestMapping(value = "goRegistPage", method = RequestMethod.POST)
public String goRegist(){
return "regist";
}
4.3、编写注册界面的前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
h1{
text-align: center;
line-height: 100%;
}
.myForm div{
margin-top: 15px;
}
.myForm div input{
margin-left: 10px;
}
#username{
margin-left: 9px;
}
#password{
margin-left: 12px;
}
#confirm{
margin-left: -51px;
}
#phone{
margin-left: -29px;
}
#registButton{
margin-top: 20px;
width:100px;
height:35px;
font-size: 18px;
margin-left: 9%;
}
#registForm {
margin-left: 40%;
}
</style>
<body>
<h1>Welcome to user regist page</h1>
<form id="registForm" class="myForm" action="userRegist" method="post">
<div id="username">username:<input type="text" name="username"></div>
<div id="phone">phone number:<input type="text" name="phoneNumber"></div>
<div id="password">password:<input type="password" name="password"></div>
<div id="confirm">confirm password:<input type="password"></div>
<input id="registButton" type="submit" value="regist">
</form>
</body>
</html>
注册界面大概是长这样的:
接下来实现 填写登陆信息后,点击登陆的业务。
5、登陆
在此之前先让我们创建一个User实体对象。
5.1 创建实体对象
package com.SpringMVC.pojo;
public class User {
private int id;
private String username;
private String password;
private String phoneNumber;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
'}';
}
}
5.2、编写用户登陆的控制器代码
@RequestMapping(value = "userLogin", method = RequestMethod.POST)
@ResponseBody
public String userLogin(User user){
if(user != null && "admin".equals(user.getUsername()) && "123".equals(user.getPassword())){
return "Successful login";
}else {
return "Logon failure";
}
}
这段代码有几个点需要注意:
- 方法中的参数
- @ResponseBody注解
下面详解
5.2.1、@ResponseBody注解
作用
该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。
使用时机
返回的数据不是html标签的页面,而是其他某种格式的数据时(如json、xml等)使用;
在示例中可以看到如果不写@ResponseBody,DispatcherServlet 就会把 return 后面的字符串当作逻辑视图名来处理,并且回去寻找对应的视图。但是根本没有这些视图,我们要的也不是这这样的结果。
现在我们加了@ResponseBody,就会把return 后面的字符串当作字符串来处理了。
5.2.2、处理请求参数。
userLogin方法中带有User对象参数,这个User对象中的"username"和"password"字段将会和前端表单提交时Request请求携带的用户名和密码参数名相匹配,如何前端在传参数的时候,也是用的"username"和"password"这两个单词,那么SpringMVC将自动帮我把值填入User对象的相应字段中。
当然还有个更简单明了的写法:
@RequestMapping(value = "userLogin", method = RequestMethod.POST)
@ResponseBody
public String userLogin(String username, String password){
......
}
这样也会自动给匹配字段并且填入值。但是有个前提就是在前端代码中传递的参数名必须和后端方法中的参数名相同。现在回去看看登陆界面的前端代码,可以看出用户名和密码有个name属性,写了它将代表在请求时把标签中的值作为参数传递。
例如,填写登陆信息:test、123456
在Request请求中能看到是这样的:
这一点,教会了我们如何在SpringMVC中处理请求参数。
5.2.3、处理URL中的参数。
这只是处理参数中的一种情况, 另一种情况就是处理URL路径中的参数。
假设有个URL请求是这样的:http://localhost:8080/getUserInfo/6 这个6代表着userID。
那么后端应该怎样处理这个userID呢?
Spring MVC允许我们在@RequestMapping 路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意的值。
@RequestMapping(value = "getUserInfo/{userID}", method = RequestMethod.GET)
@ResponseBody
public User getUserInfoByID(@PathVariable("userID") String userID){
}
getUserInfoByID()方法的userID参数上添加了@PathVariable(“userID”)注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的userID参数中。如果对“/getUserInfo/3322”发送GET请求,那么将会把“3322”传递进来,作为userID值。
如果方法的参数名碰巧与占位符的名称相同,因此我们可以去掉@PathVariable中的value属性:
@RequestMapping(value = "getUserInfo/{userID}", method = RequestMethod.GET)
@ResponseBody
public User getUserInfoByID(String userID){
}
如果@PathVariable中没有value属性的话,它会假设占位符的名称与方法的参数名相同。这能够让代码稍微简洁一些,因为不必重复写占位符的名称了。
现在我们的登陆注册应用已经可以正常跑起来了。
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。