1)博客内容为B站UP如果你想直接学习视频,可以点击传送门。老师解释得很仔细。 2)本课程的文档和源码下载链接为:传送门。 3)本文博客markdown资源下载链接为:传送门。
目录:
- 1 系统概述与环境建设
-
- 1.1 系统开发和运行环境
- 1.2 项目分析
- 1.3 搭建项目
-
- 1.3.1 项目结构
- 1.3.2 创建Spring Initializer 项目
- 1.3.3 创建数据库
- 1.4 测试连接
- 1.5 问题与解决
- 2 用户注册
-
- 2.1 用户-创建数据表
- 2.2 用户-创建实体类
- 2.3 用户-注册-持久层
-
- 2.3.1 需要实施的规划SQL语句
- 2.3.2 设计界面和抽象方法
- 2.3.3 配置SQL映射
- 2.3.4 编制单元测试
- 2.4 用户-注册-业务层
-
- 2.4.1 业务的定位
- 2.4.2 规划异常
- 2.4.3 接口和抽象方法
- 2.4.4 抽象方法的实现
- 2.4.5 编制单元测试
- 2.4.6 密码加密介绍
- 2.5 用户-注册-控制层
-
- 2.5.1 创建响应结果类
- 2.5.2 设计请求
- 2.5.3 处理请求
- 2.5.4 调整控制层
-
- 1) @ExceptionHandler 注解
- 2) 解耦控制层的异常处理代码
- 2.6 用户-注册-前端页面
-
- 2.6.1 AJAX 介绍
- 2.6.2 前端页面
- 3 用户登录
-
- 3.1 用户-登录-持久层
-
- 3.1.1 需要实施的规划SQL语句
- 3.1.2 接口与抽象方法
- 3.1.3 配置SQL映射
- 3.2 用户-登录-业务层
-
- 3.2.1 规划异常
- 3.2.2 接口和抽象方法
- 3.2.3 抽象方法的实现
- 3.3 用户-登录-控制器
-
- 3.3.1 处理异常
- 3.3.2 设计请求
- 3.3.3 处理请求
- 3.4 用户-登录-前端页面
- 3.5 用户-登录- Session会话
-
- 3.5.1 当前登录方式可能出现的问题
- 3.5.2 何为 Session
- 3.5.3 使用
- 3.6 用户-登录- 拦截器
-
- 3.6.1 当前登录方式可能出现的问题
- 3.6.2 何为拦截器
- 3.6.3 使用
- 4 修改密码
-
- 4.1 用户-修改密码-持久层
-
- 4.1.1 需要实施的规划SQL语句
- 4.1.2 接口和抽象方法
- 4.1.3 配置SQL映射
- 4.1.4 单元测试
- 4.2 用户-修改密码-业务层
-
- 4.2.1 规划异常
- 4.2.2 接口和抽象方法
- 4.2.3 抽象方法的实现
- 4.2.4 单元测试
- 4.3 用户-修改密码-控制层
-
- 4.3.1 处理异常
- 4.3.2 设计请求
- 4.3.3 处理请求
- 4.4 用户-修改密码-前端页面
- 5 修改个人资料
-
- 5.1 用户-修改密码-持久层
-
- 5.1.1 需要实施的规划SQL语句
- 5.1.2 接口和抽象方法
- 5.1.3 配置SQL映射
- 5.1.4 单元测试
- 5.2 用户-修改密码-业务层
-
- 5.2.1 规划异常
- 5.2.2 接口和抽象方法
- 5.2.3 抽象方法的实现
- 5.2.4 单元测试
- 5.3 用户-个人数据-控制器
-
- 5.3.1 处理异常
- 5.3.2 设计请求
- 5.3.3 处理请求
- 5.4 用户-个人资料-前端页面
- 6 上传用户头像
-
- 6.1 用户-头像上传-持久层
-
- 6.1.1 需要实施的规划SQL语句
- 6.1.2 接口和抽象方法
- 6.1.3 配置SQL映射
- 6.1.4 单元测试
- 6.2 用户-头像上传-业务层
-
- 6.2.1 规划异常
- 6.2.2 接口和抽象方法
- 6.2.3 抽象方法的实现
- 6.2.4 单元测试
- 6.3 用户-头像上传-控制层
-
- 6.3.1 处理异常
- 6.3.2 设计请求
- 6.3.3 处理请求
- 6.4 用户-上传头像-前端页面
- 6.5 用户-上传头像-设置上传文件大小
- 6.6 用户-上传头像-前端页面BUG解决
-
- 6.6.1 上传后显示头像
- 6.6.2 登录后显示头像
- 6.6.3 显示最新头像
1 系统概述与环境建设
1.1 系统开发及运行环境
电脑商城系统开发所需的环境及相关软件进行介绍。
1.操作系统:Windows 10
2.Java开发包:JDK 8
3.项目管理工具:Maven 3.6.3
4.项目开发工具:IntelliJ IDEA 2020.3.2 x64
5.数据库:MariaDB-10.3.7-winx64
6.浏览器:Google Chrome
7.服务器架构:Spring Boot 2.4.7 + MyBatis 2.1.4 + AJAX
1.2 项目分析
- 。本项目中涉及的数据:用户、商品、商品类别、收藏、订单、购物车、收货地址。
- 。设计开发顺序的原则是:先开发基础、简单或熟悉的数据。以上需要处理的数据的开发流程是:用户-收货地址-商品类别-商品-收藏-购物车-订单。
- 。以用户数据为例,需要开发的功能有:登录、注册、修改密码、修改资料、上传头像。
- 。原则上,应先做基础功能,并遵循增查删改的顺序来开发。则用户相关功能的开发顺序应该是:注册-登录-修改密码-修改个人资料-上传头像。
- :持久层 - 业务层 - 控制器 - 前端页面。
- 持久层开发:依据前端页面的设置规划相关的 SQL 语句,以及进行配置;
- 业务层开发:核心功能控制、业务操作以及异常的处理;
- 控制层开发:接受请求、处理响应;
- 在实际开发中,应先创建该项目的数据库,当每次处理一种新的数据时,应先创建该数据在数据库中的数据表,然后在项目中创建该数据表对应的实体类。
1.3 搭建项目
1.3.1 项目结构
名称 | 路径 |
---|---|
项目名称 | com.sharm.store |
资源文件 | resources 文件夹下(static、templates) |
单元测试 | test.com.sharm.store |
1.3.2 创建Spring Initializer 项目
本质上 Spring Initializer 是一个 Web 应用程序,它提供了一个基本的项目结构,能够帮助开发者快速构建一个基础的 Spring Boot 项目。在创建 Spring Initializer 类型的项目时需在计算机连网的状态下进行创建。
1. 首先确保计算机上安装了 JDK、IDEA、MySQL 等开发需要使用的软件,并在IDEA中配置了Maven 3.6.3项目管理工具。
2.在IDEA欢迎界面,点击【New Project】按钮创建项目,左侧选择【Spring Initializr】选项进行Spring Boot项目快速构建。
3.给项目添加Spring Web、MyBatis Framework、MySQL Driver的依赖。点击【Next】按钮完成项目创建。
4.首次创建完Spring Initializr项目时,解析项目依赖需消耗一定时间(Resolving dependencies of store…)。
1.3.3 创建数据库
使用 navicat 创建数据库。第一种方法为图形界面法:
或者使用 状态栏 - 工具 - 命令行界面
打开 navicat 的命令行。
CREATE DATABASE store character SET utf8;
1.4 测试连接
1 启动 SpringBoot 主类,是否有对应的 Spring 图形输出
找到项目的入口类(被 @SpringBootApplication 注解修饰),然后运行启动类;启动过程如果控制台输出Spring图形则表示启动成功。
2 在单元测试类中测试数据库的连接是否可以正常地加载
@SpringBootTest
class StoreApplicationTests {
// 自动装配
@Autowired
private DataSource dataSource;
/** * 数据库连接池:Hikari 是 Spring 目前默认的数据库连接池。 * @throws SQLException */
@Test
void getConnection() throws SQLException {
System.out.println(dataSource.getConnection());
}
}
如果控制面板出现HikariProxyConnection@2049602706 wrapping com.mysql.cj.jdbc.ConnectionImpl@6326c5ec
,则数据库连接成功。
3 测试项目的静态资源是否可以正常的加载
把这些静态资源复制到项目目录结构的 static 目录下,同时在访问时,static 就表示根目录,不需要在 url上显示的写出来。
由于 IDEA 对于 JS 代码的兼容性较差,所以可能存在编写了 JS 代码但无法正常加载的情况,解决方法有:
- IDEA 的缓存清理;
- Maven 右侧工具栏 Lifecycle 的 clear 清理,然后 install 安装;
- IDEA 工具栏的 rebuild 重构;
- 重启 IDEA 或者操作系统。
1.5 问题与解决
1 启动项目时提示:“配置数据源失败:'url’属性未指定,无法配置内嵌的数据源”,且有如下的错误提示。
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
解决以上操作提示的方法:在 resources 文件夹下的 application.properties 文件中添加数据源的配置。
spring.datasource.url=jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
2 用户注册
2.1 用户-创建数据表
首先规划 SQL 语句,然后确定 Java 代码的编写;
1.使用use命令先选中store数据库。
USE store;
2.在store数据库中创建t_user用户数据表。
CREATE TABLE t_user (
uid INT AUTO_INCREMENT COMMENT '用户id',
username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
password CHAR(32) NOT NULL COMMENT '密码',
salt CHAR(36) COMMENT '盐值',
phone VARCHAR(20) COMMENT '电话号码',
email VARCHAR(30) COMMENT '电子邮箱',
gender INT COMMENT '性别:0-女,1-男',
avatar VARCHAR(50) COMMENT '头像',
is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
created_user VARCHAR(20) COMMENT '日志-创建人',
created_time DATETIME COMMENT '日志-创建时间',
modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
modified_time DATETIME COMMENT '日志-最后修改时间',
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 用户-创建实体类
数据库中的表要有与之相对应的实体类,这样才可以将表中的数据拿出来放到实体类中。
1.项目中许多实体类都会有日志相关的四个属性,所以在创建实体类之前,应先创建这些实体类的基类,将4个日志属性声明在基类中。在com.sharm.store.entity包下创建BaseEntity类,作为实体类的基类。
package com.sharm.store.entity;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
/** * 1. 只要是实体类就需要满足一定的约束,比如实现 Serializable 的接口 * 2. 传统的 SSM 需要在实体类上加上 @Component 修饰,而 SpringBoot 发现这都是多余的,SpringBoot 的约定大于配置,所以无需写上该注解; */
public class BaseEntity implements Serializable {
/** * 在敲对应的属性时,可以先将 sql 的字段复制过去,然后删除后面的修饰,加上前面的修饰符。 * 能 CV 就别手敲。 */
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;
// Generate: Getter and Setter、toString()
}
2.创建com.sharm.store.entity.User用户数据的实体类,继承自BaseEntity类,在类中声明与数据表中对应的属性。
package com.sharm.store.entity;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
public class User extends BaseEntity implements Serializable {
private Integer uid;
private String username;
private String password;
private String salt;
private String phone;
private String email;
private Integer gender;
private String avatar;
private Integer isDelete;
//Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}
2.3 用户-注册-持久层
2.3.1 规划需要执行的SQL语句
1.用户注册的本质是向用户表中插入数据,需要执行的SQL语句大致是:
INSERT INTO t_user (除了uid以外的字段列表) VALUES (匹配的值列表)
2.由于数据表中用户名字段被设计为UNIQUE,在执行插入数据之前,还应该检查该用户名是否已经被注册,因此需要有“根据用户名查询用户数据”的功能。需要执行的SQL语句大致是:
SELECT * FROM t_user WHERE username=?
2.3.2 设计接口和抽象方法
1.创建com.sharm.store.mapper.UserMapper接口,并在接口中添加抽象方法。
package com.sharm.store.mapper;
import com.sharm.store.entity.User;
/** 处理用户数据操作的持久层接口 */
public interface UserMapper {
/** * 插入用户数据:本来我们只需要插入几个字段,但多个字段毕竟麻烦,所以这里直接把 User 类型输入 * @param user 用户数据 * @return 新增数据的 ID */
Integer insert(User user);
/** * 根据用户名查询用户数据 * @param username 用户名 * @return 匹配的用户数据,如果没有匹配的数据,则返回null */
User findByUsername(String username);
}
2.由于这是项目中第一次创建持久层接口,所以需要我们告诉 SpringBoot 我们的 Mapper 接口的位置;
MyBatis与Spring整合后需要实现实体和数据表的映射关系。实现实体和数据表的映射关系可以在Mapper接口上添加@Mapper注解。但建议以后直接在SpringBoot启动类中加@MapperScan(“mapper包”) 注解,这样会比较方便,不需要对每个Mapper都添加@Mapper注解。
@SpringBootApplication
// MapperScan注解指定当前项目中的Mapper接口路径的位置,在项目启动时会自动加载所有的接口
@MapperScan("com.sharm.store.mapper")
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
}
2.3.3 配置SQL映射
1.在src/main/resources下创建mapper文件夹,并在该文件夹下创建UserMapper.xml映射文件,进行以上两个抽象方法的映射配置。
1)Mapper 的配置文件的框架在 Mybatis 官网就可以拿到;
2)这样写最大的好处就是将 SQL 语句和 Java 代码进行了分离,实现了解耦。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 属性:用于指定当前的映射文件和哪一个接口进行映射,需要表注指定接口的完整路径-->
<mapper namespace="com.sharm.store.mapper.UserMapper">
<resultMap id="UserEntityMap" type="com.sharm.store.entity.User">
<!-- 只需要将表中的字段和类的属性不相同的字段进行指定匹配,名称一致的字段可以省略不写 -->
<!-- 在定义映射规则时,主键不可以省略 -->
<id column="uid" property="uid"/>
<result column="is_delete" property="isDelete"/>
<result column="created_user" property="createdUser"/>
<result column="created_time" property="createdTime"/>
<result column="modified_user" property="modifiedUser"/>
<result column="modified_time" property="modifiedTime"/>
</resultMap>
<!-- id 属性:表示映射的接口中的方法,通过 namespace 和 id 确定一个方法 useGeneratedKeys 属性:表示开启某个字段的值递增 keyProperty 属性:确认该递增的字段 -->
<!-- 双击剪切,然后井号、大括号。最后部分不能有逗号,不能有分号。同时 value 中要修改为驼峰命名法;-->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
INSERT INTO
t_user (username, password, salt, phone, email, gender, avatar, is_delete, created_user, created_time, modified_user, modified_time)
VALUES
(#{username}, #{password}, #{salt}, #{phone}, #{email}, #{gender}, #{avatar}, #{isDelete}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime})
</insert>
<!-- select 在查询的时候,结果可以是一个对象,也可以是多个对象-->
<!-- 其中查询结果的字段如果和实体类的字段都相同,比如 phone = phone,则可以直接使用 resultType -->
<!-- 若查询结果的字段和实体类的字段不都相同,比如 created_time != createdTime,则使用 resultMap -->
<select id="findByUsername" resultMap="UserEntityMap">
SELECT
*
FROM
t_user
WHERE
username = #{username}
</select>
</mapper>
2.由于这是项目中第一次使用SQL映射,所以需要在application.properties中添加mybatis.mapper-locations属性的配置,以指定XML文件的位置。
mybatis.mapper-locations=classpath:mapper/*.xml
2.3.4 编写单元测试
完成后及时执行单元测试,检查以上开发的功能是否可正确运行。在src/test/java下创建com.sharm.store.mapper.UserMapperTests单元测试类。
package com.sharm.store.mapper;
import com.sharm.store.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
// 测试时必须导入这两个注解
// @SpringBootTest 表示标注当前的类是一个测试类,不会随项目一块打包
@SpringBootTest
// @RunWith 表示可以启动这个单元测试类。其中需要传递 SpringRunner.class 这个参数
@RunWith(SpringRunner.class)
public class UserMapperTests {
// 在测试类中声明持久层对象,通过自动装配来注入值
@Autowired
private UserMapper userMapper;
/** * 单元测试方法:可以单独运行,不需要启动整个项目,提高了代码的测试效率 * 条件: * 1. 该方法必须被 @Test 注解修饰 * 2. 返回值必须时 void * 3. 方法的参数列表不指定任何类型 * 4. 方法的访问修饰符必须是 public */
@Test
public void insert() {
User user = new User();
user.setUsername("user01");
user.setPassword("123456");
Integer rows = userMapper.insert(user);
System.out.println("rows=" + rows);
}
@Test
public void findByUsername() {
String username = "user01";
User result = userMapper.findByUsername(username);
System.out.println(result);
}
}
1 如果在自动装配 userMapper 对象时出现 “Could not autowire. No beans of ‘UserMapper’ type found”错,无法进行自动装配。解决方案是,将Autowiring for bean class选项下的Severity设置为Warning即可。
2.4 用户-注册-业务层
2.4.1 业务的定位
1.业务:一套完整的数据处理过程,通常表现为用户认为的一个功能,但是在开发时对应多项数据操作。在项目中,通过业务控制每个“功能”(例如注册、登录等)的处理流程和相关逻辑。
2.流程:先做什么,再做什么。例如:注册时,需要先判断用户名是否被占用,再决定是否完成注册。
3.逻辑:能干什么,不能干什么。例如:注册时,如果用户名被占用,则不允许注册;反之,则允许注册。
4.业务的主要作用是保障数据安全和数据的完整性、有效性。
2.4.2 规划异常
运用异常的方式来作为解决方案,这是自己之前没想到的。
异常的处理方式和处理原则:捕获处理(try…catch…finally),声明抛出(throw/throws)。如果当前方法适合处理,则捕获处理;如果当前方法不适合处理,则声明抛出。
1.为了便于统一管理自定义异常,应先创建com.sharm.store.service.ex.ServiceException自定义异常的基类异常,继承自RuntimeException类,并从父类生成子类的五个构造方法。
package com.sharm.store.service.ex;
/** 业务异常的基类 */
public class ServiceException extends RuntimeException {
// Override Methods...
}
2.当用户进行注册时,可能会因为用户名被占用而导致无法正常注册,此时需要抛出用户名被占用的异常,因此可以设计一个用户名重复的com.sharm.store.service.ex.UsernameDuplicateException
异常类,继承自ServiceException类,并从父类生成子类的五个构造方法。
package com.sharm.store.service.ex;
/** 用户名重复的异常 */
public class UsernameDuplicateException extends ServiceException {
// Override Methods...
}
3.在用户进行注册时,会执行数据库的INSERT操作,该操作也是有可能失败的。则创建com.sharm.store.service.ex.InsertException
异常类,继承自ServiceException类,并从父类生成子类的五个构造方法。
package com.sharm.store.service.ex;
/** 插入数据的异常 */
public class InsertException extends ServiceException {
// Override Methods...
}
4.所有的自定义异常,都应是RuntimeException的子孙类异常。项目中目前异常的继承结构是见下。
RuntimeException
-- ServiceException
-- UsernameDuplicateException
-- InsertException
2.4.3 接口与抽象方法
1.先创建com.sharm.store.service.IUserService业务层接口,并在接口中添加抽象方法。
package com.sharm.store.service;
import com.sharm.store.entity.User;
/** 处理用户数据的业务层接口 */
// 接口的命名规则为:I + 数据名 + 层名
public interface IUserService {
/** * 用户注册 * @param user 用户数据 */
void reg(User user);
}
2.创建业务层接口目的是为了解耦。关于业务层的抽象方法的设计原则。
1.仅以操作成功为前提来设计返回值类型,不考虑操作失败的情况;
2.方法中使用抛出异常的方式来表示操作失败;
2.方法名称可以自定义,通常与用户操作的功能相关;
3.方法的参数列表根据执行的具体业务功能来确定,需要哪些数据就设计哪些数据。通常情况下,参数需要足以调用持久层对应的相关功能,同时还要满足参数是客户端可以传递给控制器的.
2.4.4 实现抽象方法
1.UserServiceImpl类需要重写IUserService接口中的抽象方法,实现步骤大致是:
// 根据参数user对象获取注册的用户名
// 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据
// 判断查询结果是否不为null
// 是:表示用户名已被占用,则抛出UsernameDuplicateException异常
// 创建当前时间对象
// 补全数据:加密后的密码
// 补全数据:盐值
// 补全数据:isDelete(0)
// 补全数据:4项日志属性
// 表示用户名没有被占用,则允许注册
// 调用持久层Integer insert(User user)方法,执行注册并获取返回值(受影响的行数)
// 判断受影响的行数是否不为1
// 是:插入数据时出现某种错误,则抛出InsertException异常
2.实现抽象方法
package com.sharm.store.service.impl;
import com.sharm.store.entity.User;
import com.sharm.store.mapper.UserMapper;
import com.sharm.store.service.IUserService;
import com.sharm.store.service.ex.InsertException;
import com.sharm.store.service.ex.UsernameDuplicateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.Date;
import java.util.UUID;
// 通过使用 @Service 注解,将当前类的对象交给 Spring 来管理,自动进行对象的创建以及维护
@Service
/** 处理用户数据的业务层实现类 */
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public void reg(User user) {
// 根据参数user对象获取注册的用户名
String username = user.getUsername();
// 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:表示用户名已被占用,则抛出UsernameDuplicateException异常
throw new UsernameDuplicateException("尝试注册的用户名[" + username + "]已经被占用");
}
// 创建当前时间对象
Date now = new Date();
// 补全数据:加密后的密码
String salt = UUID.randomUUID().toString().toUpperCase();
// (盐值 + 密码 + 盐值 ) - md5 算法进行加密,连续加载三次
String md5Password = getMd5Password(user.getPassword(), salt);
user.setPassword(md5Password);
// 补全数据:盐值
user.setSalt(salt);
// 补全数据:isDelete(0)
user.setIsDelete(0);
// 补全数据:4项日志属性
// 在注册的时候,创建时间和修改时间肯定是一样的
user.setCreatedUser(username);
user.setCreatedTime(now);
user.setModifiedUser(username);
user.setModifiedTime(now);
// 表示用户名没有被占用,则允许注册
// 调用持久层Integer insert(User user)方法,执行注册并获取返回值(受影响的行数)
Integer rows = userMapper.i