JSD-2204-异常处理-SpringJDBC事务管理-Day14

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特性”、“事务的传播”、“事务的隔离”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值