# Zero for SSM **Repository Path**: zcyr/zero-for-ssm ## Basic Information - **Project Name**: Zero for SSM - **Description**: 从零开始搭建一个简单的SSM单机项目, 仅为学习巩固一系列的api. - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2022-01-07 - **Last Updated**: 2024-09-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Zero for SSM 从零开始搭建一个简单的SSM单机项目, 仅为学习巩固一系列的api. ### 前言 我将通过一个新的Win10系统环境从零搭建一个单机SSM项目 该项目内容是一个简单的王者荣耀用户系统, 包括用户的注册登录和资源管理 通过本文档搭建一个简单的单机SSM项目你将学习: - 如何通过百度解决问题 - 如何使用API文档 - Spring相关API - SpringMVC相关API - Mybatis相关API ### 环境说明 - IDEA社区版 2021.3.1或以上 - Java (JDK) 1.8 - Mysql社区版 5.7.30 - Tomcat 8.5.73 - Windows 10 ### 0.环境搭建 首先在敲代码之前要把环境先搭好, 没有环境你敲个der, 已经搭建好的可以自行选择跳过. #### IDE下载与安装 idea是一款非常智能的Java开发工具, 本教程大多数步骤依赖该IDE的智能实现,因其强大的插件市场使得免费的社区版的功能丝毫不逊色于其终极版, 本教程使用社区版. ##### 下载 在百度搜索idea进入官网或者 [IntelliJ IDEA:JetBrains 功能强大、符合人体工程学的 Java IDE](https://www.jetbrains.com/zh-cn/idea/) 点击下载按钮后在页面中选择Community版本下载exe版 ##### 安装 运行exe文件一直点击next然后intall就好, 不想安装到C盘可以自己修改安装位置. #### JDK下载与安装 本教程直接使用idea完成JDK的下载安装, 想手动安装可以在java官网下载安装, 本教程略. #### Database下载与安装 本教程使用Mysql社区版5.7.30 zip包 ##### 下载 在百度搜索mysql进入Community下载页或者 [Download MySQL Community Server](https://dev.mysql.com/downloads/mysql/) 在下载页中点击Archives后选择**Product Version**为**5.7.30**再点击***Windows (x86, 64-bit), ZIP Archive***的Download按钮 ##### 安装 1. 解压zip到你想要安装的目录中 *PS. 安装目录最好不要出现中文* 2. 右键开始打开Power Shell 管理员输入: ```shell cd "你解压mysql的目录路径\mysql-5.7.30-winx64\bin" # 运行该命令完成步骤2 # 例如笔者的 cd C:\Users\Heming\Desktop\env\mysql-5.7.30-winx64\bin # cd "目标目录" 是一个常用的shell命令效果是进入目标目录 # 就像打开一个文件夹 ``` 3. 这样我们就成功进入了mysql的bin目录, 里面有很多的可执行文件, 可以输入命令: ls 查看里面的文件 4. 为了能在数据库存放中文, 首先创建数据库字符集配置 ```shell echo "[mysqld]`ncharacter-set-server=utf8" | Out-File ../my.ini -Encoding ascii # 运行该命令完成步骤4 ### 创建my.ini配置文件到mysql根目录中 ``` 5. 首先我们使用mysqld进行数据库的初始化 ```shell .\mysqld.exe --initialize --console # 运行该命令进行数据库的初始化, 完成步骤5 ### 以下是执行后的输出 2022-01-08T07:04:54.543675Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2022-01-08T07:04:54.953432Z 0 [Warning] InnoDB: New log files created, LSN=45790 2022-01-08T07:04:55.006094Z 0 [Warning] InnoDB: Creating foreign key constraint system tables. 2022-01-08T07:04:55.092809Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 48657244-7051-11ec-a003-000c2996407e. 2022-01-08T07:04:55.097274Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened. 2022-01-08T07:04:57.757375Z 0 [Warning] CA certificate ca.pem is self signed. 2022-01-08T07:04:58.128287Z 1 [Note] A temporary password is generated for root@localhost: #RiqDjW%a1e) ### 自动初始化完成后生成的root密码 #RiqDjW%a1e) ``` 6. 再然后安装数据库服务 ```shell .\mysqld.exe --install # 运行该命令安装数据库服务, 完成步骤6 Service successfully installed. # 看到该输出即为安装成功了 ``` 7. 最后我们启动mysql服务就好了 ```SHELL net start mysql # 运行该命令来启动数据库服务, 完成步骤7 MySQL 服务正在启动 . MySQL 服务已经启动成功。 ``` 8. 登录数据库并修改root密码, 完成该步骤就可以关闭Power Shell了 ```shell .\mysql.exe -uroot -p # 运行该命令并输入自动生成的密码即可登录 Enter password: ************ Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 5.7.30 Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> ### 输出以上信息我们就成功登录进入mysql了, 在这里我们可以输入SQL语句来执行. ``` ```SQL alter user user() identified by '123456'; # 在mysql中输入该命令即可将root密码修改为123456 Query OK, 0 rows affected (0.01 sec) # 出现该提示即为修改成功, 完成步骤8 ``` #### Tomcat下载与安装 本项目使用tomcat 8.5.73 zip包 ##### 下载 在百度进入tomcat官网下载或者 [Apache Tomcat® - Apache Tomcat 8 Software Downloads](https://tomcat.apache.org/download-80.cgi) 找到到**8.5.73**并点击下载**Binary Distributions** 下Core中的 **64-bit Windows zip (pgp, sha512)** ##### 安装 解压zip到你想要安装的目录中即可完成安装 *PS. 安装目录最好不要出现中文* ### 1.创建并运行Java项目 本项目使用maven并采用apache的maven项目模板 ##### 创建 1. 在idea中选择**New Project** 2. 在New Project窗口左边选择项目类型为**Maven** > 如果没有配置JDK此时会发现**Project SDK**中显示 <No SDK> > > - 如果Project SDK显示为No SDK或版本低于1.8时. > - 点击Project SDK后再点击Download JDK. > - 在弹出的窗口中选择Version为1.8. > - 选择Vendor为Azul Zulu Community™. > - Location按喜好填写, 推荐默认. > - 点击Download后idea会自动下载该JDK并在该项目中使用 3. 在项目窗口中勾上**Create from archetype** 4. 选择**org.apache.maven.archetypes:maven-archetype-webapp** 5. 点击next后输入项目名后一直点击next直到点击Finish后可以发现我们的项目开始创建了 6. 此时可以看见Project目录中只有.idea目录并可以看到Run窗口在疯狂的下载 > 此时的项目还未创建完成, idea正在下载Maven相关的文件插件包括Maven项目模板, > > 请耐心等待Run窗口跑完, 创建项目需要的时间取决于当前网络环境. > > 笔者在虚拟机环境中创建项目用了15分30秒,如果手动安装Maven并配置到Idea应该就不会那么久了 7. 当看见src目录出现时我们的项目就创建成功了 8. 我们的目录结构还不完善在此我们完善项目结构 9. 在idea中点开**src**并右键**main**目录选择**New**后选择**Directory** 10. 在弹出的窗口中继续选择**Maven Source Directoryes**下的**java** 11. 此时可以看见我们目录中多出了java文件夹并被idea正确识别了还标上了蓝色 12. 我们以同样的方法来创建**resources**文件夹, 可以看到resources文件夹也被idea识别了 > 一个标准的java webApp的src目录下结构应该是这样的. > > ├─main > │ ├─java > │ ├─resources > │ └─webapp > │ └─WEB-INF > └─test // 本教程略过test目录 > ├─java > └─resources > > ***请注意: 项目的目录结构应该遵守相应的规范否则会出现许多奇怪的问题*** 13. 此时我们的项目到此为止已经创建成功了! ##### 运行 既然我们使用apache的webApp模板成功创建了项目, 并发现webapp文件夹中有index.jsp文件, 不妨我们运行在tomcat中并在浏览器访问试试吧 1. 首先社区版不支持在idea中一键运行项目到tomcat, 但是我们可以借助插件即可完成一键运行. 2. 打开idea的**Settings**窗口, 并在窗口左边选择**Plugins** 3. 在**Plugins**窗口下搜索Tomcat可以看到最多下载的插件**Smart Tomcat** 4. 点击install即可完成**Smart Tomcat**的下载安装 5. 插件安装完成后, 我们将配置项目的运行配置, 即告诉idea我们的项目该怎么运行 6. 点击右上角的**Add Configutation** 7. 在弹出的窗口中点击左上角**+**号按钮并选择**Smart Tomcat** 8. 可以看见弹出了一个配置默认名字是Unnamed可以修改该配置的名字 9. 在此配置中我们配置**Tomcat Server**它是一个tomcat的目录路径 10. 这里我们配置为我们解压出来的tomcat目录, 完成后就可以确认并运行本项目了 11. 点击右上角运行,可以看见idea帮我们启动tomcat并打包项目运行到tomcat了 12. 我们也成功在浏览器中看见了**Hello World!** > 到目前为止, 我们已经有了一个能成功运行在Tomcat中的空白Web项目了, 接下来将深入该SSM项目 > > 的功能以及架构实现. ### 2.创建数据库资源 编程本质其实就是为了操作资源, 所以我们得有资源给我们操作. #### 创建数据库 ```SQL ### 在mysql客户端中运行 create database zeroforssm; ## 输入该创建一个名为zeroforssm的数据库 Query OK, 1 row affected (0.00 sec) # 输出信息 ### 在mysql客户端中运行 use zeroforssm; ## 输入该命令进入该数据库 Database changed # 输出信息 ``` #### 创建资源表 为了完成用户登录注册, 查询英雄, 查询战绩其中涉及到的资源有 用户, 英雄, 对局信息所以让我们先来创建这三个表吧 ##### 用户表(id, name, password, sex) ```sql ### 在mysql客户端中运行 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(36) CHARACTER SET utf8mb4 NOT NULL, `sex` tinyint(1) DEFAULT NULL COMMENT 'false=boy', `password` varchar(128) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ##### 英雄表(id, name) ```sql ### 在mysql客户端中运行 CREATE TABLE `hero` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(36) CHARACTER SET utf8mb4 NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ##### 对局表(id, map_name, victory, red_kill, red_money, blue_kill, blue_money, date) ```sql ### 在mysql客户端中运行 CREATE TABLE `battle` ( `id` int(11) NOT NULL AUTO_INCREMENT, `map_name` varchar(36) CHARACTER SET utf8mb4 NOT NULL, `victory` tinyint(1) COMMENT '0=red' NOT NULL, `red_kill` int(11) DEFAULT 0 NOT NULL, `red_money` int(11) DEFAULT 0 NOT NULL, `blue_kill` int(11) DEFAULT 0 NOT NULL, `blue_money` int(11) DEFAULT 0 NOT NULL, `date` DATETIME NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` #### 创建关系表 关系表顾名思义用来存储资源与资源的关系, 一个用户与英雄的关系就是一个简单的属于关系, 一个用户可能有多个英雄,一个英雄也可能属于多个用户这就是一个典型的**多对多**关系所以要保存这种关系我们需要一个表来存放 ##### 用户英雄表(id, user_id, hero_id) ```SQL ### 在mysql客户端中运行 CREATE TABLE `user_hero` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `hero_id` int(11) NOT NULL, PRIMARY KEY (`id`, `user_id`, `hero_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 该表的每一条记录都保存着某个用户*(user_id)*与某个英雄*(hero_id)*的关系*(在此表中即为拥有关系)*在这基础上可以扩展更多字段, 比如添加期限字段表示某个用户拥有该英雄但不是永久的. ##### 用户对局表(id, user_id, battle_id, kill, death, assists, money, team) ```sql ### 在mysql客户端中运行 CREATE TABLE `user_battle` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `battle_id` int(11) NOT NULL, `kill` int(11) DEFAULT 0 NOT NULL, `death` int(11) DEFAULT 0 NOT NULL, `assists` int(11) DEFAULT 0 NOT NULL, `money` int(11) DEFAULT 0 NOT NULL, `team` tinyint(1) COMMENT '0=red' NOT NULL, PRIMARY KEY (`id`, `user_id`, `battle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 该表的每一条记录都保存着某个用户*(user_id)*进行过的对局*(battle_id)*在这基础上我们扩展了belong, kill, death, assists, money等字段用来存放用户在对局中的所属队伍, 杀敌数, 死亡数, 助攻数, 经济等. ##### 插入数据 ```SQL INSERT INTO `user` (name,sex,password) VALUES ('今晚打老虎',0,'123456'), ('魔德妖',0,'123456'), ('凉殄峰',0,'123456'), ('海马羊',1,'123456'), ('路人甲',1,'123456'), ('路人乙',1,'123456'), ('路人丙',0,'123456'), ('霖国徽',1,'123456'), ('路人王',1,'123456'), ('路人爹',1,'123456'); INSERT INTO user_battle (user_id,battle_id,`kill`,death,assists,money,team) VALUES (1,1,10,10,5,100000,1), (2,1,10,5,2,100000,1), (3,1,10,10,10,100000,1), (4,1,10,10,8,100000,1), (5,1,15,10,7,200000,1), (6,1,10,10,5,100000,0), (7,1,5,10,2,100000,0), (8,1,10,10,10,100000,0), (9,1,10,10,8,100000,0), (10,1,10,15,7,100000,0); INSERT INTO user_battle (user_id,battle_id,`kill`,death,assists,money,team) VALUES (6,2,10,10,5,100000,1), (7,2,10,5,2,100000,1), (8,2,10,10,10,100000,1), (9,2,10,10,8,100000,1), (10,2,15,10,7,200000,1), (1,2,10,10,5,100000,0), (2,2,5,10,2,100000,0), (3,2,10,10,10,100000,0), (4,2,10,10,8,100000,0), (5,2,10,15,7,100000,0); INSERT INTO user_battle (user_id,battle_id,`kill`,death,assists,money,team) VALUES (5,3,10,10,5,100000,1), (4,3,10,5,2,100000,1), (3,3,10,10,10,100000,1), (2,3,10,10,8,100000,1), (1,3,15,10,7,200000,1), (10,3,10,10,5,100000,0), (9,3,5,10,2,100000,0), (8,3,10,10,10,100000,0), (7,3,10,10,8,100000,0), (6,3,10,15,7,100000,0); INSERT INTO battle (map_name,victory,red_kill,red_money,blue_kill,blue_money,`date`) VALUES ('王者峡谷',1,45,500000,55,600000,'2022-01-25 13:06:36'), ('王者峡谷',0,45,500000,55,600000,'2022-01-25 13:06:36'), ('王者峡谷',0,45,500000,55,600000,'2022-01-25 13:06:36'); ``` > #### 了解MVC框架 > > Mvc即模型,视图, 控制这是抽象的, 因为我们项目采用MVC模式来构建所以体现在我们项目中的实现就是 > > Pojo类, jsp, Controller类接下来我们一一创建这几个实现,或许你就能慢慢理解MVC模式了 > ### 3.创建Jsp视图 让我们从顶层UI慢慢深入完成项目吧. #### 创建用户登录注册界面 首先要实现登录注册我们要提供一个用户接口*(UI)*给用户进行登录注册吧? ##### login.jsp 而这个登录ui我们通过jsp实现, 我们首先在index.jsp的同级目录上创建一个login.jsp的登录页面, 页面代码如下 ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> login

