电子商城记录-用户注册
目录
1 用户-创建数据表
创建数据库并创建数据表,在库中创建数据库
2 创建用户实体类
在创建用户实体类前,观察是否有通用基类分离,项目中许多实体类都会有日志相关的四个属性,所以在创建实体类之前,应先创建这些实体类的基类,将4个日志属性声明在基类中。
1.entity包下创建BaseEntity类,作为实体类的基类。因为这个基类的作用就是用于被其它实体类继承的,所以应声明为抽象类。
2.创建用户数据的实体类User,继承自BaseEntity类,并在类中声明与数据库对应的属性,创建
-
Getter and Setter、
-
Generate hashCode() and equals():比较值是否相等定义比较规则,hashcode地址输出、
-
toString():方便测试使用
等方法,
熟练可用lombok通过注解的形式自动生成构造器,提高开发效率,不过新手可以手动创建,提高源代码的可读性和完整性。
3.注:用户的实体类 :SpringBoot 约定大于配置,在spring boot中提供了一套默认配置,不需要手动去写xml配置文件,只有默认配置不能满足我们的需求时,才会去修改配置。如果没有配置,会取默认值,而所取的默认值就是约定,具体可以了解“约定大于配置“这个概念。
3 持久层
1 准备
1.测试类中编写并执行“获取数据库连接”的单元测试,以检查数据库连接的配置是否正确。
注意application文件的配置。
2.执行测试类中的contextLoads()测试方法,以检查测试环境是否正常。
2 规划需要执行的SQL语句
1.用户注册的本质是向用户表中插入数据。需要执行insert语句。
2.观察表中属性设计,是否有UNIQUE属性,在执行插入数据之前,还应该检查该用户名是否已经被注册,因此需要有“根据用户名查询用户数据”的功能。
3 接口与抽象方法
1.创建UserMapper接口,添加抽象方法。
-
UserMapper接口是处理用户数据操作的持久层接口
-
insert 抽象方法(插入用户数据):接收用户数据,返回受影响的行数(增,删,改都有受影响,将行数作为返回值,根据返回值判断是否执行成功)
插入用户数据字段多,声明麻烦,高耦合,所以设计为User对象
-
findByUsername 抽象方法(根据用户名来查询用户数据):传入用户名,如果找到对应用户则返回这个用户的数据,如果没有找到则返回null值
-
2.第一次创建持久层接口,应在StoreApplication启动类之前添加@MapperScan("mapper包位置")注解,以配置接口文件的位置。
-
@MapperScan注解:指定当前项目中的Mapper接口路径的位置,在项目启动时候会自动加载所有接口文件
注:MyBatis与Spring整合后需要实现实体和数据表的映射关系。实现实体和数据表的映射关系可以在Mapper接口上添加@Mapper注解。但建议以后直接在SpringBoot启动类中加@MapperScan("mapper包") 注解,这样会比较方便,不需要对每个Mapper都添加@Mapper注解。
4 配置SQL映射
1.在resources下创建mapper文件夹,并在该文件夹下创建UserMapper.xml映射文件。对UserMapper接口下的两个抽象方法进行映射配置。
-
namespace属性:用于指定当前映射文件和哪些接口进行映射,需要指定接口的文件路径,需要标注包的完整路径接口
<mapper namespace="UserMapper路径">
由于数据库与Java命名规则不同,驼峰命名法,需要将表的字段和和类的属性不一致的字段进行匹配指定,名称一致的字段可以省略不 写
-
自定义映射规则:resultMap标签来完成映射规则的定义
-
核心属性1:id属性:表示个i这个映射规则分配一个唯一的id值,对应的就是resultMap="id属性的值"属性的取值
-
核心属性2: type属性:一个类,表示的是数据库中的查询结果与java中哪个实体类进行结果集的映射
-
-
配合完成名称不一致的映射: column属性:表中的字段名称 property属性:表示类中的属性名称(User实体类属性名称)
注:表示映射的接口中方法的名称,直接在标签的内部来编写SQL语句.属性建议直接复制
2.项目中第一次使用SQL映射,需要在application.properties中添加mybatis.mapper-locations属性的配置,以指定XML文件的位置。不然会出现“ Invalid bound statement (not found):mapper路径”错误提示。
3.完成抽象方法映射后及时执行单元测试,检查所开发的功能是否可正确运行。创建UserMapperTests单元测试类,在测试类的声明之前添加@RunWith(SpringRunner.class)和@SpringBootTest注解,并在测试类中声明持久层对象,通过自动装配来注入值。
-
RunWith(): 表示启动这个单元测试类(单元注解声明)是一个测试启动器,可以加载SpringBoot测试注解。如果不写,单元测试类可能无法运行,需要传递一个参数,必须是SpringRunner的实例类型(.class)
注:在idea中@RunWith(SpringRunner.class)可以不加,在正常情况下测试类是需要@RunWith的,作用是告诉java你这个类通过用什么运行环境运行,在正常情况下测试类是需要@RunWith的,作用是告诉java你这个类通过用什么运行环境运行,在IDEA里去掉@RunWith仍然能跑是因为在IDEA里识别为一个JUNIT的运行环境,相当于就是一个自识别的RUNWITH环境配置。但在其他IDE里并没有。
来自:关于@RunWith(SpringRunner.class)的作用_赢啦啦的博客-CSDN博客_springrunner作用
4.自动装配userMapper时,报“Could not autowire. No beans of 'UserMapper' type found”错,无法进行自动装配。
解决方案:将setting下搜索inspections,在spring core下的code中找到Autowiring for bean class选项,将Severity设置为Warning即可。如果没有Autowiring for bean class选项,将incorrect injection point autowiring in spring bean components下的Severity设置为Warning即可,加注解可以解决该问题,但加注解相对麻烦 更改等级权限是全局写法。
5.编写两个测试方法,对完成的两个功能进行单元测试。
注:单元测试方法必须为public修饰,方法的返回值类型必须为void,方法不能有参数列表,并且方法被@Test注解修饰。
tips:设置单元测试方法优点独立运行,不用启动整个项目,可以做单元测试,提升了代码的测试效率
特点: 1.必须被Test注解修饰 2.返回值类型必须是void 3.方法的参数列表不能指定任何类型 4.方法的访问修饰符必须是public
满足以上4个特点就可独立运行。在单元测试类中出现方法必须是单元测试方法。
4 业务层

