tomcat + spring mvc原理外传:spring mvc与前端的纠葛
前言
本来准备继续分析spring mvc的核心组件HandlerAdapter的原理,这个组件负责将请求体request交由最终的Handler处理,也就是由业务层编写的接口(Controller中的方法)处理,然后将返回的结果传给展示层解析渲染。HandlerAdapter可以说是和业务代码的直接交互层。但是写到一半无法继续了,因为目前spring mvc存在两种模式:一种是系统内统合后端和前端的功能,spring mvc不仅包括后端的代码逻辑,同时实现最后前端页面的渲染和返回;另一种是spring mvc项目只作为后端,返回json或者xml数据,由浏览器请求数据,展示页面。对于这两种方式在生产环境中应用的比例,目前看来似乎是后者更加广泛。spring mvc之所以称为mvc(model, view 和controller),是因为最初的设计思想是秉承综合处理后端(model、controller)和前端(view),这意味着目前的代码中会存在很多第一种情况的逻辑。如果想要对代码有清晰的理解,还是需要对这两种模式有基础的认识。为了方便学习,兼顾不太了解第一种模式的读者,故加增此篇作为说明。
spring mvc的设计思想
在经典的gof《设计模式》的引言部分,提出了MVC的设计思想,其中M(model)代表数据模型,V(view)代表展示视图,C(Controller)代表对用户输入的响应方式。书中有一个很典型的例子来理解这一模式,如图所示:
模型(model)中保存了视图中所需要的数据,而视图(view)可以使用这些数据实现最终展示,模型和视图的分离,可以支持同一种数据的多种展示方式,比如图中的表格、柱状图和饼状图等。而Controller的作用表现在交互的过程中,Controller负责接收传入的操作消息,生成数据model,并将model传递给view做展示。
spring mvc的设计是基于MVC的理论。比较明显的是在编写业务代码时,会用到@Controller或者@RestController注解,由此可见业务代码的各种接口综合起来,实际上实现的就是Controller模块。使用@Controller注解的接口,需要对M(model)和V(view)有感知,传入参数中包括model,返回默认为view名。但是目前多使用@RestController,返回为json数据,隐藏了model和view相关的部分,spirng mvc实际工作模式为spring mc。
spring mvc的前后端一体化模式
spring mvc前后端一体的模式比较流行的模板语言包括JSP、FreeMaker和Thymeleaf等。其中如果看了spring boot自动化配置文件——spring-boot-autoconfigure包下的spring.factories,会发现spring boot引入了如下配置:
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
......
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider
这说明spring boot本身默认支持Thymeleaf。
spring boot + Thymeleaf的demo
![tomcat + spring mvc原理外传:spring mvc与前端的纠葛-tree.png](https://i-blog.csdnimg.cn/blog_migrate/1b9c4004e74e917e52ddf18e89df0526.png)
可以使用spring intializr生成项目spring boot(spring boot基于spring mvc),当然也可以手动生成。项目的整个目录如上图所示。其中main中存放的是java代码,resource中存放项目配置和静态资源。
首先,项目中需要引入Thymeleaf依赖。因为使用简便的嵌入式h2数据库,也需要引入jdbc和h2数据库的依赖。由于我想偷懒,使用代码builder工具,所以也引入了lombok依赖。整个项目的pom.xml内容如下:
//pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.12.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>thymeleaf-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>thymeleaf-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--thymeleaf starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--h2依赖-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!--jdbc依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
schema.sql存储的是数据库的建表语句,spring boot在加载过程中会自动读取这个文件,并执行建表语句:
//schema.sql
create table friends (
id identity,
name varchar(70) not null,
phone varchar(13) not null,
qq varchar(20),
email varchar(256),
wchat varchar(256)
);
这样数据库的friends表就会在内存中建起来。这个表主要是存储姓名、电话、email、qq号、wchat字段。
//Friend.java
package com.example.thymeleafdemo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Friend{
private Long id;
private String name;
private String phone;
private String email;
private String QQ;
private String wchat;
}
对应的,代码中用Friend类来存储表中读取的每一条数据。Friend.java文件比较简单,唯一注意点是我使用了@Data、@Builder等注解节省代码,这也是为什么引入lombok依赖的原因。
//RecordRepository.java
package com.example.thymeleafdemo;
import java.util.List;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
@Repository
public class RecordRepository {
@Autowired
private JdbcTemplate jdbc;
public List<Friend> findAll() {
return jdbc.query(
"select id, name, phone, email, qq, wchat " +
"from friends order by name",
new RowMapper<Friend>() {
public Friend mapRow(ResultSet rs, int rowNum) throws SQLException {
return Friend.builder()
.id(rs.getLong(1))
.name(rs.getString(2))
.phone(rs.getString(3))
.email(rs.getString(4))
.QQ(rs.getString(5))
.wchat(rs.getString(6))
.build();
}
}
);
}
public void save(Friend friend){
jdbc.update("insert into friends" +
"(name, phone, email, qq, wchat)" +
"values (?, ?, ?, ?,?)",
friend.getName(), friend.getPhone(),
friend.getEmail(),friend.getQQ(),friend.getWchat());
}
}
RecordRepository.java使用jdbc查询和存储数据,findAll读取表中的所有记录,sava方法用来存储一条记录。
style.css设置了简单的样式,会用在home.html中。
body {
background-color: #eeeeee;
font-family: sans-serif;
}
label {
display: inline-block;
width: 120px;
text-align: right;
}
最后,重点是RecordController.java文件和home.html文件。RecordController是spring mvc中的C,home.html代表了其中的V,model是被RecordController用来向home.html中传递数据的数据结构。
为了方便理解这两个文件,先展示项目运行后的效果和可以做的操作。
这个是项目运行后,在浏览器输入"http://127.0.0.1:8080/show" ,就可以访问home.html展示页面。在页面中输入如图所示的内容,然后点击提交,会在页面下方展示表中所有录入的信息。这中间需要访问两个后端接口,一个是提交表单的POST接口,另一个是展示信息的GET接口。
先看RecordController.java文件中的接口代码。
package com.example.thymeleafdemo;
import java.util.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class RecordController {
@Autowired
private RecordRepository recordRepo;
@GetMapping("/show")
public String home(Map<String, Object> model){
List<Friend> Friends = recordRepo.findAll();
model.put("friends", Friends);
return "home";
}
@PostMapping("/show")
public String submit(Friend friend) {
recordRepo.save(friend);
return "redirect:/show";
}
}
RecordController类由@Controller注解,其中注入了RecordRepository的bean,分别用来在home方法和submit方法中查询friends表中所有数据和存入单条记录。
home方法对应处理GET类型“/show”url路径的接口访问,入参是map类型的model。model参数并不是由接口传入的,上文中在浏览器中输入的url“http://127.0.0.1:8080/show”并没有带有如何参数。model是由spring mvc框架传入,在方法返回之后,model会自动传递给view进行视图的渲染,这一步会在home.html中有所体现。home方法在查询表中数据之后,所有”friend"数据会存入到Friend类的List中,接着会把List数据输入model,并将索引命名为“friends”。return返回的String为视图名,即“home.html”的名字(view name)。这个view name和model会在后续框架中的视图解析器中使用。
sumbit方法对应处理表单数据的POST接口访问。这里的入参是由POST传入的,对应的参数名即Friend类中的参数名。在表中存入数据之后,return返回依然应该为视图名,不过这里使用了重定向,使用相对url路径重新访问“/show”接口,调用home方法,展示home.html的视图。
接下来看home.html是如何展示和传递数据的。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Thymeleaf Test</title>
<link rel="stylesheet" th:href="@{/style.css}" />
</head>
<body>
<h2>Thymeleaf Test</h2>
<form method="POST">
<label for="name">姓名:</label>
<input type="text" name="name"></input><br/>
<label for="phone">电话:</label>
<input type="text" name="phone"></input><br/>
<label for="email">Email:</label>
<input type="text" name="email"></input><br/>
<label for="QQ">QQ:</label>
<input type="text" name="QQ"></input><br/>
<label for="wchat">微信:</label>
<input type="text" name="wchat"></input><br/>
<input type="submit"></input>
</form>
<ul th:each="friend : ${friends}">
<li>
<span th:text="${friend.name}">name</span>
<span th:text="${friend.phone}">phone</span>
<span th:text="${friend.email}">email</span>
<span th:text="${friend.QQ}">QQ</span>
<span th:text="${friend.wchat}">wchat</span>
</li>
</ul>
</body>
</html>
在html头中,就声明了会使用thymeleaf模板语言。表单数据使用POST方法发送,input标签中的name对应Friend类中的参数名,这样才能在submit方法中接收输入参数。下面是所有friend数据的展示,数据源是”friends”,对应model中“friends”索引,即存入的List数据结构,这里遍历每一个Friend,将对应的name、phone等参数展示出来。
以上即为spring mvc前后端一体的模式,主要特点为spring mvc既需要负责提供后端数据,也需要进行前端的视图渲染,最终会把整个页面的数据返回给浏览器。这种模式前后端的数据交互比较简单便捷,缺点是过于笨重。
spring mvc专职后端模式
另一种模式是spring mvc只负责后端数据处理,所有的视图相关都交给独立部署的前端完成。这种模式比较典型的是RESTful Web服务,主要为JavaScript类型框架,比如Backbone.js、AngularJS、React或者Vue等,提供数据。
spring mvc的RESTful Web服务
不同于传统spring mvc需要依靠view,RESTful Web服务控制器只返回对象,对象数据作为JSON / XML直接写入HTTP响应。本质实现也是在@Controller的基础上,添加@ResponseBody注解,这也是@RestController注解的浅层实现原理。由于这里只介绍模式,底层原理会在HandlerAdapter篇中详细分析,所以不再赘述。
实现RESTful Web服务就非常简单,在上文的项目中直接修改RecordController的@Controller注解为@RestController注解,然后再次运行,访问"http://127.0.0.1:8080/show", 网页的返回结果变成了“home”。这时所有的前端文件和配置都已经不再发挥作用,因为return返回结果“home”字符串,不再是视图名,而是直接对应HTTP的响应数据,被写入到了ResponseBody中。
当然也可以返回更复杂的数据结构,比如修改代码如下:
package com.example.thymeleafdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class RecordController {
@Autowired
private RecordRepository recordRepo;
@GetMapping("/show")
public Friend home(){
return Friend.builder()
.id(0L)
.name("帆云羽")
.email("sunxin3399@163.com")
.phone("123456789")
.QQ("2038529489")
.wchat("xsun1314")
.build();
}
}
![tomcat + spring mvc原理外传:spring mvc与前端的纠葛-rest.png](https://i-blog.csdnimg.cn/blog_migrate/8559592ea98a0ec53d07c7879cb8b545.png)
如果返回Friend类实例,实际会生成一个json字符串。
目前比较常用的JavaScript框架,比如React或者Vue可以独立部署,浏览器获取页面后,可通过url访问spring mvc接口,获取json数据后解析。目前这种模式是在生产中比较常见。