study springMVC 基础配置

接触java以来web相关的东西都是springboot开箱即用,基本上没有配置过springMVC,打算还是从0到1接触接触。

0x00 spring MVC是什么

  • 数据模型层(Model):模型对象拥有最多的处理任务,是应用程序的主体部分,它负责数据逻辑(业务规则)的处理和实现数据操作(即在数据库中存取数据)。

  • 视图层(View):负责格式化数据并把它们呈现给用户,包括数据展示、用户交互、数据验证、界面设计等功能。

  • 控制层(Controller):负责接收并转发请求,对请求进行处理后,指定视图并将响应结果发送给客户端。

网上随便搜索就有很多关于MVC的概念,在spring中,JavaBean作为数据分装(Model),JSP可用作数据显示(View),Servlet则是处理用户请求(Controller),刚好一一对应上述的MVC。

image-20220409235704925

0x01 快速启用

创建项目选择 Java Enterprise

image-20220409225544372

dependencies中选择以下两项:

image-20220409225923947

之后在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>

从上述配置我们可以知道三点

  1. 初始界面为/index.jsp
  2. 配置了一个名为 springmvc 的 Servlet,类型为 DispatcherServlet ,它就是 Spring MVC 的入口,并标记为容器启动后加载,即自启动。
  3. 通过 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即可看见:

image-20220412002135766

我们配置了最简单的mvc,后续我们慢慢基于它进行一些完善,不过在这之前有必要了解一下从第一个Servlet到返回视图这个过程经历了什么。

0x02 SpringMVC执行流程

image-20220410141300306

上图一共包含以下六个组件:

  1. DispatcherServlet:Spring MVC 的所有请求都要经过 DispatcherServlet 来统一分发。DispatcherServlet 相当于一个转发器或中央处理器,控制整个流程的执行。
  2. HandlerMapping:处理器映射器,其作用是根据请求的 URL 路径,通过注解或者 XML 配置,寻找匹配的处理器(Handler)信息。
  3. HandlerAdapter:处理器适配器,其作用是根据映射器找到的处理器(Handler)信息,按照特定规则执行相关的处理器(Handler)。
  4. Handler:处理器,也称为controller,和 Java Servlet 扮演的角色一致。其作用是执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装至 ModelAndView 对象中。
  5. View Resolver:视图解析器,其作用是进行解析操作,通过 ModelAndView 对象中的 View 信息将逻辑视图名解析成真正的视图 View(如通过一个 JSP 路径返回一个真正的 JSP 页面)。
  6. 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解析特性可以进行绕过:

0x07 参考