用户登录


<% if (request.getAttribute("alert") != null) { %> <% } %> ``` ##### register.jsp 相同的方式我们创建注册页面, 代码如下 ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> register

用户注册


<% if (request.getAttribute("alert") != null) { %> <% } %> ``` ##### index.jsp 用户登录或注册成功后需要呈现给用户的内容 ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="org.example.pojo.User"%> <% User user = (User) request.getAttribute("user"); %> index

欢迎用户<%= user.getName() %>!

总对局数: null 胜利场数: null 总杀敌数: null
总助攻数: null 总死亡数: null kd: null
胜率: null
点击查看战绩
``` > #### 了解前端与后端的沟通方式 > > 其实前端的页面不论如何复杂, 在与后端交互时大部分都是通过http协议来交流不过本教程不会深入解析http协议只需要了解协议中面向我们编码的部分即可. > > ##### http请求URL > > 其实就是浏览器顶部框框内显示的路径, 也是在我们程序中首先接触的. > > ##### http请求体 > > 前端发送的数据可以放在http协议中的许多位置包括请求URL, 其中主要数据都是放在请求体中, 放在这里的数据用户无法直接查看到该数据, 需要通过一些抓包工具. jsp创建完毕后点击运行项目, 并输入路径http://localhost:8080/project-name/login.jsp访问到login.jsp. *注意路径中的**project-name**要修改成你的项目名。* 可以看到tomcat**自动**为我们处理了http://localhost:8080/project-name/login.jsp这个请求并返回了一个html页面给我们, 居然是我们login.jsp里的内容, tomcat实在太厉害了. 接下来我们就要手动去处理这些请求了, 这就是后端要完成的工作. ### 4.创建Controller类 为了手动处理请求我们需要创建Controller类来处理请求,说得简单然鹅这个类不是那么容易就创建的这时候我们就要使用SSM中的springMVC, 要让我们的Controller类可以处理请求我们就必须依赖SpringMVC中**DispatcherServlet**(前端控制器). > #### DispatcherServlet > > 前端控制器, 它是一个中心Servlet,所有请求都经过它,但它不处理请求只负责将请求分发给委托组件例如我们写的Controller类(实际上更复杂)和传统的javaWeb多个servlet处理多个不同的请求相比,这样设计更灵活, 这种设计也称为前端控制器模式. ##### 导入SpringMVC 这个sevlet已经在springMVC中实现了, 我们只需要让它载入到tomcat中就能直接使用了. 首先我们要导入springMVC的jar包, 我们通过Maven导入, 所以只需要配置pom.xml配置即可自动下载导入 > **pom.xml是一个Maven项目配置文件** ```xml org.springframework spring-webmvc 5.3.14 ``` 导入后点击idea窗口右边的Maven侧边菜单, 点击后弹出一个侧边窗口, 再点击该窗口左上角的箭头圆圈(Reload All Maven Projects), idea就会调用Maven自动下载导入springMvc的jar包了, 并且版本是5.3.14的. ##### 配置DispatcherServlet 在下载导入成功后我们只需要像配置普通Servlet一样,在web.xml中配置即可 ```XML app org.springframework.web.servlet.DispatcherServlet contextConfigLocation 1 app / ``` > **关于DispatcherServlet的url-pattern配置问题 / 和 /* 和 \*.do 的区别** > > https://shmilyz.github.io/2017/08/24/DispatcherServlet/ 这样我们就成功把符合 **"project-name/*"** *(project-name是你的项目名字)* 路径的请求全都发给DispatcherSevlet去处理了, 接下来在main/java下创建包**org.example.controller**用来存放controller类。 ##### 创建UserController 顾名思义是用来处理用户相关的请求的 将它创建好并放在org.example.controller包下 ```java package org.example.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class UserController { @ResponseBody @GetMapping("user/login") public String login() { return "恭喜你登录了"; } @ResponseBody @GetMapping("user/register") public String register() { return "恭喜你注册了"; } } ``` 这样我们这个controller就可以处理***project name/user/login***和**project name/user/register**的请求了, 即登录和注册请求, 我们可以看到这两个函数返回的是字符串类型, 难道说可以返回给前端一个字符串吗? 不妨让我们运行项目在浏览器访问试试吧! > #### Java注解@Annotation > > 不用了解太深, 只需要知道它可以为类或方法和方法参数做标记即可, 既然做了标记, 想必在运行程序中的某一行代码中会读取和处理这些标记吧? > > 例如@Controller,@ResponseBody和@GetMapping. ##### 访问Controller 我们运行了项目, 并在浏览器输入了http://localhost:8080/project-name/user/login发现直接报404了是啥情况? 我们发现在访问这个路径后控制台打印了一条信息: ```shell 09-Jan-2022 13:33:15.432 警告 [http-nio-8080-exec-1] org.springframework.web.servlet.DispatcherServlet.noHandlerFound No mapping for GET /project-name/user/login ``` 居然是**DispatcherServlet**说**No mapping for GET /project-name/user/login**, 即没有找到我们路径的GET请求映射, > **project-name是你的项目名字** 我们的路径不是已经写在controller类里并和里面的函数关联起来了吗, 然并卵阿. ##### 了解spring与ioc容器 其实是因为DispatcherServlet并不知道我们Controller类的存在, 那怎么让这个前端控制器找到我们的Controller类呢,这是就不得不提到Spring中的IOC容器了, DispatcherServlet是从IOC容器中搜索Controller类对象的, 所以我们需要将我们的Controller类对象放进这个IOC容器, 但是在放进IOC容器之前我们得先创建它吧? > ##### IOC容器 > > 其实就是一个java对象容器, 但是里面的java对象是spring框架帮你创建的, 而spring框架又根据你的配置来创建, 简单来说将创建对象的方式从写代码变成写配置了, 这并不包括所有java对象, 而是一些重要且功能性强的javaBean对象, 当然里面可以存对象那肯定也能取出来而且也是spring帮你取的, 你也可以手动取就是了. ##### 创建spring配置 spring框架根据我们的配置去创建对象, 那我们就先建个配置吧, 在**src/main/webapp/WEB-INF/**目录下创建一个名为spring-mvc.xml的配置文件. ```xml ``` 可以看见beans标签内有很多属性阿, 不管它, 现在我们配置我们的Controller类 ```xml ``` 将这个**context:component-scan**标签添加进spring-mvc.xml内的beans标签内, 内容是让spring框架帮我们扫描**org.example.controller**包下的所有类把有带有**@Component**(例如@Controller)注解标记的类生成它的对象并存放进IOC容器。 ##### 载入spring配置 当然配置写好了, 怎么让spring框架载入我们的配置呢? 原来DispatcherServlert是可以帮我们载入spring配置的,在web.xml中看到我们的DispatcherServlet中的**init-param**标签 ```XML contextConfigLocation ``` 他的作用是调用DispatcherServlet的构造函数时, 传入名为contextConfigLocation的参数, 参数的值写在param-value标签内, 可以发现它是空的. 而这里就是告诉DispatcherSerlvet要载入的spring配置文件所在路径. ```xml WEB-INF/spring-mvc.xml ``` 让我们写spring-mvc.xml的路径上去吧, 这样一来, 在DispatcherServlet创建时, 就会让Spring框架帮我们加载这个配置了, 然后IOC容器就有我们配置中的bean对象了, 我们的DispatcherServlet就能找到UserController对象啦! ##### 响应数据的字符编码问题 我们再次访问http://localhost:8080/project-name/user/login可以看见不再是404了, 但是页面却是一堆问号????? 原来String类型的消息转换器默认编码是ISO_8859_1我们在spring-mvc.xml的beans标签下添加如下配置: > **注意**: 有@RequestBody注解标记的Controller方法的返回值都是交给HttpMessageConverter处理的, > > 详细请查阅官方文档: [Web on Servlet Stack (spring.io)#mvc-ann-requestbody](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestbody) ```XML ``` 就可以让String类型的消息转换器编码设置为UTF-8, 再次访问成功了! > #### HttpMessageConverter > > 关于SpringMVC的HttpMessageConverter的详细内容请查阅官方文档:[HttpMessageConverter (Spring Framework 5.3.14 API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/) > ##### 让Controller返回视图页面 在做这件事之前, 我们先解决一个安全问题吧, 我们可以看见我们的jsp页面都是放在webapp文件夹下的, 这样做非常不安全, 浏览器可以绕过controller层直接访问该jsp资源, 所以为了避免这样的事发生, 我们可以放在**webapp/WEB-INF**文件夹下这个文件夹是不能直接访问的,所以我们在该文件夹新建一个**views**文件夹再将所有jsp页面移进去吧. ###### ModelAndView和ViewResolver View视图是一个抽象的接口, 而jsp就是其的实现之一, ModelAndView 是MVC框架中的模型视图持有对象由控制器返回, 先不管模型, 要想返回JSP界面我们就修改login方法的返回值为ModelAndView吧, 该对象将会被**ViewResolver视图解析器**处理来返回我们的jsp页面. ```java @GetMapping("user/login") public ModelAndView login() { return new ModelAndView("/WEB-INF/views/login.jsp"); } ``` 可以看到返回的时候我们new出了一个ModelAndView对象, 并在构造方法中传入了我们jsp的路径, 还可以看到@ResponseBody注解被去掉了, 因为没必要再将返回值交给HttpMessageConverter去处理了, 让它默认交给ViewResolver处理吧. 我们再访问一下http://localhost:8080/project-name/user/login发现我们的login页面成功展示出来了,**同样的方法我们把注册页面也搞定吧.** > ##### ViewResolver视图解析器 > > *将从处理程序返回的基于逻辑字符串的视图名称解析为用于呈现响应的实际视图*,摘自官方文档, 说白了就是将视图名称解析为可渲染的视图View, 例如上面的"/WEB-INF/views/login.jsp"就是我们的视图名称. > > 如果你将视图名称变为"redirect: /WEB-INF/views/login.jsp" 视图解析器就会识别到并作出对应动作即发送重定向, 你也可以自己配置视图解析器为视图解析器手动配置视图名称的前缀后缀比如/WEB-INF/views/和.jsp, 这样视图名称只需要写login就好, 简洁明了. > > springMVC提供了很多种视图解析器,详细查阅[Web on Servlet Stack (spring.io) #mvc-viewresolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-viewresolver) ### 5.处理请求参数 我们创建Controller类并成功处理了页面请求, 接下来我们要处理的是登录请求, 用户通过我们的登录界面操作后要点击登录按钮了, 它就会发送请求给我们, 并且这个请求是携带了账号和密码的参数的, 所以我们就要正确的处理它, 比如验证账号是否存在, 密码是否正确. ##### 处理登录请求 **现在我告诉你: 我们的login.jsp页面在点击按钮后会发送/user/login的POST请求, 并携带参数名为name, 和password的参数这两个参数存放着用户名输入框的内容和密码输入框的内容**, 我们不需要了解前端页面是怎么包装请求参数的, 怎么发送的, 我们只需要知道我们即将要处理的某个请求路径和它携带的参数即可. 让我们在UserController中创建一个处理登录请求的方法吧: ```java @PostMapping("user/login") public ModelAndView login(@RequestParam("name") String username, @RequestParam("password") String password) { System.out.println("用户名: " + username); System.out.println("密码: " + password); return new ModelAndView("/WEB-INF/views/login.jsp"); } ``` 可以看到**@GetMapping("user/login")**被修改成了**@PostMapping("user/login")**, 这是因为我们的页面请求是GET方法, 而登录请求是POST方法, 如不使用正确的注解DispatcherServlet就不知道该发给谁. 分析一下上面处理登录请求的方法, 还是很容易看懂的, 通过@RequestParam("name")标记的方法参数, 我们就能获取请求携带的名为name的参数*(感受到SpringMVC的强大了吗)*, password同理, 我们处理的内容就是把它的输入的用户名和密码打印到控制台看看. 在login页面中输入账号123456密码ABCDE并点击登录按钮可以看见控制台打印了: ```shell 用户名: 123456 密码: ABCDE ``` 我们居然把用户的账号和密码毫无保留的展现出来了,而且可以对这账号密码做更多的事, 想想就激动. ##### 处理注册请求 ***现在我告诉你: 我们的register.jsp页面在点击按钮后会发送/user/register的POST请求, 并携带参数名为name, 和password还有sex的参数这些个参数存放着用户名输入框的内容和密码输入框的内容还有性别***, 我们以相同的方法, 将这些用户信息毫无保留的打印到控制台吧! ##### 请求数据的字符编码问题 我们发现在前端输入非中文字符可以正确的打印出来, 但是如果是中文就会变成乱码, 这是因为我们接收到的请求默认是用ISO_8859_1解码的, 怎么设置成UTF-8呢, 我们可以通过配置过滤器为每一条请求都设置成用UTF-8解码, 在web.xml中添加: ```xml CharacterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 CharacterEncodingFilter /* ``` 可以看见url-pattern设置为/\*即处理每一条请求, 也可以设置成/user/\*即仅处理/user路径下的每一条请求 > #### Filter过滤器 > > 在特定路径*(例如上面配置的 /\*)*的请求进入Servlet之前先进入它, 它可以决定这个请求要不要传递给下一个filter或servlet. > > ##### org.springframework.web.filter.CharacterEncodingFilter > > 是springMVC帮我们写好的过滤器, 我们只需要传入想要的编码名称和配置过滤器要处理的请求路径就行, 非常方便. > > 详细查阅官方文档: [CharacterEncodingFilter (Spring Framework 5.3.14 API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/) ### 6.整合Mybatis 我们已经可以处理登录请求的参数了, 要想完成登录注册就需要在数据库中查询用户, 或者保存用户进数据库, mybatis一个强大的持久层框架, 协助我们在程序中操作数据库, 我们现在就要将mybatis使用到我们的项目中,首先导入它的jar包吧. ##### 导入mybatis,mybatis-spring, spring-jdbc, mysql驱动, 依赖 在pom.xml的dependences标签中添加: ```XML org.springframework spring-jdbc 5.3.14 org.mybatis mybatis 3.5.2 org.mybatis mybatis-spring 2.0.6 mysql mysql-connector-java 8.0.16 ``` mybatis与spring的版本关系: | MyBatis-Spring | MyBatis | Spring Framework | Java | | -------------- | ------- | ---------------- | ------ | | 2.0 | 3.5+ | 5.0+ | Java 8 | | 1.3 | 3.4+ | 3.2.2+ | Java 6 | 添加后点击idea窗口右边的Maven侧边菜单, 点击后弹出一个侧边窗口, 再点击该窗口左上角的箭头圆圈(Reload All Maven Projects), idea就会调用Maven自动下载导入jar包了, 简单说一下: - **Mybatis**是我们的持久层框架, 更容易的操作数据库. - **MyBatis-Spring** 会帮助你将 MyBatis 无缝地整合到 Spring 中, 但需要依赖**spring-jdbc**. - **Mysql-connector** 数据库连接驱动, 负责底层的连接和操作mysql数据库. > #### 数据库连接驱动 > > 不同的数据库为java提供.他们的连接驱动以便跟它们的数据库进行通信,例如mysql数据库就提供了mysql-connector-java的数据库驱动,实际上该驱动是一个jar包里面有jdbc接口的实现, 而mybatis只是操作jdbc接口而不管里面的实现是什么. ##### 配置Datasource和SqlSessionFactory到ioc 根据mybatis官方文档(http://mybatis.org/spring/zh/getting-started.html)的说明, 我们要将它整合进spring需要配置Datasource和SqlSessionFactory到IOC容器: ```XML ``` 可以看见名为dataSource的这个bean传入的都是需要连接数据库的基本用户信息和要使用的数据库驱动信息, 可以猜出来这个bean是用来连接数据库的. ##### SqlSessionFactory与SqlSession SqlSessionFactory是mybatis的提供的接口用来创建SqlSession的而SqlSession是用来执行sql语句的 > #### SqlSessionFactoryBean > > 是mybatis-spring下的对象, 实现了spring的FactoryBean接口, 可以让spring创建SqlSessionFactory对象, > > 这个对象有两个参数dataSource和mapperLocations一个是数据源一个是mapper.xml配置文件的所在路径. ##### Mapper映射文件 *MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。——mybatis* 所以映射文件其实就是写SQL的地方mybatis通过读取映射文件将里面的sql映射到对应的目标方法中,在执行该目标方法就会自动执行对应的sql语句,介于mybatis的强大,我们不需要编写目标方法的具体实现,只需要编写方法的需要的参数和返回值即可。 ##### 编写UserMapper.xml 让我们写一个根据用户id去查询用户的mapper文件吧. ```xml ``` 分析上面的文件内容, 我们可以看见mapper标签中的namespace属性, 它可以说是mapper的id或者名字, 再看里面的select标签,它也有一个id还有一个结果类型或者说返回类型然后里面的内容就是SQL语句了, 一个通过id查询的sql语句可以看见有个不属于sql语句的东西#{id}. 首先介绍一下select标签, 顾名思义里面编写的是select语句可以传入参数和返回数据,这里我们返回的是一个User类仅仅用来存放数据即Pojo类, 在这里存放的是什么数据呢? 我们在之前创建用户表的时候有设置过它字段, 这就是一个用户所携带的数据, 而我们使用sql查询出来后, 就把这个数据存放到User对象中, 当然这个对象是由mybatis来创建,而且也是由mybatis来将数据存放到User对象, 感受到mybatis的强大了吗. > ##### Pojo类 > > pojo即简单java对象, 仅仅用来存放数据的对象, 只有一些字段和一系列的setter和getter方法。对象的字段正好对应着表的字段, 所以用来存放表的一行数据再好不过了. 那#{id}是什么? 这是mybatis的参数符号,这说明我们在使用这个select语句时需要传入一个名为id的参数. 所以select标签是用来编写**PreparedStatement**(预处理语句的)的这样的语句最终会被mybatis处理好然后通过JDBC执行, 类似的标签还有**delete,update,insert.** ##### 创建User类 既然提到了Pojo类我们就创建一个用户的Pojo类吧, 在项目org.example包下创建一个Pojo包,然后根据User表来创建Pojo类: ```java package org.example.pojo; public class User { private Integer id; private String name; private boolean sex; private String password; // 底下的setter和getter还有toString方法其实都是通过idea自动生成, 具体如何操作请百度吧. public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isSex() { return sex; } public void setSex(boolean sex) { this.sex = sex; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", sex=" + sex + ", password='" + password + '\'' + '}'; } } ``` 可以看见里面的字段与表中的字段名字一模一样但是字段类型有些不同的. 我们可以在下表查询数据库类型与java类型的对应关系: | **类型名称** | **显示长度** | **JAVA类型** | | ------------ | ------------ | -------------------- | | VARCHAR | L+N | java.lang.String | | CHAR | N | java.lang.String | | BLOB | L+N | java.lang.byte[] | | TEXT | 65535 | java.lang.String | | INTEGER | 4 | java.lang.Long | | TINYINT | 3 | java.lang.Integer | | SMALLINT | 5 | java.lang.Integer | | MEDIUMINT | 8 | java.lang.Integer | | BIT | 1 | java.lang.Boolean | | BIGINT | 20 | java.math.BigInteger | | FLOAT | 4+8 | java.lang.Float | | DOUBLE | 22 | java.lang.Double | | DECIMAL | 11 | java.math.BigDecimal | | BOOLEAN | 1 | java.lang.Boolean | | ID | 11 | java.lang.Long | | DATE | 10 | java.sql.Date | | TIME | 8 | java.sql.Time | | DATETIME | 19 | java.sql.Timestamp | | TIMESTAMP | 19 | java.sql.Timestamp | | YEAR | 4 | java.sql.Date | 这样我们就创建成功了. ##### 编写UserMapper接口 我们既然编写了User映射文件, 那我们怎么使用里面的select语句呢,因为mybatis的强大, 我们可以像调用方法一样来调用select语句, 使用让我们来编写这个方法吧在org.example包下创建mapper包然后编写UserMapper接口: ```java package org.example.mapper; import org.example.pojo.User; public interface UserMapper { User selectById(int id); } ``` 我们写了一个接口里面有个``` User selectById(int id)```方法可以看到这个方法的**名字, 返回值, 参数名**对应着select标签的**id, resultType, #{id}**, 而mapper标签的**namespace**又对应着我们的UserMapper接口类, 基于这些信息, 我们就不用实现UserMapper接口即不需要编写UserMapper的实现类不需要写selectById的方法逻辑, mybatis通过这些信息将对应的语句映射到对应方法, 这样我们只用调用对应方法就能执行对应的sql语句了, 又感受到了mybatis的强大. ##### 配置UserMapper到IOC容器 我们不能直接调用UserMapper接口的方法我们需要一个UserMapper的对象, 这个对象怎么来的? 没错这个对象由mybatis来创建,那我们怎么获取这个对象? 没错我们通过spring容器来获取. 怎么交给myabtis来创建UserMapper对象? 通过查阅mybatis-spring文档[mybatis-spring-started](http://mybatis.org/spring/zh/getting-started.html) 我们要配置mybatis的一个bean到spring容器中, 这个bean就是MapperFactoryBean: ```xml ``` 可以看见这个MapperFactoryBean与我们之前配的SqlSessionFactorybean简直一模一样, 可以猜测它也是实现了spring的FactoryBean接口. 这个bean接受两个参数mapperInterface和sqlSessionFactory, 第一个参数很容易理解看第二个参数, 它通过ref属性引用了id为sqlSessionFacotory的bean然后将该bean就是我们之前配置的id为sqlSessionFactory, class为SqlSessionFactoryBean的bean传给MapperFactoryBean. MapperFactoryBean通过mapperInterface参数实现我们的UserMapper接口而接口的方法实现又要调用sqlSessionFactory对象来创建sqlSession并最终执行SQL语句和返回结果并且在我们配置SqlSessionFactory时传入了mapperLocations参数所以Mybatis就是通过sqlSessionFactory来找到UserMapper的XML映射文件. ##### 实现UserController.info(id)方法 这样一来我们IOC容器就存在UserMapper的bean对象了, 让我们在UserController中拿到它并调用吧. ```java // 在UserController中添加mapper字段和info方法 @Autowired private UserMapper mapper; @GetMapping("user/info") @ResponseBody public String info(@RequestParam("id") Integer id) { User user = mapper.selectById(id); return user.toString(); } ``` 这里我们看见了一个新注解@Autowired它是spring的注解, 作用是根据字段类型从ioc容器中找到与其相符的类型并写入该字段, 这就是spring的依赖注入的方法之一, spring是不是很强大? 前提是UserController对象必须是spring来创建的并且ioc容器中只有一个UserMapper类型的bean, 否则这个注解就不会生效或者报错. ##### 访问UserController.info(id)方法 让我们运行项目然后在浏览器中输入http://localhost:8080/project-name/user/info?id=1并访问可以看见我们成功的将一个用户id为1的信息从数据库搬到浏览器页面上了spring和mybatis真是太牛逼了哈哈. ### 7.完成用户登录业务 在前面其实我们已经学习了完成业务功能所需要的最基本的代码, 现在让我们利用所学代码来完成我们的用户登录业务吧. 其实这个业务的思路很简单首先是用户注册, 我们后端接受到用户的注册信息, 我们就需要查看这个用户信息有没有被注册过, 没有的话就注册成功并把该信息存放到数据库, 然后是登录在后端接受用户的登录信息, 也是一样在数据库查询有没有这个用户, 并检验密码是不是相同的,如果相同就登录成功. ##### 创建UserService接口 通常这些逻辑我们不会写到Controller层因为这样会让controller的方法变得臃肿,并且违背了Controller层设计的初衷, 所以让我们创建一个服务层*(Service)*来让controller调用吧. **在example.org下创建service包并添加UserService接口:** ```java public interface UserService { ModelAndView login(User user); } ``` 我们创建了一个UserService里面有个login方法并且我们让它返回视图和模型, 这样我们可以直接在controller调用返回就行了, 为什么是创建接口而不是类? 让我们接着往下写. ##### 修改UserController.login() 我们要修改的是只接受**POST**请求的方法不要改错了 ```JAVA @AutoWired private UserService userService; @PostMapping("user/login") public ModelAndView login(@RequestParam("name") String username, @RequestParam("password") String password) { User user = new User(); user.setName(username); user.setPassword(password); return userService.login(user); } ``` 可以看见这个方法异常简单仅仅帮我们封装参数到pojo对象让后传给service调用即可. 到这里我们在UserService还没有实现的情况下我们就已经完成了Controller的login方法逻辑, 这就是面向接口编程,在controller层我们只要知道要调用service的什么方法即可, 不用理会这个方法里面的逻辑是如何写的, 这有什么好处?我们接着往下写. ##### 实现UserService接口 在service包下我们创建一个**UserServiceImpl**类并使用**implements**关键字实现UserService方法 ```java @Service public class UserServiceImpl implements UserService{ @Override public ModelAndView login(User user) { return null; } } ``` 可以看见这个类有着与我们接口方法一模一样的返回值和方法名还有参数, 不同的是它具有方法体,这样我们就可以在login里写我们的登录代码逻辑了. 这里我们看见了一个新的spring注解**@Service**它和**@Controller**仅仅是名字上的不同都是标记这个类, 让spring帮我们创建它的对象并添加进ioc容器中. 为什么要这样, 因为我太懒了不想自己在controller层创建对象, 我选择让spring帮我创建并自动注入到controller对象中,希望你们也要喜欢这样做. > #### 简单说明UserService接口的作用: > > 如你所见我们创建UserServiceImpl这个类并实现UserService接口, 而我们Controller层实际上会调用的是UserServiceImpl里的方法, 那我们完全可以把controller层中userService字段的类型从UserService修改成UserServiceImpl这样对我们的controller无任何影响, 为什么不这样做的原因是我们要让Controller层依赖的是**接口**而不是具体的**实现类**, 有啥好处? 简单来说因为你可以创建多个这个接口的不同实现类, 每个类的名字必然不同, 实现逻辑也会有差别, 但是方法名和返回值还有方法参数都不会改变, 如果我们Controller一开始使用的是UserServiceImpl类当我们不满意这个类的实现时我们就要换成其他类相对的就要修改Controller上的代码. > > 假设不只是UserController使用UserServiceImpl呢? 假设有几百个类都要使用UserServiceImpl然后它们都不满足UserServiceImpl代码的性能? 那这样都要修改这几百个类里的代码, 如果这几百个类都是用UserService接口, 那我们想换哪个实现类就换哪个. > > 也许你会说那我直接修改UserServiceImpl里的代码实现不就好了吗, 假设你只要修改里面的某个实现但是里面的代码多到爆而且还不是你写的还没有注释而且写得很烂,指不定修改哪个方法就会导致另外的方法报错, 这时你会选择新建一个实现类还是整理这屎山代码呢? > > *面向接口编程的好处不止于此, 这里只是简单的举例5556它的好处之一, 在项目不断迭代变得越来越大时, 面向接口的好处就会慢慢体现出来了.* ##### 完成UserServiceImpl.login()逻辑 ```JAVA @AutoWired private UserMapper userMapper; // 首先在需要访问数据库, 我们让spring自动注入UserMapper对象 @Override public ModelAndView login(User user) { ModelAndView modelAndView = new ModelAndView(); User dUser = userMapper.selectByName(user.getName()); if (dUser == null) { modelAndView.addObject("alert", "用户名不存在"); modelAndView.setViewName("/WEB-INF/views/login.jsp"); } else if (!dUser.getPassword().equals(user.getPassword())) { modelAndView.addObject("alert", "密码错误"); modelAndView.setViewName("/WEB-INF/views/login.jsp"); } else { modelAndView.addObject("user", dUser); modelAndView.setViewName("/WEB-INF/views/index.jsp"); } return modelAndView; } ``` 上面的代码很容易看懂, 就是根据用户名查询用户, 判断用户是否存在, 存在就判断密码, 正确就跳转到index.jsp页面, 这里的**userMapper.selectByName(String name)**请自己实现, 根据之前的例子照葫芦画瓢也能弄出来吧? 逻辑很简单, 但是我看到了一个冗余的东西, 而且不美观, 它就是视图名字, 我们每次都要设置视图的全路径, 非常麻烦, 其实我们可以交给视图解析器来帮我们设置前后缀, 这样我们就可以去除重复的部分了, > #### addObject(String k, String v)和model > > 它是向model对象添加一个键值的方法, 可以在视图渲染时读取到该对象, 例如jsp可以在request域中根据键名取得model中的值, 所以model就是可以传给视图的数据对象, 供视图渲染时取用, 仅在视图渲染时使用. > > 以上例子通过给model添加alert属性, 前端就会弹出消息框并展现alert属性的值, 为什么是alert属性, 是因为前端代码需要的, 如果你觉得不好,你也可以请前端喝杯茶叫他改一下. ##### 配置视图解析器 将它添加到spring配置文件中: ```XML ``` 这样我们的代码就可以优化成: ```JAVA @Override public ModelAndView login(User user) { ModelAndView modelAndView = new ModelAndView(); User dUser = userMapper.selectByName(user.getName()); if (dUser == null) { // 判断用户是否存在 modelAndView.addObject("alert", "用户名不存在"); modelAndView.setViewName("login"); } else if (!dUser.getPassword().equals(user.getPassword())) { // 判断密码 modelAndView.addObject("alert", "密码错误"); modelAndView.setViewName("login"); } else { modelAndView.addObject("user", dUser); modelAndView.setViewName("index"); } return modelAndView; } ``` 是不是干净了许多. **注意:** - 记得Controller层里的返回视图名称的代码也要优化否则 404. - spring配置中的组件扫描标签要添加service包,否则spring在依赖注入时找不到service对象然后报错. ```xml ``` 接下来我们访问http://localhost:8080/user/login然后输入账号密码就会提示我们用户不存在了 ### 8.完成用户注册业务 同样的方式我们通过修改controller和UserService还有UserServiceImpl来完成注册业务吧, 要完成该业务需要使用映射文件的inser标签: ```xml insert into user(name, password, sex) values(#{name}, #{password}, #{sex}) ``` 在添加进映射文件后根据以上信息: - 我们就要为UserMapper接口添加**ModelAndView add(User user)**方法 - 再为UserService添加**register**方法功能来实现注册功能.***在接口添加方法然后在实现类实现方法.*** - register有着与login一样的返回值和参数, 逻辑上与login基本一致, 下面仅仅简述流程, 请自己实现 - 需要先判断密码是否小于6位, 与用户名是否小于3位, 用户名是否已注册, 并通过alert属性提醒用户, 如果验证成功就通过userMapper将该用户添加进数据库并让用户进入主页. ### 9.使用拦截器完成简单的权限控制 我们已经完成了用户的登录与注册, 但是主页不论用户有没有登录都能访问, 这是我们不希望发生的, 所以我们可以通过拦截器先拦截判断用户请求是否登录, 然后决定是否放行. ##### 创建拦截器 在org.example包下创建interceptor包并在包下创建名为UserInterceptor的类并实现springMVC的HandlerInterceptor接口: ```java public class UserInterceptor implements HandlerInterceptor { } ``` 通过idea创建接口方法的实现, 右击类中花括号内部的空白部分, 选择Generate再选择Override Methods, 然后选择接口中的preHandle方法, idea就会自动帮我们生成接口方法的实现了. ##### 导入servlet-api依赖 我们创建接口的实现后, 发现需要依赖javax.servlet这个包, 实际上我们的项目用的jsp和springMVC都需要这个包, 但是为什么我们的项目还能够运行? 原因其实是tomcat环境为我们提供了这个包, 所以我们的项目能够正常运行, 但现在我们在代码中主动使用这个包了, 所以要想编译通过就需要导入该包: ```java org.apache.tomcat tomcat-servlet-api 8.5.73 provided ``` 可以看见我们这个maven依赖配置多了个**scope**标签内容是**provided**意思是该依赖由外部提供, maven不会帮我们编译进我们的项目, 所以我们还是会使用tomcat为我们提供的servlet包, 而不是使用maven为我们下载的另一个servlet包, 而且这个依赖的版本与tomcat一致, 最终我们创建的拦截器是这样的: ```JAVA public class UserInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } } ``` 介绍一下preHandle方法, 该方法会在请求到达controller之前被调用, 返回false则拦截该请求, 它接受两个参数一个是request和response, 还有handler, 这里我们要判断的是用户请求到底有没有登录, 所以我们不用理会request以外的参数, request就是我们即将发给controller的用户请求, 我们可以读取和修改它. ##### 判断用户是否登录逻辑 实现preHandle来判断用户是否登录, 来决定是否放行该请求. 通过在reuqest中获取session并判断session中的user属性是否存在来判断用户是否登录, 下面是示例代码: ```java public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/user/login"); return false; } return true; } ``` 代码很简单, 意思是从请求对象中获取session然后从session中取出键名为user的值, 如果该值不为空则用户存在返回true放行请求, 反之就让浏览器重定向到登录界面. > #### Session和Cookie > > 简单来说session是一个存储在服务器的对象, cookie是一个存储在浏览器的对象, 这两个对象的作用都是存储数据. > > 假设有10个浏览器请求我们的项目, 我们要为每个浏览器分配一个session,分配完成后在下一次请求中, 项目如何访问属于该浏览器的session? 我们可以在分配session完成后的响应中通知浏览器在cookie中添加一个数据, 那就是sessionID, 这样浏览器接下来的每次请求都发送cookie中的数据给项目, 项目通过数据中的sessionID查找到对应的session那我们就成功访问属于该浏览器的session了. > > *想知道如何在响应中通知浏览器设置cookie可以百度一下, 这里框架已经帮我们自动完成了*. ##### 修改service的login逻辑来保存浏览器的登录状态 很显然这个拦截器永远不会为true, 因为我们的项目从来就没有将user放进session过, 所以我们要修改登录逻辑来将user存放进session中, 下面给出示例代码: ```java public ModelAndView login(User user) { ModelAndView modelAndView = new ModelAndView(); User dUser = userMapper.selectByName(user.getName()); if (dUser == null) { // 判断用户是否存在 modelAndView.addObject("alert", "用户名不存在"); modelAndView.setViewName("login"); } else if (!dUser.getPassword().equals(user.getPassword())) { // 判断密码 modelAndView.addObject("alert", "密码错误"); modelAndView.setViewName("login"); } else { session.setAttribute("user", dUser); modelAndView.addObject("user", dUser); modelAndView.setViewName("index"); } return modelAndView; } ``` 代码很简单就在登录成功的逻辑中添加session.setAttribute("user", dUser)即可是不是跟下面的addObject有点相似, 可以理解它们的功能都是一样的, 你问我这个session从哪里来? 当然也是让我们的spring框架自动注入了, 通过向service类添加session字段即可: ```java @Autowired private HttpSession session; ``` *Why the singleton bean service use autowired for using HttpSession and can use for handling multiple request from different broswer?* ##### 修改service的register逻辑来保存浏览器的登录状态 当然如果我们注册成功那也是默认登录该用户了, 那我们就要修改register的逻辑来在session中保存用户的登录状态. ##### 使用拦截器 拦截器写好后, 还要配置到spring后才能使用: ```xml ``` 添加到spring配置中, 我们拦截器就会拦截/project-name/app以下的所有请求了 ##### 编写AppController 当我们访问http://localhost:8080/project-name/app时, 发现404了, 发现拦截器没有帮我们拦截然后重定向到登录界面,难道拦截器配错了?,其实不然. 让我们在controller包下新建一个AppController来处理请求, 我们默认这个controller都是用户登录后才能使用的: ```java @Controller public class AppController { @RequestMapping("app") public ModelAndView index(HttpSession session){ ModelAndView modelAndView = new ModelAndView("index"); modelAndView.addObject("user", session.getAttribute("user")); return modelAndView; } } ``` 重启项目再次访问http://localhost:8080/project-name/app, 发现拦截器又生效了, 可以看见在请求没有处理程序的情况下,拦截器不会生效这也许是拦截器智能的表现吧? 我们登录后重新访问http://localhost:8080/project-name/app可以看见我们成功进入了index页面, 成功的使用拦截器进行权限控制! ### 10.完善功能 ##### 创建Pojo类BattleData 让我们深入完成这个app的功能吧, 首先创建Pojo: ```java public class BattleData { private int totalBattle = 0; private int totalVictory = 0; private int totalAssist = 0; private int totalDeath = 0; private int totalKill = 0; /** * 计算胜率 * @return 返回胜率 */ public float getOdds() { if (totalBattle == 0) { return 0.0f; } return (float) totalVictory / totalBattle; } /** * 计算kd * @return 返回KD */ public float getKd() { if (totalDeath == 0) { return totalKill; } return (float) totalKill / totalDeath; } // 请使用idea自行生成getter和setter } ``` ##### 创建AppMapper与映射文件并配置到IOC ```java public interface AppMapper { /** * 获取目标用户的战绩数据 * @param user 目标用户 * @return 目标用户的战斗数据 */ BattleData queryBattleData(User user); } ``` **编写AppMapper.xml:** ```xml ``` **配置到IOC容器:** ```xml ``` ##### 创建AppService和其实现 ```java public interface AppService { BattleData queryBattleData(User user); } ``` AppService实现: ```java @Service public class AppServiceImpl implements AppService{ @Autowired private AppMapper appMapper; @Override public BattleData queryBattleData(User user) { return appMapper.selectBattleData(user.getId()); } } ``` ##### 更新index.jsp和AppController.index() 更新index.jsp和AppController.index()使其支持BattleData: ```jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="org.example.pojo.User"%> <%@ page import="org.example.pojo.BattleData"%> <% User user = (User) request.getAttribute("user"); BattleData battleData = (BattleData) request.getAttribute("battleData"); if (battleData == null) { battleData = new BattleData(); } %> index

