1.概述
Spring MVC 也叫 Spring Web MVC ,属于展示层框架。Spring MVC 是 Spring 框架的一部分。
Spring Web MVC 框架提供了 mvc (模型 – 视图 – 控制器) 架构和用于开发灵活和松散耦合的 Web 应用程序的组件。 MVC 模式导致应用程序的不同方面(输入逻辑,业务逻辑和 UI 逻辑)分离,同时提供这些元素之间的松散耦合。
- 模型 (Model):封装了应用程序数据,通常它们将由 POJO 类组成。
- 视图 (View):负责渲染模型数据,一般来说它生成客户端浏览器可以解释 HTML 输出。
- 控制器 (Controller):负责处理用户请求并构建适当的模型,并将其传递给视图进行渲染。
Dispatcher servlet 组件类
Spring Web MVC 框架是围绕 DispatcherServlet 设计的,它处理所有的 HTTP 请求 和 响应 。 Spring Web MVC DispatcherServlet 的请求处理工作流如下图所示:
以下是对应于到 DispatcherServlet 的传入 HTTP 请求的事件顺序:
- 在接收到 HTTP 请求后,DispatcherServlet 会查询 HandlerMapping 以调用相应的 Controller。
- Controller 接受请求并根据使用的 GET 或 POST 方法调用相应的服务方法。 服务方法将基于定义的业务逻辑设置模型数据,并将视图名称返回给 DispatcherServlet。
- DispatcherServlet 将从 ViewResolver 获取请求的定义视图。
- 当视图完成,DispatcherServlet 将模型数据传递到最终的视图,并在浏览器上呈现。
所有上述组件,即: HandlerMapping、Controller 和 ViewResolver 是 WebApplicationContext 的一部分,它是普通 ApplicationContext 的扩展,带有 Web 应用程序所需的一些额外功能。
2.配置和结构重构
我们继续以上一章节 Spring Web 中的案例项目 iot-admin2 为基础,复制一份重命名为 iot-admin3 ,修改 pom.xml 中 <artifactId>iot-admin3</artifactId> 。
接下来我们使用 Spring MVC 重构 iot-admin3 项目:
2.1.修改POM
将 spring-web 的依赖改为对 spring-webmvc 的依赖,同时删除对 spring-context 的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
完整的 pom.xml 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="#34;
xmlns:xsi="#34;
xsi:schemaLocation=" #34;>
<modelVersion>4.0.0</modelVersion>
<groupId>net.work100.training.stage2</groupId>
<artifactId>iot-admin3</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<spring.version>5.2.3.RELEASE</spring.version>
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
<javax.jstl.version>1.2</javax.jstl.version>
<junit.version>4.12</junit.version>
<slf4j.version>1.7.25</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet-api.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${javax.jstl.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
</project>
2.2.配置 web.xml
CharacterEncodingFilter
配置字符集过滤器,用于解决中文编码问题
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
DispatcherServlet
配置 Spring 的 Servlet 分发器处理所有 HTTP 的请求和响应
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring-mvc*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
load-on-startup 标记容器是否在启动的时候就加载这个servlet,当值为0或者大于0时,表示容器在应用启动时就加载这个servlet;当是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载;党委正数时,其值越小,启动该servlet的优先级越高。
完整 web.xml 代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="#34;
xmlns:xsi="#34;
xsi:schemaLocation=" #34;
version="4.0">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-context*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring-mvc*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
2.3.配置 spring-mvc.xml
在目录 src/main/resources 下新建文件 spring-mvc.xml ,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="#34; xmlns:xsi="#34;
xmlns:context="#34;
xmlns:mvc="#34;
xsi:schemaLocation="
#34;>
<description>Spring MVC Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:iot-admin.properties"/>
<!-- 使用 Annotation 自动注册 Bean,只扫描 @Controller -->
<context:component-scan base-package="net.work100.training.stage2.iot.admin" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 默认的注解映射的支持 -->
<mvc:annotation-driven />
<!-- 定义视图文件解析 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="${web.view.prefix}"/>
<property name="suffix" value="${web.view.suffix}"/>
</bean>
<!-- 静态资源映射 -->
<mvc:resources mapping="/ static /**" location="/static/" cache-period="31536000"/>
</beans>
相关配置说明:
- context:property-placeholder:动态加载属性配置文件以变量的方式引用需要的值
- context:component-scan:当前配置文件为 MVC 相关,故只需要扫描包含 @Controller 的注解即可,由于 spring-context.xml 配置文件中也配置了包扫描,所以还需要排除 @Controller 的注解扫描。
- InternalResourceViewResolver:视图文件解析器的一种,用于配置视图资源的路径和需要解释的视图资源文件类型,这里有两个需要配置的属性 prefix(前缀)以及 suffix(后缀)。prefix:配置视图资源路径,如:/WEB-INF/views/suffix:配置视图资源类型,如:.jsp
- mvc:resources:静态资源映射,主要用于配置静态资源文件存放路径,如:JS、CSS、Image 等
2.4.配置及结构完善
配置 iot-admin.properties
在 spring-mvc.xml 中,我们配置了 <context:property-placeholder ignore-unresolvable=”true” location=”classpath:iot-admin.properties”/> 用于动态加载属性配置文件,实际开发中我们会将系统所需的一些配置信息封装到 .properties 配置文件中便于统一的管理。
在目录 src/main/resources 下创建一个名为 iot-admin.properties 的配置文件,内容如下:
#============================#
#==== Framework settings ====#
#============================#
# views path
web.view.prefix=/WEB-INF/views/
web.view.suffix=.jsp
重构项目结构
在 WEB-INF 下新建目录 views ,然后将 .jsp 文件移动到 views 下。
在 webapp 下新建目录 static ,然后将 assets 文件夹及其所属文件移动到 static 下。
目录结构如下图:
2.5.修改 spring-context.xml
由于 spring-mvc.xml 中已经配置了 @Controller 注解的扫描,而 spring-context.xml 中配置的是扫描全部注解,故在这里需要将 @Controller 注解的扫描配置排除。
修改 spring-context.xml 配置,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="#34;
xmlns:xsi="#34;
xmlns:context="#34;
xsi:schemaLocation=" #34;>
<context:annotation-config/>
<!-- 使用 Annotation 自动注册 Bean,在主容器中不扫描 @Controller 注解,在 SpringMVC 中只扫描 @Controller 注解。-->
<context:component-scan base-package="net.work100.training.stage2.iot.admin">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
3.Controller控制器重构
3.1.修改注解方式
UserDaoImpl 类
修改 UserDaoImpl 类的注解方式,代码如下:
package net.work100.training.stage2.iot.admin.dao.impl;
import net.work100.training.stage2.iot.admin.dao.UserDao;
import net.work100.training.stage2.iot.admin.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
/**
* <p>Title: UserDaoImpl</p>
* <p>Description: </p>
* <p>Url:
*
* @author liuxiaojun
* @date 2020-02-13 13:23
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-13 liuxiaojun 初始创建
* -----------------------------------------------
*/@Repository
public class UserDaoImpl implements UserDao {
private static final Logger logger = LoggerFactory.getLogger(UserDaoImpl.class);
public User getUser(String loginId, String loginPwd) {
logger.debug("调用方法 getUser(loginId:{}, loginPwd:{})", loginId, loginPwd);
// 根据 loginId 查询出用户信息
User user = getUserByLoginId(loginId);
if (user != null) {
// 验证 loginPwd 是否正确(区分大小写)
if (user.getLoginPwd().equals(loginPwd)) {
return user;
}
}
return null;
}
/**
* 获取模拟的用户数据
*
* @param loginId 登录ID
* @return
*/ private User getUserByLoginId(String loginId) {
// 模拟 DB 存在的用户数据
User dbUser = new User();
dbUser.setUserName("Xiaojun");
dbUser.setLoginId("admin");
dbUser.setLoginPwd("admin");
// 判断是否存在 loginId 的用户(忽略大小写)
if (dbUser.getLoginId().equalsIgnoreCase(loginId)) {
logger.info("匹配上用户:{}", dbUser);
return dbUser;
}
logger.warn("未匹配任何用户,将返回 null");
return null;
}
}
UserServiceImpl 类
修改 UserServiceImpl 类的注解方式,代码如下:
package net.work100.training.stage2.iot.admin.service.impl;
import net.work100.training.stage2.iot.admin.dao.UserDao;
import net.work100.training.stage2.iot.admin.entity.User;
import net.work100.training.stage2.iot.admin.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>Title: UserServiceImpl</p>
* <p>Description: </p>
* <p>Url:
*
* @author liuxiaojun
* @date 2020-02-13 13:26
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-13 liuxiaojun 初始创建
* -----------------------------------------------
*/@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
public User login(String loginId, String loginPwd) {
return userDao.getUser(loginId, loginPwd);
}
}
注解说明
- @Repository :不需要指定名称,因为只有一个实现类
- @Service : 不需要指定名称,因为只有一个实现类
- @Autowired :自动注入,Spring 自动寻找实现类来实例化对象
3.2.重构 Controller 代码
修改 LoginController
修改 LoginController 类,代码如下:
package net.work100.training.stage2.iot.admin.web.controller;
import net.work100.training.stage2.iot.admin.entity.User;
import net.work100.training.stage2.iot.admin.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* <p>Title: LoginController</p>
* <p>Description: </p>
* <p>Url:
*
* @author liuxiaojun
* @date 2020-02-13 13:28
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-13 liuxiaojun 初始创建
* -----------------------------------------------
*/@Controller
public class LoginController {
@Autowired
private UserService userService;
/**
* 登录页面
*
* @return
*/ @RequestMapping(value = {"", "login"}, method = RequestMethod.GET)
public String login() {
return "login";
}
/**
* 登录逻辑
*
* @param loginId 登录ID
* @param loginPwd 登录密码
* @return
*/ @RequestMapping(value = "login", method = RequestMethod.POST)
public String login(@RequestParam(required = true) String loginId, @RequestParam(required = true) String loginPwd) {
User user = userService.login(loginId, loginPwd);
// 登录成功
if (user != null) {
return "redirect:main";
}
// 登录失败
else {
return login();
}
}
}
新建 MainController
新建 MainController 类,代码如下:
package net.work100.training.stage2.iot.admin.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* <p>Title: MainController</p>
* <p>Description: </p>
*
* @author liuxiaojun
* @date 2020-02-20 15:19
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-20 liuxiaojun 初始创建
* -----------------------------------------------
*/@Controller
public class MainController {
/**
* 主页
*
* @return
*/ @RequestMapping(value = "main", method = RequestMethod.GET)
public String main() {
return "main";
}
}
运行
重新启动 Tomcat 验证效果。
注解说明
4.使用拦截器
4.1.简介
Spring Web MVC 的处理器拦截器,类似于 Servlet 开发中的过滤器 Filter ,用于对处理器进行预处理和后处理。
4.2.应用场景
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等
- 权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面
- 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间
- 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现
4.3.如何使用拦截器
4.4.创建登录拦截器
我们知道对系统的相关操作是需要登录后才可以使用的,当未登录时是无法直接访问需要登录权限的操作的,为了做到这个效果,我们使用登录拦截器来判断用户是否登录,如果用户已登录则放行让用户继续操作,否则就将其跳转到登录页。
在 net.work100.training.stage2.iot.admin.web 包下新建一个包 interceptor ,然后在 interceptor 包下定义一个名为 LoginInterceptor 的拦截器,代码如下:
package net.work100.training.stage2.iot.admin.web.interceptor;
import net.work100.training.stage2.iot.admin.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Title: LoginInterceptor</p>
* <p>Description: </p>
* <p>Url:
*
* @author liuxiaojun
* @date 2020-02-20 16:22
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-20 liuxiaojun 初始创建
* -----------------------------------------------
*/public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginInterceptor");
User user = (User) request.getSession().getAttribute("user");
// 未登录
if (user == null) {
response.sendRedirect("/login");
}
// 为 true 时放行,进入 postHandle
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
LoginInterceptor 拦截器的主要功能是检测用户是否登录,如果未登录则跳转至登录页(/login)
4.5.在 spring-mvc.xml 中配置拦截器
拦截器定义后还需要在 spring-mvc.xml 中配置拦截器,代码如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="net.work100.training.stage2.iot.admin.web.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
相关配置说明:
4.6.完善使用场景
我们实际的使用场景中还应该有个判断逻辑:当用户已经登录了,访问 /login 时,需要将请求跳转至 /main
创建拦截器 PermissionInterceptor
这时我们需要再创建一个拦截器 PermissionInterceptor ,代码如下:
package net.work100.training.stage2.iot.admin.web.interceptor;
import net.work100.training.stage2.iot.admin.entity.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Title: PermissionInterceptor</p>
* <p>Description: </p>
* <p>Url:
*
* @author liuxiaojun
* @date 2020-02-20 21:16
* ------------------- History -------------------
* <date> <author> <desc>
* 2020-02-20 liuxiaojun 初始创建
* -----------------------------------------------
*/public class PermissionInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("PermissionInterceptor");
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if ("login".equals(modelAndView.getViewName())) {
User user = (User) request.getSession().getAttribute("user");
if (user != null) {
response.sendRedirect("/main");
}
}
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
修改 spring-mvc.xml 配置
增加 PermissionInterceptor 拦截器的配置,代码如下:
<!-- 拦截器配置(拦截器的执行顺序为:从下往上) -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="net.work100.training.stage2.iot.admin.web.interceptor.LoginInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="net.work100.training.stage2.iot.admin.web.interceptor.PermissionInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
拦截器的执行顺序为:从下往上,即先定义的后执行
spring-mvc.xml 完整配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="#34; xmlns:xsi="#34;
xmlns:context="#34;
xmlns:mvc="#34;
xsi:schemaLocation="
#34;>
<description>Spring MVC Configuration</description>
<!-- 加载配置属性文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:iot-admin.properties"/>
<!-- 使用 Annotation 自动注册 Bean,只扫描 @Controller -->
<context:component-scan base-package="net.work100.training.stage2.iot.admin" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- 默认的注解映射的支持 -->
<mvc:annotation-driven/>
<!-- 定义视图文件解析 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="${web.view.prefix}"/>
<property name="suffix" value="${web.view.suffix}"/>
</bean>
<!-- 静态资源映射 -->
<mvc:resources mapping="/static/**" location="/static/" cache-period="31536000"/>
<!-- 拦截器配置(拦截器的执行顺序为:从下往上) -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="net.work100.training.stage2.iot.admin.web.interceptor.LoginInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="net.work100.training.stage2.iot.admin.web.interceptor.PermissionInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
</beans>
4.7.运行验证
重启 Tomcat ,分别验证 LoginInterceptor 和 PermissionInterceptor 拦截器是否生效。
5.实例源码
实例源码已经托管到如下地址: