[spring] Spring MVC & Thymeleaf(上)

[spring] Spring MVC & Thymeleaf(上)

本章内容主要过一下简单的 Spring MVC 的案例

简单来说,spring mvc 就是比较传统的网页开发流程,目前 boot 是可以比较轻松的配置 thymeleaf------毕竟 spring boot 内置对 thymeleaf 的支持

thymeleaf 是一个模板引擎,目前看起来是简单很多------我还依稀记得当年使用 JSP 写 spring mvc 的日子,那写的是真的很痛苦......

简单的 demo

这里就放一个超简单的 demo,能让页面跑起来就行

spring boot initializer

这里主要用的就是这 4 个包,下里用的案例会多一个依赖

实现简单 demo

因为是 spring mvc,所以肯定是需要有 Mocel-View-Controller 三个实现的。不过这个 demo 里面因为不涉及到数据的交互,所以没有 model

具体实现如下:

  • controller

    java 复制代码
    package com.example.thymeleafdemo.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class DemoController {
        // create a mapping for "/hello"
        @GetMapping("/hello")
        public String sayHello(Model model) {
            model.addAttribute("date", java.time.LocalDateTime.now());
    
            return "helloworld";
        }
    }
    • @Controller 的实现都比较熟悉了,这代表这是一个 Controller 的注解。因为这不是一个前后端分离的 rest api 实现,所以这里不需要使用 @RestController,也不需要做 request mapping

    • @GetMapping 这个就比较熟悉了,当用户访问 http://localhost:8080/hello 是就会调用当前方法

    • sayHello 是方法名,它返回的是一个字符串,而这个字符串代表着 view 的名字。以 thymeleaf 为例,spring mvc 会找到对应的模板渲染 view 层,这个也是下面会提到的

      实现的模板引擎的文件名为这里返回的字符串,即 helloworld.html

    • Model model 是 spring 在察觉到当前方法是 controller 的方法时,自动进行绑定的参数。view 层可以直接调用 model 里被添加的属性------model.addAttribute("date", java.time.LocalDateTime.now());,也就是 date 这个属性

  • view

    view 层具体在的位置位于 resources/templates 下:

    实现为:

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
        <meta charset="UTF-8" />
        <link rel="stylesheet" th:href="@{/css/demo.css}" />
        <title>Title</title>
      </head>
      <body>
        <p th:text="'Time on the server is ' + ${date}" class="funny"></p>
    
        <script src="http://localhost:35729/livereload.js"></script>
      </body>
    </html>

    这里的的视线相对比较简单,基础的 HTML 就不谈了

    • xmlns:th="http://www.thymeleaf.org 是 thymeleaf 的命名空间,也就是说下面都可以通过 th:<attr> 使用 thymeleaf 特有的属性

    • <link rel="stylesheet" th:href="@{/css/demo.css}" /> 使用的就是 thymeleaf 的语法,这里会自动解析路径去寻找对应的 css 文件,也就是在 static/css/demo.css 这个文件

    • <p th:text="'Time on the server is ' + ${date}" class="funny"></p> 这里用的是一个新的 thymeleaf 的语法,th:text 可以将后面的表达式,也就是 'Time on the server is ' + ${date} 写入到 element 中

      ${date} 就是在 controller 中传到 model 的属性,可以通过 ${} 的方式获得

    • <script src="http://localhost:35729/livereload.js"></script> 是一个热更新的脚本,在开启了 devtool 之后可以实现保存后 HTML 页面自动更新的功能

      换言之不需要刷新页面,也不需要重启服务器......就是稍微有些慢......

效果如下:

简述 MVC 的工作原理

这张图可以描述 spring mvc 的流程是什么样的

首先,浏览器会访问 front controller 组件------spring mvc 中一般指的是 DispatcherServlet,它会:

  • 集中处理所有的 HTTP 请求,并通过 URL mapping,将对应的 http 请求委托给对应的 handler/controller 进行处理
  • 解析 view 层
  • 处理 model,并与 model 和 view 层进行交互,实现 MVC 整体的数据交互

