接触java以来web相关的东西都是springboot开箱即用,基本上没有配置过springMVC,打算还是从0到1接触接触。
0x00 spring MVC是什么
-
数据模型层(Model):模型对象拥有最多的处理任务,是应用程序的主体部分,它负责数据逻辑(业务规则)的处理和实现数据操作(即在数据库中存取数据)。
-
视图层(View):负责格式化数据并把它们呈现给用户,包括数据展示、用户交互、数据验证、界面设计等功能。
-
控制层(Controller):负责接收并转发请求,对请求进行处理后,指定视图并将响应结果发送给客户端。
网上随便搜索就有很多关于MVC的概念,在spring中,JavaBean作为数据分装(Model),JSP可用作数据显示(View),Servlet则是处理用户请求(Controller),刚好一一对应上述的MVC。
0x01 快速启用
创建项目选择 Java Enterprise
在dependencies中选择以下两项:
之后在pom.xml
中添加:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<junit.version>5.8.1</junit.version>
<spring.version>4.3.6.RELEASE</spring.version>
</properties>
......
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>8.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.mvc</groupId>
<artifactId>javax.mvc-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>3.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.2.13.RELEASE</version>
</dependency>
</dependencies>
初始化成功后,我们看看当前的结构是怎样的:
index.jsp :
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>
HelloServlet :
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
默认访问 index.jsp,点击链接可以跳转到HelloServlet,但这并不是mvc ,只是简单的jsp+servlet跳转。
下面我们在 /WEB-INF/jsp/
下创建 View: login.jsp (内容无所谓)
WEB-INF是Java的WEB应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。如果想在页面中直接访问其中的文件,必须通过web.xml文件对要访问的文件进行相应映射才能访问。
创建一个控制器:
@Controller
public class LoginController {
@RequestMapping("/login")
public ModelAndView login(){
return new ModelAndView("/WEB-INF/jsp/login.jsp");
}
@RequestMapping("/login2")
public String login2(){
return "/WEB-INF/jsp/login.jsp";
}
}
这两种方式返回视图原理上是一样的,只是后续和model结合写法会有所不一样,看个人习惯。
这时候运行会发现访问全部404,这是因为我们还没有配置xml文件,也是springMVC相比springBoot而言最麻烦的一点…
Spring MVC 是基于 Servlet 的,DispatcherServlet 是整个 Spring MVC 框架的核心,主要负责截获请求并将其分派给相应的处理器处理(后续会慢慢介绍)。所以配置 Spring MVC,首先要定义 DispatcherServlet,也就是在web.xml下进行配置。
web.xml:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>springMVC</display-name>
<welcome-file-list>
<!--入口文件-->
<!--实际存在的物理文件地址,不能将首页设置成Servlet或Controller的地址-->
<!--可以选择不设置,默认访问 / ,然后把controller绑定在上面-->
<welcome-file>/index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 表示容器再启动时立即加载servlet -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- 处理所有URL -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
从上述配置我们可以知道三点
- 初始界面为/index.jsp
- 配置了一个名为 springmvc 的 Servlet,类型为 DispatcherServlet ,它就是 Spring MVC 的入口,并标记为容器启动后加载,即自启动。
- 通过 servlet-mapping 映射到
/
,即 DispatcherServlet 会截获并处理该项目的所有 URL 请求。
这只是一个初始化定义,接下来我们需要根据配置的servlet(springmvc),创建其对应的配置文件,该配置文件命名规则为 <servlet-name>-servlet.xml,上述例子也就是 springmvc-servlet.xml :
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
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.example.springmvc.controller"/>
</beans>
目前这里的有用代码只是一句 <context:component-scan base-package="com.example.springmvc.controller"/>
,表明扫描包com.example.springmvc.controller
下所有的控制器,根据其注解所对应的路由,并将其生命周期纳入Spring管理。
不使用注解的话,也可以手动把控制器和路由进行绑定
<bean name="/" class="com.example.springmvc.controller.LoginController"/>
这样启动后,访问/login
,就可以跳转到/WEB-INF/jsp/login.jsp
。
不过你会发现在controller中每次跳转到jsp都需要写一个完整路径,可以在springmvc-servlet.xml
中通过视图解析器(ViewResolver)匹配一个定义的视图:
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--前缀 -->
<property name="prefix" value="/WEB-INF/jsp/" />
<!--后缀 -->
<property name="suffix" value=".jsp" />
</bean>
之后controller可改为:
@Controller
public class LoginController {
@RequestMapping("/login")
public String login(Model model){
model.addAttribute("name","theoyu");
return "login";
}
}
这里我们随意引入了Model对象,在视图中可以通过el表达式${name}
取出来
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Login</title>
</head>
<body>
This is Login Page , Hello ${name}
</body>
</html>
部署后,访问/login
即可看见:
我们配置了最简单的mvc,后续我们慢慢基于它进行一些完善,不过在这之前有必要了解一下从第一个Servlet到返回视图这个过程经历了什么。
0x02 SpringMVC执行流程
上图一共包含以下六个组件:
- DispatcherServlet:Spring MVC 的所有请求都要经过 DispatcherServlet 来统一分发。DispatcherServlet 相当于一个转发器或中央处理器,控制整个流程的执行。
- HandlerMapping:处理器映射器,其作用是根据请求的 URL 路径,通过注解或者 XML 配置,寻找匹配的处理器(Handler)信息。
- HandlerAdapter:处理器适配器,其作用是根据映射器找到的处理器(Handler)信息,按照特定规则执行相关的处理器(Handler)。
- Handler:处理器,也称为controller,和 Java Servlet 扮演的角色一致。其作用是执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装至 ModelAndView 对象中。
- View Resolver:视图解析器,其作用是进行解析操作,通过 ModelAndView 对象中的 View 信息将逻辑视图名解析成真正的视图 View(如通过一个 JSP 路径返回一个真正的 JSP 页面)。
- View:视图,其本身是一个接口,实现类支持不同的 View 类型(JSP、FreeMarker、Excel 等)。
具体流程在org.springframework.web.servlet.DispatcherServlet#doDispatch,跟进一遍就非常明了。
0x03 重定向和转发
Spring MVC 请求方式分为转发、重定向 2 种,分别使用 forward 和 redirect 关键字在 controller 层进行处理。
- 转发(forward):客户端发送http请求,web服务器接受,调用内部方法在容器内部将请求转发给另外一个视图或者处理请求(之前request中存放的消息不会消失),最后将目标资源发送给客户端。
- 重定向(redirect):客户浏览器发送 http 请求,Web 服务器接受后发送 302 状态码响应及对应新的 location 给客户浏览器,客户浏览器发现是 302 响应,则自动再发送一个新的 http 请求(之前请求request的消息全部失效)。
//转发给视图
return "test";
//转发给一个请求方法
return "forward:/test";
//重定向到一个请求方法
return "redirect:/test"
0x04 参数传递
说到请求肯定就少不了传参,一般来说springMVC有以下几种方式:
- 通过处理方法的形参接收请求参数
- 通过 @RequestParam 接收请求参数
- 通过 HttpServletRequest 接收请求参数
- 通过 @PathVariable 接收 URL 中的请求参数
- 通过实体 Bean 接收请求参数
- 通过 @ModelAttribute 接收请求参数
通过处理方法的形参接收请求参数
该方法就是把表单的参数直接写在控制器对应方法的形参中,要求形参名称与请求参数名称完全相同。
@RequestMapping(value = "/login1")
public String login1(String username,Model model){
model.addAttribute("name",username);
return "login";
}
通过 @RequestParam 接收请求参数
在控制器对应方法入参处使用 @RequestParam 注解指定其对应的请求参数。@RequestParam 有以下三个参数:
- value:参数名
- required:是否必须,默认为 true,表示请求中必须包含对应的参数名,若不存在将抛出异常
- defaultValue:参数默认值
@RequestMapping(value = "/login2")
public String login2(@RequestParam (required = false,defaultValue = "Theoyu")String username, Model model){
model.addAttribute("name",username);
return "login";
}
RequestParam和直接形参传入差别不大,多了一些可以自定义的限制,包括默认情况下请求参数和接受参数不一样会报404错误。
通过 HttpServletRequest 接收请求参数
@RequestMapping(value = "/login3")
public String login3(HttpServletRequest request, Model model){
String username = request.getParameter("username");
model.addAttribute("name",username);
return "login";
}
通过 @PathVariable 接收 URL 中的请求参数
从请求路由中解析
@RequestMapping(value = "/login4/{username}")
public String login4(@PathVariable String username, Model model){
model.addAttribute("name",username);
return "login";
}
通过实体 Bean 接收请求参数
首先创建一个User实体类
package com.example.springmvc.entity.user;
public class User {
private String username;
private String password;
public String getPassword() {
return password;
}
public String getUsername(){
return username;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
}
get和post传入都可以,表单参数需要和bean属性对应。
@RequestMapping(value = "/login5")
public String login5(User user, Model model){
model.addAttribute("name",user.getUsername());
return "login";
}
通过 @ModelAttribute 接收请求参数
@ModelAttribute作用在方法的参数上时,会自动把数据和model进行绑定(省去了model.addAttribute(x,x)
这一步)
@RequestMapping(value = "/login6")
public String login6(@ModelAttribute("user")User user, Model model){
return "login";
}
不过这里需要修改一下view:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Login</title>
</head>
<body>
This is Login Page , Hello ${user.username}
</body>
</html>
0x05 service注解
@Service注解会将标注类自动注册到 Spring 容器中。
@Autowired注解可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。
一般service层多用作业务模块的逻辑设计。
接口:
public interface UserServices {
boolean isAdmin(User user);
}
实现类:
@Service
public class UserServicesImpl implements UserServices{
@Override
public boolean isAdmin(User user) {
if (user.getUsername().equals("admin")){
return true;
}
return false;
}
}
admin下进行判断
@Autowired
private UserServices userServices;
@RequestMapping(value = "/admin")
public String admin(HttpSession session){
if (userServices.isAdmin((User) session.getAttribute("user"))){
return "admin";
}
return "redirect:/login";
}
0x06 Interceptor 拦截器
拦截器其实不算陌生,之前spring内存木马就有Interceptor类型。这里简单写一个权限校验的拦截器:
public class AdminInterceptor implements HandlerInterceptor {
@Autowired
private UserServices userServices;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
String url = httpServletRequest.getRequestURI();
HttpSession session = httpServletRequest.getSession();
if(url.startsWith("/admin")){
if(!userServices.isAdmin((User) session.getAttribute("user"))){
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
判断请求路由是否为/admin
开头,是则从session中判断是否为admin用户。
不过tomcat下使用url解析特性可以进行绕过: