1. 开发流程
正常的项目开发流程大致是:
- 先整理出当前项目涉及的数据的类型
- 例如:电商类包含用户、商品、购物车、订单等
- 再列举各种数据类型涉及的数据操作
- 例如:用户类型涉及注册、登录等
- 再挑选相对简单的数据类型先处理
- 简单的易于实现,且可以积累经验
- 在各数据类型涉及的数据操作中,大致遵循增、查、删、改的开发顺序
- 只有先增,还可能查、删、改
- 只有查了以后,才能明确有哪些数据,才便于实现删、改
- 删和改相比,删一般更加简单,所以先开发删,再开发改
- 在开发具体的数据操作时,应该大致遵循持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序
2. 管理员登录-持久层
2.1. 创建或配置
如果是整个项目第1次开发持久层,在Spring Boot项目中,需要配置:
-
使用
@MapperScan
配置接口所在的根包 -
在配置文件中通过
mybatis.mapper-locations
配置XML文件的位置
如果第1次处理某种类型数据的持久层访问,需要: -
创建接口
-
创建XML文件
本次需要开发的“管理员登录”并不需要再做以上操作
2.2. 规划需要执行的SQL语句
需要执行的SQL语句大致是:
select * from ams_admin where username=?
由于在ams_admin
表中有大量字段,同时,不允许使用星号表示字段列表,则以上SQL语句应该细化为:
select id, username, password, nickname, avatar, is_enable from ams_admin where username=?
提示:理论上,还应该查出login_count
,当登录成功后,还应该更新login_count
、gmt_last_login
等数据,此次暂不考虑。
2.3. 在接口中添加抽象方法(含创建必要的VO类)
提示:所有的查询结果,都应该使用VO类,而不要使用实体类,根据阿里的开发规范,每张数据表中都应该有id
、gmt_create
、gmt_modified
这3个字段,而gmt_create
、gmt_modified
这2个字段都是用于特殊情况下排查问题的,一般情况下均不会使用,所以,如果使用实体类,必然存在多余的属性,同时,由于不使用星号作为字段列表,则一般也不会查询这2个字段的值,会导致实体类对象中永远至少存在2个属性为null
。
根据以上提示,以前已经写好的getByUsername()
是不规范的,应该调整已存在此方法,本次并不需要添加新的抽象方法。
则先创建cn.tedu.boot.demo.pojo.vo.AdminSimpleVO
类,添加此次查询时需要的属性:
package cn.tedu.boot.demo.pojo.vo;
@Data
public class AdminSimpleVO implements Serializable {
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private Integer isEnable;
}
然后,在AdminMapper
接口文件中,将原有的Admin getByUsername(String username);
改为:
AdminSimpleVO getByUsername(String username);
注意:一旦修改了原有代码,则调用了原方法的代码都会出现错误,包括:
- 测试
- 业务逻辑层的实现类
应该及时修改错误的代码,但是,由于此时还未完成SQL配置,所以,相关代码暂时并不能运行。
2.4. 在XML中配置SQL
在AdminMapper.xml
中,需要调整:
- 删除
<sql>
中不必查询的字段,注意:此处的字段列表最后不要有多余的逗号 - 修改
<resultMap>
节点的type
属性值 - 在
<resultMap>
节点下,删除不必要的配置
<select id="getByUsername" resultMap="BaseResultMap">
select
<include refid="BaseQueryFields" />
from
ams_admin
where
username=#{username}
</select>
<sql id="BaseQueryFields">
<if test="true">
id,
username,
password,
nickname,
avatar,
is_enable
</if>
</sql>
<resultMap id="BaseResultMap" type="cn.tedu.boot.demo.pojo.vo.AdminSimpleVO">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="nickname" property="nickname" />
<result column="avatar" property="avatar" />
<result column="is_enable" property="isEnable" />
</resultMap>
2.5. 编写并执行测试
此次并不需要编写新的测试,使用原有的测试即可!
注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。
3. 管理员登录-业务逻辑层
3.1. 创建
如果第1次处理某种类型数据的业务逻辑层访问,需要:
- 创建接口
- 创建类,实现接口,并在类上添加
@Service
注解
本次需要开发的“管理员登录”并不需要再做以上操作
3.2. 在接口中添加抽象方法(含创建必要的DTO类)
在设计抽象方法时,如果参数的数量超过1个,且多个参数具有相关性(是否都是客户端提交的,或是否都是控制器传递过来的等),就应该封装!
在处理登录时,需要客户端提交用户名和密码,则可以将用户名、密码封装起来:
package cn.tedu.boot.demo.pojo.dto;
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
在IAdminService
中添加抽象方法:
AdminSimpleVO login(AdminLoginDTO adminLoginDTO);
3.3. 在实现类中设计(打草稿)业务流程与业务逻辑(含创建必要的异常类)
此次业务执行过程中,可能会出现:
- 用户名不存在,导致无法登录
- 用户状态为【禁用】,导致无法登录
- 密码错误,导致无法登录
关于用户名不存在的问题,可以自行创建新的异常类,例如,在cn.tedu.boot.demo.ex
包下创建UserNotFoundException
类表示用户数据不存在的异常,继承自ServiceException
,且添加5款基于父类的构造方法:
package cn.tedu.boot.demo.ex;
public class UserNotFoundException extends ServiceException {
// 自动生成5个构造方法
}
再创建UserStateException
表示用户状态异常:
package cn.tedu.boot.demo.ex;
public class UserStateException extends ServiceException {
// 自动生成5个构造方法
}
再创建PasswordNotMatchException
表示密码错误异常:
package cn.tedu.boot.demo.ex;
public class PasswordNotMatchException extends ServiceException {
// 自动生成5个构造方法
}
登录过程大致是:
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 通过参数得到尝试登录的用户名
// 调用adminMapper.getByUsername()方法查询
// 判断查询结果是否为null
// 是:表示用户名不存在,则抛出UserNotFoundException异常
// 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】
// 【以下可视为:存在与用户名匹配的管理员数据】
// 判断查询结果中的isEnable属性值是否不为1
// 是:表示此用户状态是【禁用】的,则抛出UserStateException异常
// 【如果程序可以执行到此步,表示此用户状态是【启用】的】
// 从参数中取出此次登录时客户端提交的密码
// 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证
// 判断以上验证结果
// true:密码正确,视为登录成功
// -- 将查询结果中的password、isEnable设置为null,避免响应到客户端
// -- 返回查询结果
// false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常
}
3.4. 在实现类中实现业务
在AdminServiceImpl
中重写接口中新增的抽象方法:
@Override
public AdminSimpleVO login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("即将处理管理员登录的业务,尝试登录的管理员信息:{}", adminLoginDTO);
// 通过参数得到尝试登录的用户名
String username = adminLoginDTO.getUsername();
// 调用adminMapper.getByUsername()方法查询
AdminSimpleVO queryResult = adminMapper.getByUsername(username);
// 判断查询结果是否为null
if (queryResult == null) {
// 是:表示用户名不存在,则抛出UserNotFoundException异常
log.warn("登录失败,用户名不存在!");
throw new UserNotFoundException("登录失败,用户名不存在!");
}
// 【如果程序可以执行到此步,则可以确定未抛出异常,即查询结果不为null】
// 【以下可视为:存在与用户名匹配的管理员数据】
// 判断查询结果中的isEnable属性值是否不为1
if (queryResult.getIsEnable() != 1) {
// 是:表示此用户状态是【禁用】的,则抛出UserStateException异常
log.warn("登录失败,此账号已经被禁用!");
throw new UserNotFoundException("登录失败,此账号已经被禁用!");
}
// 【如果程序可以执行到此步,表示此用户状态是【启用】的】
// 从参数中取出此次登录时客户端提交的密码
String rawPassword = adminLoginDTO.getPassword();
// 调用PasswordEncoder对象的matches()方法,对客户端提交的密码和查询结果中的密码进行验证
boolean matchResult = passwordEncoder.matches(rawPassword, queryResult.getPassword());
// 判断以上验证结果
if (!matchResult) {
// false:密码错误,视为登录失败,则抛出PasswordNotMatchException异常
log.warn("登录失败,密码错误!");
throw new PasswordNotMatchException("登录失败,密码错误!");
}
// 密码正确,视为登录成功
// 将查询结果中的password、isEnable设置为null,避免响应到客户端
queryResult.setPassword(null);
queryResult.setIsEnable(null);
// 返回查询结果
log.debug("登录成功,即将返回:{}", queryResult);
return queryResult;
}
3.5. 编写并执行测试
在AdminServiceTests
中添加测试:
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() {
// 测试数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言不会抛出异常
assertDoesNotThrow(() -> {
// 执行测试
AdminSimpleVO adminSimpleVO = service.login(adminLoginDTO);
log.debug("登录成功:{}", adminSimpleVO);
// 断言测试结果
assertEquals(1L, adminSimpleVO.getId());
assertNull(adminSimpleVO.getPassword());
assertNull(adminSimpleVO.getIsEnable());
});
}
@Sql({"classpath:truncate.sql"})
@Test
public void testLoginFailBecauseUserNotFound() {
// 测试数据
String username = "admin001";
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出UserNotFoundException
assertThrows(UserNotFoundException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecauseUserDisabled() {
// 测试数据
String username = "admin005"; // 通过SQL脚本插入的此数据,is_enable为0
String password = "123456";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出UserStateException
assertThrows(UserStateException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginFailBecausePasswordNotMatch() {
// 测试数据
String username = "admin001";
String password = "000000000000000000";
AdminLoginDTO adminLoginDTO = new AdminLoginDTO();
adminLoginDTO.setUsername(username);
adminLoginDTO.setPassword(password);
// 断言会抛出PasswordNotMatchException
assertThrows(PasswordNotMatchException.class, () -> {
// 执行测试
service.login(adminLoginDTO);
});
}
4. 管理员登录-控制器层
4.1. 创建
如果是整个项目第1次开发控制器层,需要:
-
创建统一处理异常的类
- 添加
@RestControllerAdvice
- 添加
-
创建统一的响应结果类型及相关类型
- 例如:
JsonResult
及State
如果第1次处理某种类型数据的控制器层访问,需要:
- 例如:
-
创建控制器类
- 添加
@RestController
- 添加
@RequestMapping
本次需要开发的“管理员登录”并不需要再做以上操作
- 添加
4.2. 添加处理请求的方法,验证请求参数的基本有效性
在AdminLoginDTO
的各属性上添加验证基本有效性的注解,例如:
package cn.tedu.boot.demo.pojo.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
public class AdminLoginDTO implements Serializable {
@NotNull(message = "登录失败,请提交用户名!") // 新增
private String username;
@NotNull(message = "登录失败,请提交密码!") // 新增
private String password;
}
在AdminController
中添加处理请求的方法:
@RequestMapping("/login") // 暂时使用@RequestMapping,后续改成@PostMapping
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
4.3. 处理异常(按需)
先在State
中添加新创建的异常对应枚举:
public enum State {
OK(200),
ERR_USERNAME(201),
ERR_PASSWORD(202),
ERR_STATE(203), // 新增
ERR_BAD_REQUEST(400),
ERR_INSERT(500);
// ===== 原有其它代码 =====
}
在GlobalExceptionHandler
的handleServiceException()
方法中添加更多分支,针对各异常进行判断,并响应不同结果:
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
if (e instanceof UsernameDuplicateException) {
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else if (e instanceof UserNotFoundException) { // 从此行起,是新增的
return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
} else if (e instanceof UserStateException) {
return JsonResult.fail(State.ERR_STATE, e.getMessage());
} else if (e instanceof PasswordNotMatchException) {
return JsonResult.fail(State.ERR_PASSWORD, e.getMessage()); // 新增结束标记
} else {
return JsonResult.fail(State.ERR_INSERT, e.getMessage());
}
}
4.4. 测试
启动项目,暂时通过 http://localhost:8080/admins/login?username=admin001&password=123456 类似的URL测试访问。注意:在测试访问之前,必须保证数据表中的数据状态是符合预期的。