一般来说,front controller, controller, model, view 是四个比较高度抽象化的概念,它们的实施可以通过具体的组件去实现,如:

  • DispatcherServlet 是 front controller 的组件
  • @Controller & @RestController 是 controller 的 bean/class
  • @Model 显而易见的是 model 的组件
  • JSP/Thymeleaf 是 view 的组件等
  • spring 配置文件,如 XML,注解,java 配置文件等,也是 spring 的组件

总体来说,这些组件 spring 队伍已经进行实现完毕,并且内部完成了对应的配置,所以开发需要做的事情就是使用这些组件,将具体的业务填写完毕即可,如:

  • model

    声明必要的 entity 并添加对应的属性;通过 front controller 进行 controller 层和 view 层的沟通

  • view

    实现 thymeleaf/jsp 等支持的引擎模板,从 front controller 获取数据,并进行对应的处理以完成 UI 层面的渲染

  • controller

    与 service 层进行沟通,获取并处理对应的数据,并将其处理为 view 层所需的 model 送到 front controller 去

    一般来说,业务逻辑会从 controller 中抽离出来,以保证 SPR 和代码的低耦合性,不过这篇笔记不会涉及到 crud 的操作,因此可以暂时忽略 service 层

表单 demo

下面写一个 MVC 之间互动的 demo,这样可以更好理解上一个部分中比较抽象的概念

业务逻辑如下:
request mapping via /processForm with data view controller

这里省略掉了 front controller 的存在,毕竟这部分是 spring 已经实现好并且封装起来的功能

具体的实现流程如下:

  1. 创建 controller

  2. 展示 view 层,即渲染引擎模板

    这里具体要实现的功能也分为两步:

    1. 创建对应的 controller 方法去显示 html 表单
    2. 创建 HTML 模板去显示页面

    这两步必须要全部实现,才能通过访问对应的 URL 渲染对应的模板引擎

  3. 处理 HTML 表单

    根据上面的流程图所说,view 需要通过 /processForm 去和 controller 进行交互,这个过程中,view 会将用户填写的数据传给 controller

    这一步处理的过程和上面大致是一样的逻辑:

    1. 创建对应的 controller 方法去处理传来的数据,并显示处理完的页面

    2. 创建 HTML 模板去显示页面

controller 初始代码

java 复制代码
package com.example.thymeleafdemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HelloWorldController {
    // need a controller method to show initial HTML form
    @RequestMapping("/showForm")
    public String showForm() {
        return "helloworld-form";
    }

    // need a controller method to process HTML form
    @RequestMapping("/processForm")
    public String processForm() {
        return "helloworld";
    }
}

这里没有什么特别复杂的地方,和上面简单 demo 提到的一样

模板引擎初始代码

这里要实现的是两个模板引擎,一个是 helloworld-form.html,用来显示表单,让用户填写数据;一个是 helloworld,这是 controller 收集了用户提交的数据后,新定向的页面,这里将会显示用户输入的数据

具体实现如下:

  • helloworld-form

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
        <meta charset="UTF-8" />
        <title>Hello World - Input Form</title>
      </head>
      <body>
        <form th:action="@{/processForm}" method="get">
          <input type="text" name="studentName" placeholder="Student Name" />
          <input type="submit" value="Submit" />
        </form>
    
        <script src="http://localhost:35729/livereload.js"></script>
      </body>
    </html>
  • helloworld

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
        <meta charset="UTF-8" />
        <title>Title</title>
      </head>
      <body>
        Hello World of Spring!
        <br />
        <br />
    
        Student name: <span th:text="${param.studentName}"></span>
    
        <script src="http://localhost:35729/livereload.js"></script>
      </body>
    </html>

这里简单的过一遍两个模板都做了什么,第一个 helloworld-form 会渲染一个表单,用户可以通过表单输入数据,并且提交数据。这里提交的方式是 get,所以提交的数据会添加到 URL 路径上;同时 action 中的路径是 @{/processForm},这也是 thymeleaf 的语法,表示会 map 到对应的 /processForm,也就是 controller 中的 @RequestMapping("/processForm") 对应的方法

helloworld 会渲染已经处理好的数据,并且通过 URL 获取用户提交的数据并将其渲染到页面上,效果如下:

使用 model

之前是直接使用 query parameter 通过 URL 去进行数据传递,不过这只能用在 GET 方法上,如果要使用 POST,那么就需要使用 request body 进行和 controller 的沟通

这个情况下,controller 可以通过 HttpServletRequest 或者 @RequestParam("attributeName") 的方式,从模板引擎那里获取对应的资料,具体实现方式如下:

controller 修改:

java 复制代码
    // need a controller method to read from data and add data to the model
    @RequestMapping("/processFormV2")
    public String formWithModel(HttpServletRequest request, Model model) {
        // read req param from html form
        String name = request.getParameter("studentName");
        // convert data to all caps
        name = name.toUpperCase();
        // create the message
        String result = "Yo! " + name;
        // add message to the model
        model.addAttribute("message", result);
        return "helloworld";
    }

注意这里是通过 HttpServletRequest 获取对应的数据,HttpServletRequest 通过 dependency injection 动态完成注入的。在使用 GetMapping 的情况下,HttpServletRequest 会从 URL 的 query string 上获取对应数据;在 PostMapping 的情况下,HttpServletRequest 会从 request body 中动态获取,默认的格式为 Content-Type: application/x-www-form-urlencoded

⚠️:我这里用的是 @RequestMapping("/processFormV2"),最好是使用单独的 PostMappingGetMapping 去增强安全性

HTML 部分省略了,主要就是修改一下 form 请求的地址,变更为 processFormV2,随后就是获取信息的方式为 The message: <span th:text="${message}"></span>

完成修改后的结果如下:

绑定 request params

这是上面提到的,使用注解的方式获取信息:

java 复制代码
    // need a controller method to read from data and add data to the model
    @RequestMapping("/processFormV3")
    public String formWithModel(@RequestParam("studentName") String name, Model model) {
        // convert data to all caps
        name = name.toUpperCase();
        // create the message
        String result = "Using Annotation! " + name;
        // add message to the model
        model.addAttribute("message", result);
        return "helloworld";
    }

HTML 模板方面,同样将指向的地址从 v2 修改到 v3 就可以,效果如下:

GetMapping & PostMapping

这是上面提到的安全性问题,从开发实现的角度来说,其实不太需要特别在意 spring 底层是怎么完成依赖注入的。不过从语义化开发和安全性的角度,分别使用 @PostMapping@GetMapping 还是挺重要的,一般来说:

  • GET 是用来获取数据的(Retrieve),而 POST 是用来发送数据的(Create, Update, Delete)

  • GET 的数据传送通过 URL Query Param,而 POST 通过 body request

  • GET 的安全性更低,而 POST 安全性更高

  • GET 可以被用来保存书签,而 POST 不可以

  • GET 长度限制比较大,而 POST 的长度限制比较小

    一般情况下 GET 是够用的,毕竟好像有 8000 个左右的字符,我只遇到过一个情况被后台拒绝了......那就是老板想要穷举一堆 AND/OR 的操作让后台可以直接拼接到数据库了去搜索,结果就......超过限制了......

目前 HTML 模板中使用的都是 GET ,如果 Spring 这里使用 @PostMapping 的话,那么 spring 就会抛出 method not allow 的错:

所以这部分还是要注意的,如果特地规范了 @PostMapping@GetMapping,那么 HTML 部分也要进行对应的更新,如:

java 复制代码
    @PostMapping("/showForm")
    public String showForm() {
        return "helloworld-form";
    }

以及

java 复制代码
    @GetMapping("/processFormV3")
    public String formWithModel(@RequestParam("studentName") String name, Model model) {
        // convert data to all caps
        name = name.toUpperCase();
        // create the message
        String result = "Using Annotation! " + name;
        // add message to the model
        model.addAttribute("message", result);
        return "helloworld";
    }

更换成 POST 的效果展现如下:

这里数据就不会从 URL 中传递,反而是通过 request body:

数据绑定

前面一直手动获取 request body 中的数据,不过,其实 spring 也提供数据绑定,回顾一下这张图:

数据绑定就是直接对数据进行一个预处理,将数据绑定到对应的 POJO 上,省去了很多的手动操作。下面是具体的实现:

实现一个 POJO:

java 复制代码
package com.example.thymeleafdemo.model;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@NoArgsConstructor
@ToString
public class Student {
    private String firstName;
    private String lastName;
}

更新 controller,使用新注解 @ModelAttribute

java 复制代码
package com.example.thymeleafdemo.controller;

import com.example.thymeleafdemo.model.Student;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class StudentController {
    @GetMapping("/showStudentForm")
    public String showForm(Model model) {
        // create a student obj
        Student student = new Student();
        // add student obj to the model
        model.addAttribute("student", student);
        return "student-form";
    }

    @PostMapping("/processStudentForm")
    public String processForm(@ModelAttribute("student") Student student) {
        // log the input data
        System.out.println("student: " + student.toString());

        return "student-confirmation";
    }
}

⚠️:这里 @ModelAttribute("student") 的名字,必须要和下面 thymeleaf 中的 object 一致

更新 HTML 模板:

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Student Form</title>
  </head>
  <body>
    <h3>Student Registration Form</h3>

    <form
      th:action="@{/processStudentForm}"
      th:object="${student}"
      method="post"
    >
      First Name: <input type="text" th:field="*{firstName}" />

      <br /><br />

      Last Name: <input type="text" th:field="${student.lastName}" />

      <br /><br />

      <input type="submit" value="Submit" />

      <script src="http://localhost:35729/livereload.js"></script>
    </form>
  </body>
</html>

⚠️:这里的 th:object="${student}" 就是 @ModelAttribute("student") 中的 student,这里的名字必须一致

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Student Confirmation</title>
  </head>
  <body>
    <h3>Student Confirmation</h3>

    The student is confirmed:
    <span th:text="${student.firstName} + ' ' + ${student.lastName}"></span>

    <script src="http://localhost:35729/livereload.js"></script>
  </body>
</html>

⚠️:这里的 student 是手动通过 model.addAttribute("student", student); 进行绑定的

最终效果如下:

thymeleaf 属性

这里新增一些比较常用的 thymeleaf 的表单属性的用法

下拉框

即 dropdown,一个比较死板的写法如下:

html 复制代码
<select th:field="*{country}">
  <option th:value="Brazil">Brazil</option>
  <option th:value="France">France</option>
  <option th:value="Germany">Germany</option>
  <option th:value="India">India</option>
</select>

在显示的页面新增代码:

html 复制代码
<br /><br />

Country: <span th:text="${student.country}"></span>

这时候更新一下 学生 这个 POJO,新增一个 country 的属性:

java 复制代码
public class Student {
    private String firstName;
    private String lastName;
    private String country;
}

因为使用了数据绑定,所以 controller 部分的代码不需要进行任何的变动,实现效果如下:

动态获取下拉框

上面的代码是写死的,不过有的情况下,可能需要通过 properties 文件获取一些数据,这个时候就没有办法在 thymeleaf 写死所有的选项了

properties 文件更新如下:

properties 复制代码
countries=Brazil,France,Germany,India,Mexico,Spain,United States

这里新增的是一个关于国家的数组

这个时候就需要更新 controller 了,需要从 properties 文件中获取对应的国家,并且将其传到模板引擎中:

java 复制代码
    @Value("${countries}")
    private List<String> countries;

    @GetMapping("/showStudentForm")
    public String showForm(Model model) {
        // create a student obj
        Student student = new Student();
        // add student obj to the model
        model.addAttribute("student", student);

        // add the list of countries to the model
        model.addAttribute("countries", countries);
        return "student-form";
    }

HTML 部分更新如下:

html 复制代码
<select th:field="*{country}">
  <option
    th:each="tempCountry : ${countries}"
    th:value="${tempCountry}"
    th:text="${tempCountry}"
  ></option>
</select>

这里使用的是一个 thymeleaf 的 for 循环,最终展示效果如下:

单选

即 radio button,这里也通过两个方式实现,一个就是写死值的方式,另一个是通过 properties 文件导入

