1.处理异常
在登录时,可能出现:
- 用户名错误:
BadCredentialsException
- 密码错误:
BadCredentialsException
- 账号被禁用:
DisabledException
在访问时,可能出现:
- 无此权限:
AccessDeniedException
以上异常都可以由统一处理异常的机制进行处理,则先在ServiceCode
中添加对应的业务状态码:
/**
* 未授权的访问
*/
Integer ERR_UNAUTHORIZED = 40100;
/**
* 未授权的访问:账号禁用
*/
Integer ERR_UNAUTHORIZED_DISABLED = 40101;
/**
* 禁止访问,通常是已登录,但无权限
*/
Integer ERR_FORBIDDEN = 40300;
然后,在统一处理异常的类中,添加对相关异常的处理:
@ExceptionHandler
public JsonResult<Void> handleBadCredentialsException(BadCredentialsException e) {
String message = "登录失败,用户名或密码错误!";
log.debug("处理BadCredentialsException:{}", message);
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}
@ExceptionHandler
public JsonResult<Void> handleDisabledException(DisabledException e) {
String message = "登录失败,此账号已禁用!";
log.debug("处理DisabledException:{}", message);
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLED, message);
}
@ExceptionHandler
public JsonResult<Void> handleAccessDeniedException(AccessDeniedException e) {
String message = "访问失败,当前登录的账号无此权限!";
log.debug("处理AccessDeniedException:{}", message);
return JsonResult.fail(ServiceCode.ERR_FORBIDDEN, message);
}
另外,在解析JWT的过程中,也可能出现异常,由于解析JWT是在过滤器中进行的,如果出现异常,不会被统一处理异常的机制获取得到(因为过滤器执行的时间点太早),所以,只能在过滤器中自行处理异常,例如:
// 尝试解析JWT
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (MalformedJwtException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (SignatureException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (ExpiredJwtException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_EXPIRED, "登录信息已过期,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (Throwable e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
}
1.1添加管理员时分配角色
目前,当执行添加管理员时,并没有处理管理员的“角色”,以至于这些新添加的管理员没有权限!所以,添加管理员时,必须为管理员分配角色!
需要执行的任务:
- 修改
AdminAddNewDTO
,增加Long[]
属性,用于表示客户端选择的若干种角色的id,使得Controller能接收客户端在“添加管理员”时选择的若干种角色 - 在Service中,需要调用Mapper实现“向管理员与角色的关联表中批量插入数据”
- 在Mapper层,需要实现“向管理员与角色的关联表中批量插入数据”
首先,实现Mapper层,需要执行的SQL语句大致是:
insert into ams_admin_role (admin_id, role_id) values (?,?), (?,?) ... (?,?);
在根包下创建mapper.AdminRoleMapper
接口(如果不存在,则创建,如果已存在,不需要重复创建),并在接口中添加抽象方法:
int insertBatch(AdminRole[] adminRoleList);
在AdminRoleMapper.xml
中配置以上抽象方法映射的SQL语句:
<!-- int insertBatch(AdminRole[] adminRoleList); -->
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
INSERT INTO ams_admin_role
(admin_id, role_id, gmt_create, gmt_modified)
VALUES
<foreach collection="array" item="adminRole" separator=",">
(#{adminRole.adminId}, #{adminRole.roleId},
#{adminRole.gmtCreate}, #{adminRole.gmtModified})
</foreach>
</insert>
完成后,在AdminRoleMapperTests
中编写并执行测试:
@Test
public void testInsertBatch() {
Long adminId = 5L;
Long[] roleIds = {2L, 3L, 4L};
LocalDateTime now = LocalDateTime.now();
AdminRole[] adminRoleList = new AdminRole[roleIds.length];
for (int i = 0; i < roleIds.length; i++) {
AdminRole adminRole = new AdminRole();
adminRole.setAdminId(adminId);
adminRole.setRoleId(roleIds[i]);
adminRole.setGmtCreate(now);
adminRole.setGmtModified(now);
adminRoleList[i] = adminRole;
}
int rows = mapper.insertBatch(adminRoleList);
log.debug("批量插入管理员与角色的关联数据成功,受影响的行数={}", rows);
}
测试通过后,在AdminAddNewDTO
类中补充private Long[] roleIds;
属性:
/**
* 管理员关联到的若干个角色的id
*/
@ApiModelProperty("管理员关联到的若干个角色的id")
private Long[] roleIds;
然后,在AdminServiceImpl
中,先自动装配AdminRoleMapper
对象:
@Autowired
private AdminRoleMapper adminRoleMapper;
并在“添加管理员”的业务的最后,补充向管理员与角色关联的表中插入数据:
// 向管理员与角色关联的表中插入数据
log.debug("准备向管理员与角色关联的表中插入数据");
Long adminId = admin.getId();
Long[] roleIds = adminAddNewDTO.getRoleIds();
LocalDateTime now = LocalDateTime.now();
AdminRole[] adminRoleList = new AdminRole[roleIds.length];
for (int i = 0; i < roleIds.length; i++) {
AdminRole adminRole = new AdminRole();
adminRole.setAdminId(adminId);
adminRole.setRoleId(roleIds[i]);
adminRole.setGmtCreate(now);
adminRole.setGmtModified(now);
adminRoleList[i] = adminRole;
}
rows = adminRoleMapper.insertBatch(adminRoleList);
// 判断受影响的行数是否小于1(可能插入多条数据,所以,大于或等于1的值均视为正确)
if (rows < 1) {
// 是:日志,抛出ServiceException
String message = "添加管理员失败,服务器忙,请稍后再次尝试![错误代码:2]";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_INSERT, message);
}
完成后,修改原有的AdminServiceTests
中添加管理员的测试方法,为测试数据补充Long[] roleIds
属性的值,再进行测试:
@Test
void testAddNew() {
AdminAddNewDTO adminAddNewDTO = new AdminAddNewDTO();
adminAddNewDTO.setUsername("test-admin-109");
adminAddNewDTO.setPassword("123456");
Long[] roleIds = {2L, 4L}; // 新增代码
adminAddNewDTO.setRoleIds(roleIds); // 新增代码
try {
service.addNew(adminAddNewDTO);
log.debug("添加管理员成功!");
} catch (ServiceException e) {
log.debug(e.getMessage());
}
}
2.基于Spring JDBC的事务管理
事务:Transaction,是数据库中可以保障多次写(增删改)操作要么全部成功,要么全部失败的机制。
在基于Spring JDBC的数据库编程(包括使用Mybatis框架实现数据库编程)中,在处理业务的方法上添加@Transactional
注解,即可保证此业务方法是事务性的。
关于事务的相关概念:
- 开启事务:Begin
- 提交事务:Commit
- 回滚事务:Rollback
在Spring JDBC的事务管理中,其实现大概是:
开启事务
try {
执行业务方法
提交事务
} catch (RuntimeException e) {
回滚事务
}
可以看到,当执行业务方法时,如果出现了RuntimeException
(含其子孙类异常),都会回滚事务,这是Spring JDBC事务管理的默认处理方式!
在使用@Transactional
注解时,可以通过配置rollbackFor
及相关属性,来指定回滚的异常类型,还可以通过配置noRollbackFor
及相关属性,来指定不执行回滚的异常类型。
关于@Transactional
注解,可以添加在:
- 接口上
- 接口的实现类上
- 接口的抽象方法上
- 实现类的实现方法上
如果将此注解添加在“接口”或“接口的实现类”上,则表示对应的所有方法都是事务性的!
如果“接口”或“接口的实现类”上添加了此注解,并配置了注解的某属性,同时,“接口的抽象方法”或“实现类的实现方法上”也添加了此注解,也配置了注解的同样的属性,却是不同的值,则以方法上的配置值为准!
通常,推荐将注解添加在接口上,或接口中的抽象方法上!
本质上,Spring JDBC是通过接口代理模式来实现的事务管理,如果将@Transactional
注解添加在业务实现类中的自定义方法上(未在接口中声明的方法),会是错误的!
通常,如果某个业务涉及2次或以上次数的写(增删改)操作,就必须使其是事务性的!另外,如果某个业务中涉及多次查询,使用@Transactional
可以使得这些查询共用同一个数据库连接对象,可提高查询效率。
另外:建议学习“事务的ACID特性”、“事务的传播”、“事务的隔离”。