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)代表对用户输入的响应方式。书中有一个很典型的例子来理解这一模式,如图所示:
tomcat + spring mvc原理外传:spring mvc与前端的纠葛-QQ20200130-1.png
模型(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

    可以使用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中传递数据的数据结构。
    为了方便理解这两个文件,先展示项目运行后的效果和可以做的操作。
tomcat + spring mvc原理外传:spring mvc与前端的纠葛-home.png
这个是项目运行后,在浏览器输入"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

如果返回Friend类实例,实际会生成一个json字符串。
    目前比较常用的JavaScript框架,比如React或者Vue可以独立部署,浏览器获取页面后,可通过url访问spring mvc接口,获取json数据后解析。目前这种模式是在生产中比较常见。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值