写死的方式如下:

html 复制代码
Favorite Programming Language:

<label>
  <input type="radio" th:field="*{favoriteLanguage}" th:value="Go" />
  Go
</label>
<label>
  <input type="radio" th:field="*{favoriteLanguage}" th:value="Java" />
  Java
</label>
<label>
  <input type="radio" th:field="*{favoriteLanguage}" th:value="Python" />
  Python
</label>

其余需要更新的地方也只有 POJO,这里略过不提,展现效果如下:

动态获取单选

实现方法类似,这里也不多赘述

properties 文件更新:

properties 复制代码
languages=Go,Java,Python,Rust,TypeScript,JavaScript

controller 部分略过不提,下面是 HTML 的修改:

html 复制代码
Favorite Programming Language:

<div th:each="language : ${languages}">
  <label>
    <input type="radio" th:field="*{favoriteLanguage}" th:value="${language}" />
    <span th:text="${language}"></span>
  </label>
</div>

⚠️:这里的 for 循环是绑定在一个外部的 div 上,如果直接迭代 label,那就代表着一个 label 会对应不同的 input,就会影响具体的功能实现

最终渲染效果:

多选

也就是 checkbox,具体不多赘述,丢代码即可

两个 HTML 的修改:

html 复制代码
Favorite Operation System:

<input
  type="checkbox"
  id="Linux"
  th:field="*{favoriteOSs}"
  th:value="Linux"
/><label for="Linux">Linux</label>
<input
  type="checkbox"
  id="MacOS"
  th:field="*{favoriteOSs}"
  th:value="MacOS"
/><label for="MacOS">MacOS</label>
<input
  type="checkbox"
  id="ms"
  th:field="*{favoriteOSs}"
  th:value="'Microsoft Windows'"
/><label for="ms">Microsoft Windows</label>
html 复制代码
Favorite Operating Systems: <span th:text="${student.favoriteOSs}"></span>

POJO:

java 复制代码
private List<String> favoriteOSs;

效果如下:

循环渲染结果

这里的格式还是稍微有点奇怪的,因为多选的保存格式为 List<String>,尽管 toString() 的默认实现不是很奇怪,不过也可以稍微优化一下:

html 复制代码
<ul>
  <li th:each="favOs: ${student.favoriteOSs}" th:text="${favOs}"></li>
</ul>
动态渲染多选

具体实现也略过了,和之前的实现一样:

properties 复制代码
systems=Linux,MacOS,Microsoft Windows,Android,IOS
java 复制代码
public class StudentController {
    @Value("${systems}")
    private List<String> systems;

    @GetMapping("/showStudentForm")
    public String showForm(Model model) {
        // ...
        model.addAttribute("systems", systems);
        // ...
    }
}
html 复制代码
Favorite Operation System:

<span th:each="system: ${systems}">
  <input
    type="checkbox"
    id="${system}"
    th:field="*{favoriteOSs}"
    th:value="${system}"
  />
  <label for="${system}" th:text="${system}"></label>
</span>

最终效果如下:

相关推荐
心灵Haven3 分钟前
1_安装JDK和Hadoop
java·开发语言·hadoop
web1368856587110 分钟前
PHP For 循环
android·java·php
loyd324 分钟前
【数据分析】5 设计不同业务分析框架
java·网络·数据分析
m0_7482451730 分钟前
Spring Boot项目开发常见问题及解决方案(上)
java·spring boot·后端
今天的接口写完了吗?31 分钟前
Spring Boot操作MaxComputer(保姆级教程)
java·spring boot·后端
金州小铁匠44 分钟前
基于EasyExcel封装的Excel工具类,支持高效导出和读取操作
java·spring·excel
IIIIIIlllii1 小时前
java练习(43)
java·开发语言
xxxxxmy1 小时前
Spring MVC 程序开发(1)
java·spring·mvc
不平衡的叉叉树1 小时前
使用优化版的编辑距离算法替代ES默认的评分算法
java·算法
没什么技术1 小时前
Spock框架:让单元测试更优雅的高效武器
java·spock