[JavaEE] Spring Web MVC入门

目录

[1. 什么是Spring Web MVC](#1. 什么是Spring Web MVC)

[2. 学习Spring MVC](#2. 学习Spring MVC)

[2.1 项目准备](#2.1 项目准备)

[2.2 建立连接](#2.2 建立连接)

[2.2.1 @RequestMapping注解介绍](#2.2.1 @RequestMapping注解介绍)

[2.3 Postman介绍](#2.3 Postman介绍)

[2.4 请求](#2.4 请求)

[2.4.1 @RequestMapping注解接收特定方法的请求](#2.4.1 @RequestMapping注解接收特定方法的请求)

[2.4.2 请求传递参数](#2.4.2 请求传递参数)

[2.4.3 请求参数传递对象](#2.4.3 请求参数传递对象)

[2.4.4 后端参数的重命名](#2.4.4 后端参数的重命名)

[2.4.5 传递数组](#2.4.5 传递数组)

[2.4.6 传递集合](#2.4.6 传递集合)

[2.4.7 传递JSON数据](#2.4.7 传递JSON数据)

[2.4.8 获取URL中参数@PathVariable](#2.4.8 获取URL中参数@PathVariable)

[2.4.9 上传文件@RequestPart](#2.4.9 上传文件@RequestPart)

[2.4.10 获取Cookie/Session](#2.4.10 获取Cookie/Session)

获取Cookie

获取Session

[2.4.11 获取Header](#2.4.11 获取Header)

[2.5 响应](#2.5 响应)

[2.5.1 返回静态页面 和 返回数据@ResponseBody](#2.5.1 返回静态页面 和 返回数据@ResponseBody)

[2.5.2 返回HTML代码片段](#2.5.2 返回HTML代码片段)

[2.5.3 返回Json](#2.5.3 返回Json)

[2.5.4 设置状态码](#2.5.4 设置状态码)

[2.5.5 设置Header](#2.5.5 设置Header)

[2.5.6 设置Content-Type](#2.5.6 设置Content-Type)

[3. 综合性练习](#3. 综合性练习)

[3.1 加法计算器](#3.1 加法计算器)

[3.1.1 约定前后端交互接口](#3.1.1 约定前后端交互接口)

[3.1.2 服务器代码](#3.1.2 服务器代码)

[3.1.3 前端代码](#3.1.3 前端代码)

[3.1.4 运行测试](#3.1.4 运行测试)

[3.2 用户登录](#3.2 用户登录)

[3.2.1 约定前后端交互接口](#3.2.1 约定前后端交互接口)

[3.2.2 服务器代码](#3.2.2 服务器代码)

[3.2.3 前端代码](#3.2.3 前端代码)

[3.2.4 运行测试](#3.2.4 运行测试)

[3.3 留言板](#3.3 留言板)

[3.3.1 约定前后端交互接口](#3.3.1 约定前后端交互接口)

[3.3.2 服务器代码](#3.3.2 服务器代码)

lombok介绍:

引入依赖的新方式:

服务器代码实现:

[3.3.3 前端代码](#3.3.3 前端代码)

[3.3.4 运行测试](#3.3.4 运行测试)

[3.4 图书管理系统](#3.4 图书管理系统)

[3.4.1 生成前端页面的工具](#3.4.1 生成前端页面的工具)

[3.4.2 约定前后端交互接口](#3.4.2 约定前后端交互接口)

[3.4.3 服务器代码](#3.4.3 服务器代码)

[3.4.4 前端代码](#3.4.4 前端代码)

[3.4.5 运行测试](#3.4.5 运行测试)

[4. 应用分层](#4. 应用分层)

[5. 总结](#5. 总结)


1. 什么是Spring Web MVC

Spring Web MVC 是基于 servlet API 构建的原始Web框架,从一开始就包含在spring框架里面,但通常称为:Spring MVC,也就是说 Spring Web MVC 最开始就包含在Spring FrameWork框架里面。

servlet:就是用来处理客户端请求,处理业务逻辑,生成响应内容的。

而Tomcat这样的servlet容器是用来接收客户端请求,管理servlet的执行,并将响应返回给客户端。

Spring Web MVC则是一个Web框架,它是基于servlet实现的一个框架,可以更加高效的处理请求。

MVC:

MVC是一种软件设计的架构模式,把软件系统分为模型,视图,控制器三层:

View(视图)指在应用程序中专门用来与浏览器进行交互,展示数据的资源.。

Model(模型)是应用程序的主体部分,用来处理程序中数据逻辑的部分。

Controller(控制器)可以理解为一个分发器,用来决定对于视图发来的请求,需要用哪一个模型 来处理,以及处理完后需要跳回到哪一个视图。即用来连接视图和模型。

Spring MVC就是一个采用MVC架构的Web框架。

Spring Boot 项目里面包含了Spring MVC框架,使用这个框架来实现Web功能,对Spring MVC框架做出了一些修改,如图所示:

Spring Boot框架相当于集成了很多功能,类似于12306,Spring Framework相当于火车,而Spring MVC相当于售票站。

2. 学习Spring MVC

2.1 项目准备

我们学习Spring MVC 框架主要学习三个方面:

建立连接:将浏览器和服务器建立连接,使得用户能通过浏览器访问服务器的资源。

请求:用户请求时候会有一些参数,服务器能够获取到这些参数。

响应:服务器处理好请求后得到响应,然后将响应发送给浏览器。

我们创建Spring Boot项目时候,框选了Spring Web就相当于创建了Spring MVC项目:

我们需要自己在idea创建一个Spring Boot项目。

2.2 建立连接

先创建一个UserController类,来实现浏览器和服务器的连接,代码如下:

java 复制代码
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/m1")
    public String m1() {
        return "m1";
    }
}

然后启动程序,访问http://127.0.0.1:8080/user/m1这个网址,就可以在网页上看到返回的数据了。

2.2.1 @RequestMapping注解介绍

这个注解相当于路径映射,可以修饰类,也可以修饰方法,URL中有资源路径,这个资源路径就是类路径+方法路径。

使用注意:

  1. 我们在使用这个注解时候,路径里面的/可以省略,Spring会帮我们检查。
  2. 注解里面可以是多层路径,第一个路径可以省略/,后面的不能省略。
  3. 类路径和方法路径可以重复,需要保证资源路径是唯一的,但是平常使用都不会重名。

这时候用户输入一个URL后,服务器能通过这个URL访问到具体的某个类某个方法,但是Spring是如果找到这个具体的类或者方法呢?

Spring通过扫描所有加了@RestController注解的类,查看里面有没有@RequestMapping注解,从而找到对应的方法,没有加@RestController注解的类不会扫描。

@RequestMapping注解既可以接收get请求又可以接收post请求。

浏览器一般发送的都是get请求,我们如何去构造一个post请求呢?

2.3 Postman介绍

我们可以使用Postman这个软件来构造不同方法的请求,测试客户端和服务器的连接,我们使用postman发送的请求给客户端 和 浏览器给客户端发送的请求是一样的效果的。

下载地址:https://www.postman.com/downloads/,可能需要注册一个账号,才能使用。

创建请求:

我们就可以使用Postman这个软件来构造请求,发送给服务器,比如我们构造一个post请求,使用fiddler抓包看是不是一个post请求:

我们使用postman构造一个post请求,发送给服务器,返回了m1。

使用fiddler抓包到请求的信息:

POST http://127.0.0.1:8080/user/m1 HTTP/1.1

User-Agent: PostmanRuntime/7.49.1

Accept: */*

Postman-Token: 47008be0-e53d-4db9-8fe4-10c06dfff6fa

Host: 127.0.0.1:8080

Accept-Encoding: gzip, deflate, br

Connection: keep-alive

Content-Length: 0

显示是一个post请求。

2.4 请求

2.4.1 @RequestMapping注解接收特定方法的请求

此时的@RuquestMapping注解既支持get请求,也支持post请求,其它请求也支持。

如果想让@RequestMapping注解只支持某一种请求,该如何去编写代码:

java 复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    //get和post请求都支持
    @RequestMapping("/m1")
    public String m1() {
        return "m1";
    }
    //只支持get请请求
    @RequestMapping(value = "/m2",method = RequestMethod.GET)
    public String m2() {
        return "m2";
    }
    //只支持post请求
    @RequestMapping(value = "/m3",method = RequestMethod.POST)
    public String m3() {
        return "m3";
    }
}

这里访问http://127.0.0.1:8080/user/m2,这个路径只支持get请求,其他请求会报405错误。

访问http://127.0.0.1:8080/user/m3,这个路径只支持post请求。

上面的代码还有另外一种写法:

java 复制代码
    //只支持get请求
    @GetMapping("/m4")
    public String m4() {
        return "m4";
    }
    //只支持post请求
    @PostMapping("/m5")
    public String m5() {
        return "m5";
    }

这里的m4只支持get请求,m5只支持post请求,使用这种写法的话注解里面的字符串可以相同,这里就会根据资源路径+请求的方法来区分那个方法。

这里的method也可以支持多个请求:

java 复制代码
    //支持get和post请求
    @RequestMapping(value = "/m6",method = {RequestMethod.POST, RequestMethod.GET})
    public String m6() {
        return "m6";
    }

get和post请求都支持。

2.4.2 请求传递参数

我们使用postman构造请求时候,是可以进行传参的。

参数介绍:

Params是传普通参数,通过查询字符串的键值对的形式传递。

Headers:请求头,键值对的形式可以自己设置。

Body:正文,可以自己添加内容。

from-data是通常用于提交图片或者文件。

x-www-form-urlencoded是对应的form表单。

raw可以上传text,json,html,xml等。

我们创建一个新的类来演示RequestController类

java 复制代码
@RequestMapping("/request")
@RestController
public class RequestController {
    //传递一个参数
    @RequestMapping("/r1")
    public String r1(String keyword) {
        return "接受的参数:" + keyword;
    }
    //传递多个参数
    @RequestMapping("r2")
    public String r2(String userName, String password) {
        return "账号:" + userName + " " + "密码:" + password;
    }
}

传递一个参数的请求:

传递多个参数的请求,通过查询字符串传递数据:

传递多个请求,通过正文来传递数据:

fiddler抓包结果:

POST http://127.0.0.1:8080/request/r2 HTTP/1.1

User-Agent: PostmanRuntime/7.49.1

Accept: */*

Postman-Token: 6eabf928-ee24-493a-a2c6-bc60c40e135b

Host: 127.0.0.1:8080

Accept-Encoding: gzip, deflate, br

Connection: keep-alive

Content-Type: application/x-www-form-urlencoded

Content-Length: 30

userName=admin&password=123456

这里面的用户名和密码数据就通过正文传递了。

传递参数是整型和基本类型呢?

java 复制代码
    //传递包装类型整型
    @RequestMapping("/r3")
    public String r3(Integer number) {
        return "接收参数:" + number;
    }
    //传递基本类型整型
    @RequestMapping("/r4")
    public String r4(int number) {
        return "接收参数:" + number;
    }

传递包装类型和基本类型都能传递成功,显示200。

传递包装类型和基本类型,传递字母,会显示400,客户端错误:

如果包装类型不传参数,此时就是null,但是基本类型不传参数,就会报错,500,服务器错误,基本类型不能为null。

2.4.3 请求参数传递对象

如果传的参数过多的情况,我们接收时候可以使用一个对象来接收:

定义一个UserInfo类:

java 复制代码
package com.sias.demo.controller;

public class UserInfo {
    private String name;
    private int gender;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}

利用对象来接收请求的参数:

java 复制代码
    //使用对象来接收参数
    @RequestMapping("r5")
    public String r5(UserInfo userInfo) {
        return "接收参数userInfo = " + userInfo.toString();
    }

2.4.4 后端参数的重命名

required属性是设置该参数是否必须传参。

java 复制代码
    //接收到参数a,把a的值给keyword,使用这个注解的参数,默认是必须传参,否则报错
    @RequestMapping("r6")
    public String r6(@RequestParam("a") String keyword) {
        return "接收参数:" + keyword;
    }
    //设置非必须传参
    @RequestMapping("r7")
    public String r7(@RequestParam(value = "a", required = false) String keyword) {
        return "接收参数:" + keyword;
    }

通过postman发送请求:

强制传参:

非强制传参:

2.4.5 传递数组

java 复制代码
    //传递数组
    @RequestMapping("r8")
    public String r8(String[] arr) {
        return "接收参数:" + Arrays.toString(arr);
    }

利用postman构造请求:

方法一:

方法二:

2.4.6 传递集合

默认情况下,传递同一个参数的多个值,是会放在数组里面的,如果要存到集合里面,需要使用@RequestParam注解来绑定参数关系。

错误代码:

java 复制代码
    //传递集合
    @RequestMapping("r9")
    public String r9(List<Integer> list) {
        return "接收参数:" + list;
    }

利用上面代码接受请求,结果会报错,500:

利用@RequestParam注解绑定关系后:

java 复制代码
    //传递集合
    @RequestMapping("r9")
    public String r9(@RequestParam List<Integer> list) {
        return "接收参数:" + list;
    }

这里跟数组一样,两种传参方式都可以:

2.4.7 传递JSON数据

JSON概念:

JSON就是一种数据格式,有着自己的规则来组织数据,本质是一个字符串,是客户端和服务端进行交互的一种数据格式。

JSON语法:

  • 数据在键值对中
  • 数据之间用 , 分隔
  • 对象用 { } 表示
  • 数组用 [ ] 表示
  • 值可以为对象,也可以为数组,数组中可以包含多个对象。

示例代码:

java 复制代码
{
 "squadName": "Super hero squad",
 "homeTown": "Metro City",
 "formed": 2016,
 "secretBase": "Super tower",
 "active": true,
 "members": [{
 "name": "Molecule Man",
 "age": 29,
 "secretIdentity": "Dan Jukes",
 "powers": ["Radiation resistance", "Turning tiny", "Radiation 
blast"]
 }, {
 "name": "Madame Uppercut",
 "age": 39,
 "secretIdentity": "Jane Wilson",
 "powers": ["Million tonne punch", "Damage resistance", "Superhuman 
reflexes"]
 }, {
 "name": "Eternal Flame",
 "age": 1000000,
 "secretIdentity": "Unknown",
 "powers": ["Immortality", "Heat Immunity", "Inferno", 
"Teleportation", "Interdimensional travel"]
 }]
}

压缩版本:

{ "squadName": "Super hero squad", "homeTown": "Metro City", "formed": 2016, "secretBase": "Super tower", "active": true, "members": [{ "name": "Molecule Man", "age": 29, "secretIdentity": "Dan Jukes", "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"] }, { "name": "Madame Uppercut", "age": 39, "secretIdentity": "Jane Wilson", "powers": ["Million tonne punch", "Damage resistance", "Superhuman reflexes"] }, { "name": "Eternal Flame", "age": 1000000, "secretIdentity": "Unknown", "powers": ["Immortality", "Heat Immunity", "Inferno", "Teleportation", "Interdimensional travel"] }] }

我们可以通过一些格式化工具将上面的压缩版本格式化:

https://www.json.cn/

JSON字符串和Java对象转换:

常用的转换工具:gson,fastjson,jackson。

spring boot项目自带的是jackson,我们可以直接去使用。

我们在test目录下,创建一个JsonTest类,来演示Jackson的使用:

java 复制代码
package com.sias.demo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sias.demo.controller.UserInfo;
import org.junit.jupiter.api.Test;

public class JsonTest {
    //对象转Json
    @Test
    void testObjectToJson() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //构造一个对象
        UserInfo userInfo = new UserInfo();
        userInfo.setName("张三");
        userInfo.setGender(1);
        userInfo.setAge(18);
        //对象转JSON
        String s = objectMapper.writeValueAsString(userInfo);
        System.out.println(s);
    }

    //Json转对象
    @Test
    void testJsonToObject() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //构造一个Json
        String s = "{\"name\":\"张三\",\"gender\":1,\"age\":18}";
        //Json转对象
        UserInfo userInfo = objectMapper.readValue(s, UserInfo.class);
        System.out.println(userInfo.toString());
    }
}

我们使用了Jackson工具里面的ObjectMapper类的writeValueAsString()方法和readValue()方法来进行转换的。

这里我使用了一个@Test注解,这个注解类似于main方法,可以运行测试方法,这个注解在一个类里面可以有多个,一个类里面只能有一个main方法。

我们使用writeValueAsString()方法将对象转换成JSON,利用readValue()方法将JSON转换为对象,该方法参数里面要标明转成那个对象。

JSON的优点:

  1. 语法简单,便于阅读理解,能够快速的进行数据转换。
  2. JSON可以被多种语言解析,跨平台性好。
  3. JSON的数据格式消耗的带宽小,可以提高传输效率。
  4. JSON数据格式是纯文本格式,不是包含什么可执行代码,比较安全。
  5. JSON数据格式容易拓展,嵌套数组和对象等复杂类型。

传递JSON数据:

这里使用了@RequestBody注解,作用是将请求中的JSON数据转换成参数里面的对象类型,代码如下:

java 复制代码
    //传递JSON数据
    @RequestMapping("r10")
    public String r10(@RequestBody UserInfo userInfo) {
        return "接收的参数:" + userInfo.toString();
    }

利用postman构造请求,包含JSON的数据:

注意事项:

我们在test里面的代码,将JSON对象转为对象使用的是readValue()方法,这个代码工作原理是:利用对象对应的类里面的空的构造方法,先new一个对象,然后使用这个对象赋值类里面的属性,如果我们在类里面重写了带有参数的构造方法,没有空的构造方法,此时下面代码执行就会报错,不能实现JSON转对象的过程。

java 复制代码
    //Json转对象
    @Test
    void testJsonToObject() throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        //构造一个Json
        String s = "{\"name\":\"张三\",\"gender\":1,\"age\":18}";
        //Json转对象
        UserInfo userInfo = objectMapper.readValue(s, UserInfo.class);
        System.out.println(userInfo.toString());
    }

2.4.8 获取URL中参数@PathVariable

我们打开一个今日头条网站,然后随便找两篇文章,我们可以看到这两篇文章的URL如下:

https://www.toutiao.com/trending/7576914740409139263/?rank=1\&log_from=b9be4e9e1f37d_1764233947866

https://www.toutiao.com/trending/7576991131788247049/?rank=2\&log_from=d5f8b1470f48c8_1764233959884

我们可以看到这两篇文章的资源路径中存在两个不同的数字,我们可以猜测后端接收到请求,是根据这两个数组来查询是哪一篇文章,把内容返回给浏览器。

那后端如何获取到URL里面的参数呢?

这里要用到@PathVariable注解来获取,这里URL的名字要和获取的参数名字相同,参数名字也可以修改成其他名字,这里的获取的参数要使用{ }来包含。

java 复制代码
    //获取URL的参数
    @RequestMapping("/article/{articleId}")
    public String r11(@PathVariable Integer articleId) {
        return "获取到的参数:" + articleId;
    }
    //获取多个参数
    @RequestMapping("/article/{type}/{articleId}")
    public String r12(@PathVariable Integer articleId, @PathVariable("type") String articleType) {
        return "获取到的参数:" + articleId + " " + articleType;
    }

使用postman构造请求:

2.4.9 上传文件@RequestPart

这里使用MultipartFile类型来接收文件,使用transferTo将获取到的文件保存在本地路径下。

@RequestPart注解还可以用来对参数重命名的。

java 复制代码
    //获取文件并存储本地
    @RequestMapping("/r13")
    public String r13(@RequestPart("file11") MultipartFile file) throws IOException {
        System.out.println(file.getOriginalFilename());
        //将文件保存到本地
        file.transferTo(new File("D:\\" + file.getOriginalFilename()));
        return "文件获取成功,并上传到本地";
    }

2.4.10 获取Cookie/Session

什么是Cookie:

cookie是客户端浏览器端存储的小型文本数据(键值对的形式),负责被动接收服务器发来的数据,然后存储到本地,等到发出请求时,自动将cookie中的数据存储在请求中。

http协议是一种无状态协议,就是说客户端和服务器的这次会话和下一次会话之间没有关系。

但是在实际开发中,会存在一些需要关联起来不同的会话。比如我们在网页登录自己的账号时候,下次进入网页就是直接登录好的页面。

**会话:**就是客户端和服务器建立的一次连接,就是一个会话,相当于打电话也是一次会话。

当一个客户端访问服务器时候,而这时候服务器就会生成一个身份令牌sessionId,将这个sessionId存储在响应里面的set-cookie字段中传给客户端,此时客户端的cookie就保存sessionId这个属性,下次请求时候将sessionId存储在请求的cookie字段中,发送给服务器,服务器就可以通过sessionId来确定是哪个客户端访问的服务器。

什么是Session:

在实际开发中,一个服务器需要处理很多个客户端的请求,也就会产生很多会话,那么服务器是如何区分那个请求是那个客户端发送来的呢?

这里服务器就会对每个客户端创建一个session对象,里面包括sessionId,以及客户端的信息等内容,session本质是一个哈希表,sessionId是key值,value是客户端的信息。

当一个客户端首次访问服务器,此时服务器就会创建一个session对象,生成sessionId,然后将这个sessionId放在响应的set-cookie字段中,返回给客户端,客户端的cookie中就会存储sessionId这个数据,等到下一次客户端发送请求,就会将这个sessionId数据放在请求的cookie字段发送给服务器,服务器就可以根据sessionId找到对应的客户端数据。

图示:

Cookie和Session的区别:

Cookie是客户端保存用户信息的一种机制,Session是服务器保护用户信息的一种机制。

Cookie和Session之间通过SessionId关联起来的。

Cookie和Session经常一起配合使用,但是也可以不一起使用,Cookie也可以保存一些其他数据在客户端,Session中的SessionId不一定非要Cookie/set-Cookie字段来传递,URL也可以。

获取Cookie

传统获取方法:

java 复制代码
    //传统获取Cookie的方法
    @RequestMapping("r14")
    public String r14(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if(cookies != null) {
            for(Cookie cookie : cookies) {
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }
        return "获取cookie成功";
    }

HttpServletRequest类是http请求的所有信息,HttpServletResponse类包含http响应的所有内容。

我们通过postman来设置cookie的值,然后发送请求,通过上面代码获取到cookie的键值对,然后返回字符串获取cookie成功。

简单获取cookie方法:

java 复制代码
    //通过注解拿到cookie的值
    @RequestMapping("/r15")
    public String r15(@CookieValue("name") String name) {
        return "从cookie中获取的值" + name;
    }

这里使用了@CookieValue注解来获取cookie的值。

获取Session

存储session:

java 复制代码
    //存储session
    @RequestMapping("/setSession")
    public String setSession(HttpServletRequest request) {
        //先从request里面获取sessionId,然后根据sessionId找到session对象。
        //这里默认为true,表示没有session就创建个session。
        //参数为false,表示没有session就返回null。
        HttpSession session = request.getSession(true);
        //设置用户名和年龄
        session.setAttribute("name", "lisi");
        session.setAttribute("age", 20);
        return "存储session成功";
    }

这里使用HttpServletRequest来获取到session,使用setAttribute方法来设置session的值。

获取session:

java 复制代码
    //获取session
    @RequestMapping("/getSession1")
    public String getSession1(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session == null) {
            return "没有session对象";
        }else {
            String userName = (String)session.getAttribute("name");
            return "用户名是:" + userName;
        }
    }

    //获取session
    @RequestMapping("/getSession2")
    public String getSession2(HttpSession session) {
        String userName = (String)session.getAttribute("name");
        return "用户名是:" + userName;
    }

    //获取session
    @RequestMapping("/getSession3")
    public String getSession3(@SessionAttribute("name") String userName) {
        return "用户名是:" + userName;
    }

有三种方法获取session,第三种使用了@SessionAttribute注解来获取session的值。

2.4.11 获取Header

java 复制代码
    //获取Header
    @RequestMapping("/getHeader1")
    public String getHeader1(HttpServletRequest request) {
        String userAgent = request.getHeader("User-Agent");
        return userAgent;
    }

    //获取Header
    @RequestMapping("/getHeader2")
    public String getHeader2(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
    }

这里获取Header中的键值对的属性值,存在两种方法,第二种使用了@RequestHeader注解来获取。

2.5 响应

2.5.1 返回静态页面 和 返回数据@ResponseBody

java 复制代码
@RequestMapping("/resp")
@Controller
public class RespController {
    //返回静态页面
    @RequestMapping("/r1")
    public String returnPage() {
        return "/index.html";
    }
    //返回数据
    @RequestMapping("/r2")
    @ResponseBody
    public String returnData() {
        return "返回的数据";
    }

}

这里我们使用了@Controller注解。

@Controller注解 和 @RestController注解的区别:

@RestController = @Controller + @ResponseBody,被@Controller注解修饰的类,会被扫描的。程序运行时候默认扫描的是启动类所在的目录下的类。

@Conteoller注解是该类返回的都是页面时候使用的。

@RestController注解是该类返回的都是数据的时候使用的。

如果一个类既有页面又有数据,我们可以使用@Controller注解,然后返回数据的方法上使用@ResponseBody注解,该注解既是类注解,也是方法注解。

2.5.2 返回HTML代码片段

java 复制代码
    //返回HTML代码片段
    @RequestMapping("r3")
    @ResponseBody
    public String returnHtml() {
        return "<h1>一级标题<h1>";
    }

    //返回HTML代码
    @RequestMapping(value = "r4", produces = "text/plain")
    @ResponseBody
    public String returnText() {
        return "<h1>一级标题<h1>";
    }

我们如果返回的是HTML的代码,浏览器会自动转换为HTML格式,我们可以通过设置Content-Type键值对的值来改变在浏览器显示的类型,使用produces参数来修改。

2.5.3 返回Json

java 复制代码
    //返回Json
    @RequestMapping("/r5")
    @ResponseBody
    public UserInfo returnJson() {
        UserInfo userInfo = new UserInfo("zhangsan",1,2);
        return userInfo;
    }

这里的返回结果为对象。

2.5.4 设置状态码

java 复制代码
    //设置响应的状态码
    @RequestMapping("/r6")
    @ResponseBody
    public UserInfo setStatus(HttpServletResponse response) {
        response.setStatus(404);
        UserInfo userInfo = new UserInfo("zhangsan",1,2);
        return userInfo;
    }

此时看到状态码被设置成404。

2.5.5 设置Header

java 复制代码
    //设置Header
    @RequestMapping("/r7")
    @ResponseBody
    public String setHeader(HttpServletResponse response) {
        response.setHeader("one","one");
        return "设置header成功";
    }

此时看到请求头多了一个设置的键值对。

2.5.6 设置Content-Type

java 复制代码
    //返回HTML代码
    @RequestMapping(value = "/r4", produces = "text/plain")
    @ResponseBody
    public String returnText() {
        return "<h1>一级标题<h1>";
    }

使用produces属性来设置。

3. 综合性练习

3.1 加法计算器

3.1.1 约定前后端交互接口

接口定义:

请求参数:

响应数据:

3.1.2 服务器代码

后端接收到两个参数后,返回数据显示在新页面里。

java 复制代码
@RequestMapping("/calc")
@RestController
public class CalcController {
    @RequestMapping("/sum")
    public String sum(Integer num1, Integer num2) {
        if(num1 == null || num2 == null) {
            return "输入的参数不合法,请重新输入";
        }
        Integer sum = num1 + num2;
        return "<h1>计算机计算结果:" + sum + "</h1>";
    }
}

3.1.3 前端代码

这里使用的from标签来发送请求,action是请求的URL,method是请求的方式,input标签的name属性是key的值,输入的是value的值,type="submit"是提交按钮,点击后将from标签域内的数据提交给服务器。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 使用from标签来向服务器发送请求 -->
     <form action="calc/sum" method="post">
        <h1>计算器</h1>
        数字1:<input name="num1" type="text"><br>
        数字2:<input name="num2" type="text"><br>
        <input type="submit" value=" 点击相加 ">
    </form>
</body>

</html>

3.1.4 运行测试

3.2 用户登录

3.2.1 约定前后端交互接口

用户登录接口:

请求参数:

响应数据:

Content-Type : text/html

响应内容:

true //账号密码验证成功

false //账号密码验证失败

第二个接口:

请求数据:

请求路径:/user/getLoginUser

请求方式:GET

接口描述:查询当前登录的用户

请求参数:无
响应数据:

Content-Type:text.html

响应内容:zhangsan

3.2.2 服务器代码

服务器代码实现了两个接口,一个是登录页面,接收账号和密码,并且将账号的信息存储在session中,如果账号密码正确返回true,其余情况返回false。

一个是显示登录用户名字的页面,根据session查询登录用户的名字返回给前端。

java 复制代码
@RequestMapping("/userL")
@RestController
public class UserLoginController {
    @RequestMapping("/login")
    public boolean login(String userName, String password, HttpSession session) {
        //参数的校验
        //方法1:
//        if(userName == null || userName == "" || password == null || password == "") {
//            return false;
//        }
        //方法2:
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            return false;
        }
        //这里使用固定的账号密码
        if("admin".equals(userName) && "123456".equals(password)) {
            //将账号信息存储在session中
            session.setAttribute("userName", userName);
            return true;
        }
        return false;
    }
    @RequestMapping("/getLoginUser")
    public String getLoginUser(HttpSession session) {
        String userName = (String)session.getAttribute("userName");
        return userName;
    }
}

3.2.3 前端代码

登录界面:

这里使用了Ajax技术,作用是可以保证页面不重新加载页面,跟服务器交换数据,更新局部页面,下面使用的jQuery语法来创建Ajax请求。

html 复制代码
<body>
  <h1>用户登录</h1>
  用户名:<input name="userName" type="text" id="userName"><br>
  密码:<input name="password" type="password" id="password"><br>
  <input type="button" value="登录" onclick="login()">
  
  <script src="js/jquery-3.7.1.min.js"></script>
  <script>
    // 创建login函数
    function login() {
      // 使用jQuery来创建Ajax请求
      $.ajax({
        url: "/userL/login",
        type: "post",
        //发送到服务端的数据
        data: {
          userName: $("#userName").val(),
          password: $("#password").val()
        },
        //http请求成功
        success: function(response) {
          if(response) {
            //跳转到新的页面
            location.href = "index.html";
          }else {
            alert("密码错误,请重新输入");
          }
        }
      });
    }
  </script>
</body>

用户登录成功首页:

这里也是用了Ajax技术来实现的。

html 复制代码
<body>
    登录人: <span id="loginUser"></span>

    <script src="js/jquery-3.7.1.min.js"></script>
    <script>
        $.ajax({
            url: "userL/getLoginUser",
            type: "get",
            success: function(userName) {
                $("#loginUser").text(userName);
            }
        });
    </script>
</body>

3.2.4 运行测试

3.3 留言板

3.3.1 约定前后端交互接口

获取全部留言:

请求:GET

url:message/getList
响应:JSON格式

{ "from": "A", "to": "B", "message": "C" },{ "from": "D", "to": "E", "message": "F" }

发表新的留言:

请求: POST JSON格式

url:/message/publish

{

"from": "A",

"to": "B",

"message": "C"

}
响应:JSON格式

{

ok: 1

}

3.3.2 服务器代码

lombok介绍:

Lombok是一个工具库,通过添加注解的方式,来简化Java的开发。

我们使用时候,需要导入依赖:

java 复制代码
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

lombok库里面包含很多注解:

@Data注解是直接包含很多的方法,在编译时候就会加到类里面的,此时注解会消失。

下面这个类,可以换成添加注解的形式:

java 复制代码
public class UserInfo {
    private String name;
    private int gender;
    private int age;

    public UserInfo() {

    }
    public UserInfo(String name, int gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}

下面代码在编译之后,就会包含上面的各种方法。

java 复制代码
@Data
public class UserInfo {
    private String name;
    private int gender;
    private int age;
}
引入依赖的新方式:

使用EditStarter插件。

我们就可以通过上面页面添加依赖了。

服务器代码实现:

先定义一个类来表示一条留言对象。

java 复制代码
@Data
public class MessageInfo {
    private String from;
    private String to;
    private String message;
}

设置两个接口,一个是接收留言数据,显示在下面,另一个是保存留言数据在顺序表中,保证留言信息不会刷新掉。

java 复制代码
@RequestMapping("/message")
@RestController
public class MessageController {
    private List<MessageInfo> messageInfoList = new ArrayList<>();

    @PostMapping("/publish")
    public String publish(@RequestBody MessageInfo messageInfo) {
        //使用@RequestBody注解,将JSON字符串转成对象
        //检查参数规范
        if(!StringUtils.hasLength(messageInfo.getFrom()) ||
                !StringUtils.hasLength(messageInfo.getTo()) ||
                !StringUtils.hasLength(messageInfo.getMessage())) {
            return "{\"ok\": 0}";
        }
        //存储留言
        messageInfoList.add(messageInfo);
        return "{\"ok\": 1}";
    }

    @GetMapping("/getList")
    public List<MessageInfo> getList() {
        return messageInfoList;
    }
}

3.3.3 前端代码

前端使用JSON.stringify()方法将对象转为JSON字符串,使用JSON.parse()方法将JSON字符串转为对象。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>留言板</title>
    <style>
        .container {
            width: 350px;
            height: 300px;
            margin: 0 auto;
            /* border: 1px black solid; */
            text-align: center;
        }

        .grey {
            color: grey;
        }

        .container .row {
            width: 350px;
            height: 40px;

            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .container .row input {
            width: 260px;
            height: 30px;
        }

        #submit {
            width: 350px;
            height: 40px;
            background-color: orange;
            color: white;
            border: none;
            margin: 10px;
            border-radius: 5px;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>留言板</h1>
        <p class="grey">输入后点击提交, 会将信息显示下方空白处</p>
        <div class="row">
            <span>谁:</span> <input type="text" name="" id="from">
        </div>
        <div class="row">
            <span>对谁:</span> <input type="text" name="" id="to">
        </div>
        <div class="row">
            <span>说什么:</span> <input type="text" name="" id="say">
        </div>
        <input type="button" value="提交" id="submit" onclick="submit()">
        <!-- <div>A 对 B 说: hello</div> -->
    </div>

    <script src="js/jquery-3.7.1.min.js"></script>
    <script>
        load();
        //显示所有留言
        function load() {
            $.ajax({
                type: "get",
                url: "/message/getList",
                success: function(messages) {
                    if(messages != null && messages.length > 0) {
                        var finalHtml = "";
                        for(var m of messages) {
                            finalHtml += "<div>" + m.from + "对" + m.to + "说" + m.message + "</div>";
                        }
                        $(".container").append(finalHtml);
                    }
                }
            });
        }
        function submit(){
            //1. 获取留言的内容
            var from = $('#from').val();
            var to = $('#to').val();
            var say = $('#say').val();
            if (from== '' || to == '' || say == '') {
                return;
            }
            var data = {
                from: from,
                to: to,
                message: say
            }
            //使用Ajax向后端发送请求
            $.ajax({
                type: "post",
                url: "/message/publish",
                contentType: "application/json",
                //stringify方法将对象转为JSON字符串
                data: JSON.stringify(data),

                success: function (result) {
                    //使用parse方法将JSON字符串转成对象
                    var str = JSON.parse(result);
                    if(str.ok == 1) {
                        //成功
                        var divE = "<div>" + from + "对" + to + "说" + say + "</div>";
                        //将内容添加到留言板下面
                        $(".container").append(divE);
                        //清空输入内容
                        $("#from").val("");
                        $("#to").val("");
                        $("#say").val("");
                    }else {
                        //失败
                        alert("留言发布失败");
                    }
                }
            });
        }
    </script>
</body>
</html>

3.3.4 运行测试

3.4 图书管理系统

3.4.1 生成前端页面的工具

Bootstarp网站,这里面可以有很多模版和样式,可以直接使用。

https://www.bootcss.com/

3.4.2 约定前后端交互接口

登录接口:

url /user/login

post

请求参数:name=admin&password=admin

响应:

true //账号密码正确

false //账号密码错误

图书列表展示:

url /book/getList

post

请求参数:无

响应:

返回图书列表

{ "id": 1, "bookName": "活着", "author": "余华", "count": 270, "price": 20, "publish": "北京⽂艺出版社",17 "status": 1, "statusCN": "可借阅" }, ........

字段说明:

3.4.3 服务器代码

用户登录接口:

java 复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public Boolean login(String name, String password, HttpSession session) {
        if(!StringUtils.hasLength(name) && !StringUtils.hasLength(password)) {
            return false;
        }
        //未使用数据库
        if("admin".equals(name) && "123456".equals(password)) {
            session.setAttribute("userName", name);
            return true;
        }
        return false;
    }
}

图书列表接口:

java 复制代码
@RequestMapping("/book")
@RestController
public class BookController {
    @RequestMapping("/getList")
    public List<BookInfo> getList() {
        //这里没有使用数据库,先mock数据(自己构造虚假数据)
        List<BookInfo> bookInfos = mockData();
        //这里返回给前端的图书借阅状态设置成文字
        for(BookInfo book : bookInfos) {
            if(book.getStatus() == 1) {
                book.setStatusCN("可借阅");
            }else {
                book.setStatusCN("不可借阅");
            }
        }
        return bookInfos;
    }

    //该方法从mock的数据中查数据
    public List<BookInfo> mockData() {
        List<BookInfo> bookInfos = new ArrayList<>();
        for (int i = 1; i <= 15 ; i++) {
            BookInfo book = new BookInfo();
            book.setId(i);
            book.setBookName("图书" + i);
            book.setAuthor("作者" + i);
            book.setCount(new Random().nextInt(80) + 21); //[20,100]
            book.setPrice(new BigDecimal(new Random().nextInt(80) + 21));
            book.setPublish("图书出版社" + i);
            book.setStatus(i % 5 == 0 ? 2 : 1); //1-可借阅 2-不可借阅
            bookInfos.add(book);
        }
        return bookInfos;
    }
}

3.4.4 前端代码

登录页:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/login.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
</head>

<body>
    <div class="container-login">
        <div class="container-pic">
            <img src="pic/computer.png" width="350px">
        </div>
        <div class="login-dialog">
            <h3>登陆</h3>
            <div class="row">
                <span>用户名</span>
                <input type="text" name="userName" id="userName" class="form-control">
            </div>
            <div class="row">
                <span>密码</span>
                <input type="password" name="password" id="password" class="form-control">
            </div>
            <div class="row">
                <button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
            </div>
        </div>
    </div>
    <script src="js/jquery.min.js"></script>
    <script>
        function login() {
            $.ajax({
                type: "post",
                url: "/user/login",
                data: {
                    name: $("#userName").val(),
                    password: $("#password").val()
                },
                success: function (result) {
                    if(result) {
                        //账号密码正确
                        location.href = "book_list.html";
                    }else {
                        alert("账号或密码错误,请重新输入");
                    }
                }
            });
        }
    </script>
</body>

</html>

图书列表页:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图书列表展示</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">

    <link rel="stylesheet" href="css/list.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript" src="js/bootstrap.min.js"></script>
    <script src="js/jq-paginator.js"></script>

</head>

<body>
    <div class="bookContainer">
        <h2>图书列表展示</h2>
        <div class="navbar-justify-between">
            <div>
                <button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>
                <button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>
            </div>
        </div>

        <table>
            <thead>
                <tr>
                    <td>选择</td>
                    <td class="width100">图书ID</td>
                    <td>书名</td>
                    <td>作者</td>
                    <td>数量</td>
                    <td>定价</td>
                    <td>出版社</td>
                    <td>状态</td>
                    <td class="width200">操作</td>
                </tr>
            </thead>
            <tbody>
            </tbody>
        </table>

        <div class="demo">
            <ul id="pageContainer" class="pagination justify-content-center"></ul>
        </div>
        <script>

            getBookList();
            function getBookList() {
                $.ajax({
                    type: "post",
                    url: "/book/getList",
                    success: function (books) {
                        var finalHtml = "";
                        for(var book of books) {
                            finalHtml += '<tr>';
                            finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
                            finalHtml += '<td>' + book.id + '</td>';
                            finalHtml += '<td>' + book.bookName + '</td>';
                            finalHtml += '<td>' + book.author + '</td>';
                            finalHtml += '<td>' + book.count + '</td>';
                            finalHtml += '<td>' + book.price + '</td>';
                            finalHtml += '<td>' + book.publish + '</td>';
                            finalHtml += '<td>' + book.statusCN + '</td>';
                            finalHtml += '<td><div class="op">';
                            finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
                            finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';
                            finalHtml += '</div></td></tr>';
                        }
                        $("tbody").html(finalHtml);
                    }
                });
            }
    
            //翻页信息
            $("#pageContainer").jqPaginator({
                totalCounts: 100, //总记录数
                pageSize: 10,    //每页的个数
                visiblePages: 5, //可视页数
                currentPage: 1,  //当前页码
                first: '<li class="page-item"><a class="page-link">首页</a></li>',
                prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
                next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
                last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
                page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
                //页面初始化和页码点击时都会执行
                onPageChange: function (page, type) {
                    console.log("第"+page+"页, 类型:"+type);
                }
            });
            function deleteBook(id) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    //删除图书
                    alert("删除成功");
                }
            }
            function batchDelete() {
                var isDelete = confirm("确认批量删除?");
                if (isDelete) {
                    //获取复选框的id
                    var ids = [];
                    $("input:checkbox[name='selectBook']:checked").each(function () {
                        ids.push($(this).val());
                    });
                    console.log(ids);
                    alert("批量删除成功");
                }
            }

        </script>
    </div>
</body>

</html>

3.4.5 运行测试

4. 应用分层

为什么要应用分层?

我们前面写的代码创建了很多类,这些类混在一起就会难以阅读和区分,不方便别人阅读自己的代码,此时就需要一种规范来将代码进行分类,类似于spring MVC框架将系统分为Model(模型),View(视图),Controller(控制器)三个层次,将用户视图和业务处理完全分开,通过控制器来进行连接。

现在主流的开发模式是"前后端分离",前端和后端各自进行开发,又在Java后端产生了新的三层架构:

表现层 :用来接受请求和返回响应的,表现层一般使用Controller来表示。

业务逻辑层 :用来处理具体的业务的,业务逻辑层一般使用Sevice来表示。

数据层 :用来管理和存储数据的,数据层一般使用Dao来表示。

实体类可以使用 model , entity , pojo 这些来表示。

MVC模式和三层架构的联系:

MVC模型主要是把数据和视图分离,将数据展示和数据处理分开,通过控制器来建立联系。

三层架构是从不同的维度处理数据的高内聚和低耦合,将交互逻辑,业务处理,数据库管理分开。

5. 总结

本文介绍了很多注解:

@RequestMapping:路由映射,用来表示URL的。

@RequestParam:对前端传过来的参数名字进行重命名。

@RequestBody:接收前端传来的JSON类型的参数的。

@PathVariable:接受路径中的参数的。

@RequestPart:接收前端传来的文件的。

@ResponseBody:返回数据的。

@Controller:定义一个控制器,spring框架启动时加载该类,把这个对象交给spring管理,默认该类返回视图。

@RestController:@ResponseBody + @Controller 返回数据。

@CookieValue:从Cookie中获取值。

@SessionAttribute:从Session中获取值。

@RequestHeader:从Header中获取值。

Spring MVC内置的HttpServletRequest 和 HttpServletResponse两个对象,可以获得请求和响应中的所有参数,Cookie和session可以从HttpServletRequest中获取,可以使用HttpServletResponse来设置状态码。

相关推荐
一袋米扛几楼985 分钟前
【Git】规范化协作:详解 GitHub 工作流中的 Issue、Branch 与 Pull Request 最佳实践
前端·git·github·issue
网络点点滴19 分钟前
前端与后端的区别与联系
前端
EnCi Zheng43 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen1 小时前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技1 小时前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人1 小时前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实1 小时前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha1 小时前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript