前言
本来准备继续分析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引入了如下配置:
1 2 3 4 |
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration ...... org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider |
这说明spring boot本身默认支持Thymeleaf。
spring boot + Thymeleaf的demo
可以使用spring intializr生成项目spring boot(spring boot基于spring mvc),当然也可以手动生成。项目的整个目录如上图所示。其中main中存放的是java代码,resource中存放项目配置和静态资源。
首先,项目中需要引入Thymeleaf依赖。因为使用简便的嵌入式h2数据库,也需要引入jdbc和h2数据库的依赖。由于我想偷懒,使用代码builder工具,所以也引入了lombok依赖。整个项目的pom.xml内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
//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在加载过程中会自动读取这个文件,并执行建表语句:
1 2 3 4 5 6 7 8 9 10 |
//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字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//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依赖的原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//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中。
1 2 3 4 5 6 7 8 9 10 11 |
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文件中的接口代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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是如何展示和传递数据的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<!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> name phone email QQ wchat </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中。
当然也可以返回更复杂的数据结构,比如修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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(); } } |
如果返回Friend类实例,实际会生成一个json字符串。
目前比较常用的JavaScript框架,比如React或者Vue可以独立部署,浏览器获取页面后,可通过url访问spring mvc接口,获取json数据后解析。目前这种模式是在生产中比较常见。
作者:孙新
– 现在注册滴滴云,得10000元立减红包
– 8月特惠,1C2G1M云服务器 9.9元/月限时抢
– 滴滴云使者专属特惠,云服务器低至68元/年
– 输入大师码【7886】,GPU全线产品9折优惠