1 规划异常
业务执行产生问题,java角度属于异常
规划异常目的:将错误控制在范围内
如何规划:
1.RuntimeException异常,作为者异常的子类,然后再去定义具体的异常类型来继承这个异常。异常也有分等级,在业务层异常的基 类,ServiceException异常。这个异常继承RuntimeException异常。在有具体异常,继承ServiceException异常。异常机制建立
tips:直接抛java所提供的异常可以吗?结构上没问题,但这种异常过于笼统,不明确,插入过程中用户名占用,密码错误,宕机都属 于异常,RuntimeException异常,没有办法第一时间由开发者快速定位错误类型,效率降低。
public class ServiceException extends RuntimeException{
//定义子类中的构造方法,重写父接口5个构造方法
public ServiceException() {
super();
}
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
根据业务层不同功能,来详细定义具体的异常的类型,统一的去继承ServiceException
2.用户在注册的时候可能会产生用户名被占用的错误,抛出一个异常:UsernameDuplicatedException 异常
package com.cy.store.service;
public class UsernameDuplicatedException extends ServiceException{
public UsernameDuplicatedException() {
super();
}
public UsernameDuplicatedException(String message) {
super(message);
}
public UsernameDuplicatedException(String message, Throwable cause) {
super(message, cause);
}
public UsernameDuplicatedException(Throwable cause) {
super(cause);
}
protected UsernameDuplicatedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
3.正在执行数据插入操作,服务器,数据库宕机。处于正在插入的过程中所产生的异常。
InsertException异常
package com.cy.store.service;
public class InsertException extends ServiceException{
public InsertException() {
super();
}
public InsertException(String message) {
super(message);
}
public InsertException(String message, Throwable cause) {
super(message, cause);
}
public InsertException(Throwable cause) {
super(cause);
}
protected InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
2 设计接口和抽象方法
命名:以大写i “I”开头,跟上业务名称,业务层,在跟上service
1.在service包下创建一个IUserService接口,在接口中添加抽象方法
package com.cy.store.service.impl;
import com.cy.store.entity.User;
/**用户模块业务层接口*/
public interface IUserService {
//定义方法,完成注册,注册时业务层不需要返回值
/**
* 用户注册方法
* @param user 用户的数据对象
*/
void reg(User user);
}
2.创建一个实现类UserServiceImpl类,实现这个接口,并且实现抽象方法。创建业务层接口目的是为了解耦。
-
1.仅以操作成功为前提来设计返回值类型,不考虑操作失败的情况;
-
2.方法名称可以自定义,通常与用户操作的功能相关;
-
3.方法的参数列表根据执行的具体业务功能来确定,需要哪些数据就设计哪些数据。通常情况下,参数需要足以调用持久层对应的相关功能;同时还要满足参数是客户端可以传递给控制器的;
-
4.方法中使用抛出异常的方式来表示操作失败。
业务逻辑:异常的处理和捕获
3 实现接口方法

1.在service.impl.UserServiceImpl业务层下创建实现类,并实现IUserService接口。在类之前添加@Service注解,并在类中添加持久层UserMapper对象。
package com.cy.store.service.impl;
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
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管理,自动创建对象以及对象的维护
// 不加,报错 UnsatisfiedDependencyException 依赖异常 NoSuchBeanDefinitionException 没有找到接口的实现类 并没有交给spring管理
@Service
public class UserServiceImpl implements IUserService {
public void reg(User user) {
}
}
2.reg()具体实现
package com.cy.store.service.impl;
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
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管理,自动创建对象以及对象的维护
// 不加,报错 UnsatisfiedDependencyException 依赖异常 NoSuchBeanDefinitionException 没有找到接口的实现类 并没有交给spring管理
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
/***
* 调用mapper层方法,把user对象传递下去
* 业务逻辑:异常的处理和捕获
* @param user 用户的数据对象
*/
@Override
public void reg(User user) {
//通过user参数来获取传递过来的username获取注册的用户名
String username = user.getUsername();
//调用持久层的User findByUsername(username)判断用户是否被注册过
//判断有无拿到,使用结构集接收
User result = userMapper.findByUsername(username);
//判断结果集是否不为null则抛出用户名被占用的异常,为null执行注册逻辑
if(result != null){
//抛出异常
throw new UsernameDuplicatedException("用户名被占用");
}
// 密码加密处理实现(user交给insert之前):MD5算法的形式
// (串 + password + 串) ------ 再将整体由MD5加密,连续加载3次
//盐值 + password + 盐值 ----盐值就是一个随机的字符串
String oldPassword = user.getPassword();
//获取盐值(随机生成一个盐值)
String salt = UUID.randomUUID().toString().toUpperCase();
// 补全数据:盐值的记录
user.setSalt(salt);
//将密码和盐值作为一个整体进行加密处理,忽略原有密码的强度,提升数据的安全性
String md5Password = getMD5Password(oldPassword, salt);
//将加密密码重新补全设置到user对象中
user.setPassword(md5Password);
// user数据补全操作:is_delete设置为0
user.setIsDelete(0);
// user数据补全操作:4个日志字段信息
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
// 执行注册业务逻辑功能的实现(rows == 1)插入成功
Integer rows = userMapper.insert(user);
if(rows != 1){
throw new InsertException("在用户注册过程中产生了未知异常");
}
}
}
4 设置MD5加密密码
getMD5Password()方法具体实现
/** 定义一个MD5算法的加密处理*/
private String getMD5Password(String password,String salt){
// MD5加密算法方法的调用(进行3次加密)
for (int i = 0; i <3 ; i++) {
password = DigestUtils.md5DigestAsHex((salt+password+salt).getBytes()).toUpperCase();
}
// 返回加密之后的密码
return password;
}
//密码加密处理实现(user交给insert之前):MD5算法的形式
// (串 + password + 串) ------ 再将整体由MD5加密,连续加载3次
//盐值 + password + 盐值 ----盐值就是一个随机的字符串
String oldPassword = user.getPassword();
//获取盐值(随机生成一个盐值)
String salt = UUID.randomUUID().toString().toUpperCase();
// 补全数据:盐值的记录
user.setSalt(salt);
//将密码和盐值作为一个整体进行加密处理,忽略原有密码的强度,提升数据的安全性
String md5Password = getMD5Password(oldPassword, salt);
//将加密密码重新补全设置到user对象中
user.setPassword(md5Password);
5 测试检查
在单元测试包下创建一个UserServiceTest类,在这个类中添加单元测试功能,执行操作,检测数据库。
package com.cy.store.service;
import com.cy.store.entity.User;
import com.cy.store.mapper.UserMapper;
import com.cy.store.service.ex.ServiceException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
//@SpringBootTest:表示标注当前的类是一个测试类,不会随同项目一块打包发送
@SpringBootTest
//RunWith():表示启动这个单元测试类(单元注解声明)(如果不写,单元测试类无法运行),需要传递一个参数,必须是SpringRunner的实例类型(.class)
//@RunWith(SpringRunner.class)
public class UserServiceTests {
//报错“Could not autowire. No beans of 'UserMapper' type found”
/**
* 原因:idea有检测功能,接口是不能够直接创建Bean的(动态代理技术解决),认为语法不合理
* 本质项目启动,接口能运行的原因:mybatis创建接口动态代理实现类,完成对象的创建
*/
@Autowired
private IUserService userService;
/**
* 单元测试方法特点:满足以下4个特点就可以单独独立运行,不用启动整个项目,可以做单元测试,提升了代码的测试效率
* 1.必须被Test注解修饰
* 2.返回值类型必须是void
* 3.方法的参数列表不能指定任何类型
* 4.方法的访问修饰符必须是public
*
* 在单元测试类中出现方法必须是单元测试方法
*/
@Test
public void reg(){
try {
User user = new User();
user.setUsername("john02");
user.setPassword("123");
userService.reg(user);// Integer rows观察影响的行数
System.out.println("ok");
} catch (ServiceException e) {
// 获取类的对象,在获取类的名称
System.out.println(e.getClass().getSimpleName());
// 获取异常的具体描述信息
System.out.println(e.getMessage());
}
}
}
5 控制层
1 创建响应
-
状态码
-
状态描述信息
-
响应数据
所有控制层对应这三方面操作,减少开发,将这部分功能封装再一个类中,将这个类作为方法返回值,返回给前端浏览器。
public class JsonResult<E> implements Serializable {
/** 状态码 */
private Integer state;
/** 描述信息 */
private String message;
/** 数据,对应数据,数据类型不确定,用泛型表示 */
private E data;
}
向后端服务器发送请求,针对请求,将用户数据插入后端,首先是设计请求
2 设计请求
依据当前的业务功能模块进行请求的设计
请求路径: /user/reg 请求参数: User user 请求类型: POST 响应结果: JsonResult<void>
3 处理请求
1.创建一个控制层对应的类UserController类。依赖于业务层的接口,添加上相应注解,接收相关请求。
package com.cy.store.controller;
import com.cy.store.entity.User;
import com.cy.store.service.IUserService;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.UsernameDuplicatedException;
import com.cy.store.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
//@Controller
@RestController // 等效 @Controller + @ResponseBody
@RequestMapping("user")// 什么样的请求需要被拦截到子类当中
public class UserController {
@Autowired
private IUserService userService;
//方法接收什么样的请求
@RequestMapping("reg")
//相关响应结果
//@ResponseBody // 表示此方法的响应结果以json格式进行数据的响应给到前端
public JsonResult<Void> reg(User user){
// 创建响应结果对象,即JsonResult对象
JsonResult<Void> result = new JsonResult<>();
try {
userService.reg(user);
result.setState(200);
result.setMessage("用户注册成功");
} catch (UsernameDuplicatedException e) {
result.setState(4000);
result.setMessage("用户名被占用");
}catch (InsertException e) {
result.setState(5000);
result.setMessage("注册时产生未知的异常");
}
return result;
}
}
业务逻辑相对麻烦,凡是业务层抛出异常都在控制层捕获,如果其他业务模块也抛用户名占用或插入异常,就会重复编写下面代码
catch (UsernameDuplicatedException e) {
result.setState(4000);
result.setMessage("用户名被占用");
}catch (InsertException e) {
result.setState(5000);
result.setMessage("注册时产生未知的异常");
}
4 控制层优化设计
1.在控制层抽离一个父类,再这个父类中统一的处理关于异常的相关操作。编写一个BaseController,在其中定义表示响应成功的状态码及统一处理异常的方法。
@ExceptionHandler注解用于统一处理方法抛出的异常。当我们使用这个注解时,需要定义一个异常的处理方法,再给这个方法加上@ExceptionHandler注解,这个方法就会处理类中其他方法(被@RequestMapping注解)抛出的异常。@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常。
package com.cy.store.controller;
import com.cy.store.service.ex.InsertException;
import com.cy.store.service.ex.ServiceException;
import com.cy.store.service.ex.UsernameDuplicateException;
import com.cy.store.util.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
/** 控制器类的基类 */
public class BaseController {
/** 操作成功的状态码 */
public static final int OK = 200;
/** @ExceptionHandler用于统一处理方法抛出的异常 */
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleException(Throwable e) {
JsonResult<Void> result = new JsonResult<Void>(e);
if (e instanceof UsernameDuplicateException) {
result.setState(4000);
} else if (e instanceof InsertException) {
result.setState(5000);
}
return result;
}
}
2.最后简化UserController控制器类中的用户注册reg()方法的代码。
/** 处理用户相关请求的控制器类 */
@RestController
@RequestMapping("users")
public class UserController extends BaseController {
@Autowired
private IUserService userService;
@RequestMapping("reg")
public JsonResult<Void> reg(User user) {
// 调用业务对象执行注册
userService.reg(user);
// 返回
return new JsonResult<Void>(OK);
}
}
3.完成后启动项目,打开浏览器访问http://localhost:8080/users/reg?username=controller&password=123456请求进行测试。
6 前端页面
1.编写发送请求
在register页面中编写发送请求的方法 点击事件完成(/users/reg),选中对应的按钮($(选择器)),再去添加点击的事件,$.ajax()函数发送异步请求。
2.JQUery 封装了一个函数 ajax
JQUery 封装了一个函数,称之为 $.ajax()函数,通过对象调用ajax()函数,可以异步加载相关的请求(异步:就是页面不会改变,但是数据会不断加载),依靠的是Javscript提供的一个对象XHR (XmlHttpResponse),封装了这个对象。
3.ajax()使用方式,语法结构
-
ajax()使用方式,需要传递一个方法题作为方法的参数来使用,一对大括号称之为方法体。要求传递一系列参数。
-
ajax接收多个参数,参数与参数之间要求使用英文下的逗号","进行分割;
-
每一组参数之间使用":"进行分割;
-
参数的组成部分一个是参数的名称(不能随意的定义),另一个是参数的值,要求使用字符串来标识 “ ”。
-
参数的声明顺序没有要求。
语法结构:
/**
$.ajax(fun());//不能达到复用
function fun(){
//TODO
}
*/
正确:
$.ajax({
url: "",
type: "",
data: "",
dataType: "",
success:function(){
},
error:function(){
}
})
4.ajax()函数参数的含义:
| 参数 | 功能描述 |
|---|---|
| url | 表示请求的地址(url地址),不能博阿寒参数列表部分的内容。例如:“url:”localhost/users/reg“ |
| type | 请求的类型(GET和POST)。例如:type:”POST“ |
| data | 向指定的请求url地址提交的数据。例如:data:”username=tom&pwd=123“ |
| dataType | 提交的数据的类型。数据的类型一般指定为json类型。例如:dataType:”json“ |
| success | 当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上 |
| error | 当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上 |
5.js代码可以独立存放在一个后缀为js的文件里或者声明在一个script标签中。
<script type="text/javascript">
//1.监听注册按钮是否被点击,借助选择器选中按钮,id选择器以#开头
//click()监听是否被点击,如果被点击,可以执行一个方法,
$("#btn-reg").click(function () {
//动态获取表单中控制的数据
//let username = $("#username").val();
//let pwd = $("#password").val();
console.log($("#form-reg").serialize());//把表单所有的控件输出
//2.发送ajax()的异步请求来完成用户的注册功能,注意记得加逗号分割
$.ajax({
url: "/users/reg",
type: "POST",
// username=Tom&password=123
data: $("#form-reg").serialize(),//先拿到表单,serialize()检测表单有哪些控件
//复杂不推荐:data: "username="+username + "&password="+pwd,
dataType: "JSON",
success:function(json){//success中的state与error中的status不同,error是官网提供
if (json.state == 200){
alert("注册成功");
}else{
alert("注册失败");
}
},
error:function(){
alert("注册时产生未知的错误!" + xhr.status);
}
})
});
</script>
6.js代码无法正常被服务器解析执行,体现在点击页面中的按钮没有任何响应。
解决方案:
-
在项目的maven下clear清理项目-install重新部署
-
在项目的file选项下-cash清理缓存
-
重新构建项目:build选项下-rebuild选项
-
重启idea
-
重启电脑
注意:检查id选择器以#开头