day69(SpringBoot5:处理登录:开发流程、Knife4j)
day69(SpringBoot5:处理登录:开发流程、Knife4j)
1.处理登录
1.开发流程
1.先整理出当前项目涉及的数据的类型
- 例如:电商类包含用户、商品、购物车、订单等
2.再列举各种数据类型涉及的数据操作
- 例如:用户类型涉及注册、登陆等
3.再挑选相对简单的数据类型先处理
- 简单的易于实现,且可以积累经验
4.在各类数据涉及到的数据操作中,大致遵循增、查、删、改的开发顺序
- 只有先增,还可能查、删、改
- 只有查了以后,才能明确有哪些数据,才便于实现删、改
- 删和改相比,删一般更加简单,所以先开发删,再开发改
5.在开发具体的数据操作时,应该大致遵循持久层 >> 业务逻辑层 >> 控制器层 >> 前端页面的开发顺序
2.管理员登陆-持久层
1.创建或配置(第一次开发)
- 配置(Spring Boot项目)
- 使用@MapperScan配置接口所在的根包
- 在配置文件中通过mybatis.mapper-locations配置XML文件的位置
- 处理某种类型数据的持久层访问,需要:
- 创建接口
- 创建XML文件
2.规划需要执行的SQL语句
-
SQL大致语句:
select * from ams_admin where username=?
-
由于不允许使用*号,因此细分为:
select id,username,password,nickname,avatar,is_enable from ams_admin username=?
-
提示:理论上,还应该查出
login_count
,当登录成功后,还应该更新login_count
、gmt_last_login
等数据,此次暂不考虑。
3.在接口中添加抽象方法(含创建必要的VO类)
1.所有查询结果都应该使用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; }
2.修改AdminMapper接口文件
- 将Admin getByUsername(String username)改为
AdminSimpleVO getByUsername(String username);
- 因为修改了源代码,所以调用了原方法的代码都会出现错误,包括:
- 测试
- 业务逻辑层的实现类
- 因及时修改,但是由于未完成SQL配置,相关代码暂时不能运行
4.在XML中的配置SQL
1.调整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>
5.编写并执行测试
此次并不需要编写新的测试,使用原有的测试即可!
注意:由于本次是修改了原“增加管理员”就已经使用的功能,应该检查原功能是否可以正常运行。
3.管理员登录-业务逻辑层
1.创建
如果第1次处理某种类型数据的业务逻辑层访问,需要:
- 创建接口
- 创建类,实现接口,并在类上添加
@Service
注解
本次需要开发的“管理员登录”并不需要再做以上操作
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.在实现类中设计(打草稿)业务流程与业务逻辑(含创建必要的异常类)
此次业务执行过程中,可能会出现:
- 用户名不存在,导致无法登录
- 用户状态为【禁用】,导致无法登录
- 密码错误,导致无法登录
关于用户名不存在的问题,可以自行创建新的异常类,例如,在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异常
}
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;
}
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.管理员登录-控制器层
1.创建
如果是整个项目第1次开发控制器层,需要:
- 创建统一处理异常的类
- 添加
@RestControllerAdvice
- 添加
- 创建统一的响应结果类型及相关类型
- 例如:
JsonResult
及State
- 例如:
如果第1次处理某种类型数据的控制器层访问,需要:
- 创建控制器类
- 添加
@RestController
- 添加
@RequestMapping
- 添加
本次需要开发的“管理员登录”并不需要再做以上操作
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")
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = iAdminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
3.处理异常(按需)
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.测试
启动项目,暂时通过 http://localhost:8080/admins/login?username=admin001&password=123456 类似的URL测试访问。注意:在测试访问之前,必须保证数据表中的数据状态是符合预期的。
5.管理员登录-前端页面
使用vue-project-005项目
6.控制层测试
关于控制器层,也可以写测试方式进行测试,在Spring Boot项目中,可以使用MockMvc
进行模拟测试,例如:
package cn.tedu.boot.demo.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@SpringBootTest
@AutoConfigureMockMvc // 自动配置MockMvc
public class AdminControllerTests {
@Autowired
MockMvc mockMvc; // Mock:模拟
@Sql({"classpath:truncate.sql", "classpath:insert_data.sql"})
@Test
public void testLoginSuccessfully() throws Exception {
// 准备测试数据,不需要封装
String username = "admin001";
String password = "123456";
// 请求路径,不需要写协议、服务器主机和端口号
String url = "/admins/login";
// 执行测试
// 以下代码相对比较固定
mockMvc.perform( // 执行发出请求
MockMvcRequestBuilders.post(url) // 根据请求方式决定调用的方法
.contentType(MediaType.APPLICATION_FORM_URLENCODED) // 请求数据的文档类型,例如:application/json; charset=utf-8
.param("username", username) // 请求参数,有多个时,多次调用param()方法
.param("password", password)
.accept(MediaType.APPLICATION_JSON)) // 接收的响应结果的文档类型,注意:perform()方法到此结束
.andExpect( // 预判结果,类似断言
MockMvcResultMatchers
.jsonPath("state") // 预判响应的JSON结果中将有名为state的属性
.value(200)) // 预判响应的JSON结果中名为state的属性的值,注意:andExpect()方法到此结束
.andDo( // 需要执行某任务
MockMvcResultHandlers.print()); // 打印日志
}
}
执行以上测试时,并不需要启动当前项目即可测试。
在执行以上测试时,响应的JSON中如果包含中文,可能会出现乱码,需要在配置文件(application.properties
或application.yml
这类文件)中添加配置。
在.properties
文件中:
server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8
在.yml
文件中:
server:
servlet:
encoding:
force: true
charset: utf-8
2.Knife4j
Knife4j是一款可以提供在线API文档的框架,是基于Swagger框架实现的。
在Spring Boot项目中,使用Knife4j需要添加依赖knife4j-spring-boot-starter
:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
然后,需要添加配置,则在boot-demo
项目的cn.tedu.boot.demo.config
包下创建Knife4jConfig
类:
package cn.tedu.boot.demo.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "cn.tedu.boot.demo.controller";
/**
* 分组名称
*/
private String groupName = "product";
/**
* 主机名
*/
private String host = "http://java.tedu.cn";
/**
* 标题
*/
private String title = "酷鲨商城在线API文档--商品管理";
/**
* 简介
*/
private String description = "酷鲨商城在线API文档--商品管理";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "Java教学研发部";
/**
* 联系网址
*/
private String contactUrl = "http://java.tedu.cn";
/**
* 联系邮箱
*/
private String contactEmail = "java@tedu.cn";
/**
* 版本号
*/
private String version = "1.0.0";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
String groupName = "1.0.0";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
注意:必须修改以上配置中的包名,保证是当前项目中控制器类所在的包!其它各项均可不修改,以上配置代码可以从Knife4j的官网找到!
最后,还需要在配置文件中开启Knife4j的增强模式:
# Knife4j配置
knife4j:
# 是否开启增强模式
enable: true
完成后,启动项目,在浏览器中访问 http://localhost:8080/doc.html 即可查看当前项目的API文档。
在控制器类上添加@Api
注解,并配置tags
属性,可以指定模块名称,例如:
@Api(tags = "管理员管理模块") // 新增
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
// ===== 原有其它代码 =====
}
在处理请求的方法上添加@ApiOperation
注解可以配置业务名称,例如:
@ApiOperation("管理员登录") // 新增
@PostMapping("/login")
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
当需要指定各业务在API文档中的显示顺序时,可以在处理请求的方法上添加@ApiOperationSupport
注解,配置此注解的order
属性,最终在显示API文档时,会根据order
属性值升序排列,例如:
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 900) // 新增
@PostMapping("/login")
public JsonResult<AdminSimpleVO> login(@Validated AdminLoginDTO adminLoginDTO) {
AdminSimpleVO adminSimpleVO = adminService.login(adminLoginDTO);
return JsonResult.ok(adminSimpleVO);
}
通常,建议以上配置的order
值至少是2位的数字,并且有预留位置,例如1019之间的都是增加数据的业务,2029之间的都是删除数据的业务,3039之间都是修改数据的业务,4049之间都是查询数据的业务。