欢迎用户<%= user.getName() %>!

总对局数: <%= battleData.getTotalBattle() %> 胜利场数: <%= battleData.getTotalVictory() %> 总杀敌数: <%= battleData.getTotalKill() %>
总助攻数: <%= battleData.getTotalAssist() %> 总死亡数: <%= battleData.getTotalDeath() %> kd: <%= battleData.getKd() %>
胜率: <%= battleData.getOdds() * 100 %>%
点击查看战绩
``` **AppController.index():** 这里我们要用到AppService不要忘记自动注入 ```java @RequestMapping("app") public ModelAndView index(HttpSession session){ ModelAndView modelAndView = new ModelAndView("index"); User user = (User) session.getAttribute("user"); modelAndView.addObject("user", user); modelAndView.addObject("battleData", appService.queryBattleData(user)); return modelAndView; } ``` ##### 更新UserServiceImpl.login()与rigister() 更新login和rigister方法使其不再直接返回index的视图, 而是让用户重定向到/app让AppController处理 ```java @Override public ModelAndView login(User user) { ModelAndView modelAndView = new ModelAndView(); User dUser = userMapper.selectByName(user.getName()); if (dUser == null) { modelAndView.addObject("alert", "用户名不存在"); modelAndView.setViewName("login"); } else if (!dUser.getPassword().equals(user.getPassword())) { modelAndView.addObject("alert", "密码错误"); modelAndView.setViewName("login"); } else { session.setAttribute("user", dUser); // modelAndView.setViewName("index"); // modelAndView.addObject("user", user); modelAndView.setViewName("redirect:/app"); } return modelAndView; } @Override public ModelAndView register(User user) { ModelAndView modelAndView = new ModelAndView(); if (user.getPassword() == null || user.getPassword().length() < 6){ modelAndView.addObject("alert", "密码长度不能小于6"); modelAndView.setViewName("register"); } else if (user.getName() == null || user.getName().length() < 3) { modelAndView.addObject("alert", "用户名长度不能小于3"); modelAndView.setViewName("register"); } else { User dUser = userMapper.selectByName(user.getName()); if (dUser != null) { modelAndView.addObject("alert", "用户名已存在"); modelAndView.setViewName("register"); return modelAndView; } userMapper.add(user); session.setAttribute("user", user); // modelAndView.setViewName("index"); // modelAndView.addObject("user", user); modelAndView.setViewName("redirect:/app"); } return modelAndView; } ``` ##### 登录到用户今晚打老虎 访问http://localhost:8080/project-name/user/login 输入账号: 今晚打老虎 密码: 123456 我们就能看到今晚打老虎的数据了 ### 11.处理非页面请求 有时候前端需要的不是页面, 只是数据, 这时候我们就不能返回给前端页面了而是返回数据, 这时候就要用到@ResponseBody注解, 并且我们的数据以json格式返回. > #### JSON(JavaScript Object Notation) > > 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。 现在前端在点击按钮后会发送/app/battle请求并携带用户名参数, 意思是需要请求该用户的战绩,这时候我们就要返回战绩数据的json格式, 并且你们做好了约定,json里应该有的数据: ```json { "code" : "服务器返回的状态码", "msg" : "服务器返回的消息", "data" : "服务器返回的数据" } ``` 通常code为0就代表这个请求成功的处理了. 如果code为其他那这个请求就出问题了, 然后msg会说明出问题的原因, 最后被前端展现给用户. ##### 创建AppController.queryBattle(String username); 现在让我们创建一个方法来处理这个非页面请求吧: ```java @RequestMapping("app/battle") @ResponseBody public Map queryBattle() { Map ret = new HashMap<>(); ret.put("code", 1); ret.put("msg", "接口测试"); return ret; } ``` 这里我们返回的是一个map对象, 通常json都是有格式的字符串类型的数据, 如果我们返回字符串的话, 我们就要自己将字符串格式化为json格式,非常麻烦, 我们可以让它返回map然后交给消息转换器来帮我们自动将Map转为json字符串. ##### 配置消息转换器(MessageConverter) ```xml ``` 之前我们因为字符编码问题配置了一个字符串类型的消息转换器, 这次我们配置一个MappingJackson类型的消息转换器吧, 该转换器不仅支持map类型的对象, 还支持pojo等对象的转换. 不过该对象依赖第三方库**Jackson** > #### 关于Jackson库 > > Jackson被称为“Java JSON库”或“Java最好的JSON解析器”。 > > 更多内容请查阅官网[GitHub - FasterXML/jackson: Main Portal page for the Jackson project](https://github.com/FasterXML/jackson) 所以我们要在maven中配置Jackson的依赖才能使用这个转换器否则报错ClassNotFound~. ##### 配置Jackson依赖 ```xml com.fasterxml.jackson.core jackson-databind 2.12.3 ``` 这里我们使用2.12.3版本的jackson 这个时候, 我们登录进index然后**点击查看战绩**就能看见: *接受到的数据为: {"msg": “接口测试”,"code":1}* 真的是非常方便啊. ##### 编写Service 接下来我们就真正要处理这个请求了,让我们在Service层编写逻辑. 在AppService中添加新的方法, 并在AppServiceImpl中实现: ```java Map queryBattle(String username); ``` ```java public Map queryBattle(String username) { Map ret = new HashMap<>(); ret.put("code", 0); ret.put("msg", "请求成功"); ret.put("data", null); return ret; } ``` 然后在controller中调用: ```java @RequestMapping("app/battle") @ResponseBody public Map queryBattle(@RequestParam("user") String username) { return appService.queryBattle(username); } ``` 让我们重新运行项目并刷新页面然后再次**点击查看战绩**就发现页面显示**暂无数据...** 其实是因为我们data字段返回的是null, 要显示数据我们该如何封装data中的数据呢? 其实这也是要与前端约定好的. 现在前端告诉我data字段要为一个数组, 并且内容为一个一个的战绩对象, 然后每个对象又有一些数据, 这里我们用文字很难描述出来, 请看如下data的数据结构: ```json { "code" : 0, "msg" : "请求成功", "data" : [ { "mapName" : "地图名", "date" : "时间", "victory" : "胜利方", "teams" : [{ "name" : "用户1名", "kill" : "杀敌数", "assists" : "助攻数", "death" : "死亡数", "money" : "经济", "team" : "所属团队" },{ "name" : "用户2名", "kill" : "杀敌数", "assists" : "助攻数", "death" : "死亡数", "money" : "经济", "team" : "所属团队" }...] // 一共有10个用户此处省略 }, ... // data中可能有多个战绩信息此处省略 ] } ``` 这个结构还有其他8个用户没展现完但都一样, 看起来蛮复杂的,其实就是数组与对象, 我们将它转成Pojo类就没那么复杂了. ##### 创建Pojo类Battle ```java public class Battle { static class BattleUser { private String name; private Integer kill; private Integer assists; private Integer death; private Integer money; private Boolean team; // false = 红队 // 请使用idea自行创建Setter与Getter方法 } private String mapName; private Date date; private Boolean victory; // false = 红队获胜 private List teams; // 请使用idea自行创建Setter与Getter方法 } ``` 看这个类的结构是不是很容易就看懂了上面json的结构了? 此时的data字段其实就是一个Battle对象集合:``` List data;``` ##### 创建AppMapper.selectBattles(String username) 要获取战绩数据当然要查询数据库了, 那我们就开始编写mapper与映射文件吧: ```java List selectBattles(String username); ``` 在AppMapper中添加以上方法并编写方法对应的映射文件: ```xml ``` 这个映射文件有点复杂,为了满足List\类型的对象也是没办法的事. 然后我们在service中调用这个mapper方法并填进data字段即可, 消息转换器会自动帮我们把pojo的数据转换为json格式, 注意这个pojo必须有getter方法. 让我们重启项目,再次刷新页面后**点击查看战绩**可以看见我们的数据成功的显示出来了. 恭喜恭喜!