若依项目学习笔记08——事务&日志
1. 事务
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成;可以理解成一次性处理操作了较大,复杂度较高的数据,并且一次性做完,例如在管理系统中,要删除一个人员,我们既要删除其基本资料,也要删除该人员的相关信息,文章,邮箱等等
关系型数据库中要使用事务必须满足ACID[1];同时在MySQL中只有Innodb引擎才支持事务(右键表-选择改变表即可看到对应信息,如下),一般用于管理insert、update和delete操作
在MySQL中默认事务是自动提交(commit)的,因此要显式开启事务必须使用BEGIN、START、TRANSACTION或执行SET AUTOCOMMIT = 0 ,来禁止使用当前会话的commit
1.1 事务的使用
Spring Boot中,一般都会有 starter 或 web 依赖,这两个基础依赖中都已经包含了对于 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入 DataSourceTransactionManager 或 JpaTransactionManager 。 所以我们不需要任何额外配置就可以用 @Transactional
注解进行事务的使用
@Transactional
注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务
我们可以打开 @Transactional
接口
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";//value() 和 transactionManager() 都是用于指定使用哪个事务管理器
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;//传播行为
Isolation isolation() default Isolation.DEFAULT;//隔离级别
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;//超时时间
boolean readOnly() default false;//是否只读
Class<? extends Throwable>[] rollbackFor() default {};//指定异常回滚
String[] rollbackForClassName() default {};//指定异常名称
Class<? extends Throwable>[] noRollbackFor() default {};//指定什么异常不回滚
String[] noRollbackForClassName() default {};//指定什么异常不回滚的名称
}
下面用一个例子来讲解,例如用户新增需要插入用户表、用户与岗位关联表、用户与角色关联表,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作, 这样可以防止出现脏数据,就可以使用事务让它实现回退,我们只需在方法或类添加 @Transactional
注解即可;
//system.service.impl.SysUserServiceImpl的 `insertUser()` 新增保存用户信息方法
@Override
@Transactional
public int insertUser(SysUser user)
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}
Transactional注解的常用属性表
属性 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED |
isolation | 事务的隔离度,默认值采用 DEFAULT |
timeout | 事务的超时时间,默认值为-1,不超时。如果设置了超时时间(单位秒),那么如果超过该时间限制了但事务还没有完成,则自动回滚事务 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true |
rollbackFor | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 |
noRollbackFor | 抛出 no-rollback-for 指定的异常类型,不回滚事务。 |
.... |
TransactionDefinition传播行为的常量
常量 | 含义 |
---|---|
TransactionDefinition.PROPAGATION_REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值 |
TransactionDefinition.PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
TransactionDefinition.PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
TransactionDefinition.PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
TransactionDefinition.PROPAGATION_MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
TransactionDefinition.PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED |
要注意,这里有两个坑点,一个是发生异常没有回滚,一个是自己去捕获异常导致没有回滚,详细大家去看看官方文档
2. 日志
本小节将讲解登录日志和操作日志
2.1 登录日志
一款管理系统,一般都配备有登录日志,这样管理者可以方便查看每个登录用户的登录情况,下面我们来看看是如何实现的吧~
首先我们打开 framework.web.service.SysLoginService,我们看到login()
方法
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));//采用的是异步执行
throw new CaptchaExpireException();
}
首先获取并判断验证码是否为空,这里会调用 AsyncManager异步任务管理器 ,其中有一个 execute()
方法,会调用定时任务TimerTask;其中还有一个 ScheduledExecutorService异步操作任务调度线程池,这个在 ThreadPoolConfig 中有配置,用于执行定时任务;还有一个 记录登录信息方法recordLogininfor()
,分别带有用户名、状态(已定义)和信息(从i18n中获取);后面的密码错误或者其他错误都会对其进行记录
在 framework.security.handle.LogoutSuccessHandlerImpl 中,这里退出操作同样也会记录日志,那接下来我们来看看这个 recordLogininfor()
方法都做了什么
public class AsyncFactory
{
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
public static TimerTask recordLogininfor(final String username, final String status, final String message,
final Object... args)//记录登录信息
{
//获取用户代理,这里的UserAgent需要导入UserAgentUtils工具jar包的
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
/从而获得用户的ip地址,这个ip要放在线程外面
final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
return new TimerTask()
{
@Override
public void run()
{
//根据ip获得本地地址
String address = AddressUtils.getRealAddressByIP(ip);
StringBuilder s = new StringBuilder();
s.append(LogUtils.getBlock(ip));
s.append(address);
s.append(LogUtils.getBlock(username));
s.append(LogUtils.getBlock(status));
s.append(LogUtils.getBlock(message));
// 打印信息到日志
sys_user_logger.info(s.toString(), args);
// 获取客户端操作系统
String os = userAgent.getOperatingSystem().getName();
// 获取客户端浏览器
String browser = userAgent.getBrowser().getName();
// 封装对象
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(ip);
logininfor.setLoginLocation(address);
logininfor.setBrowser(browser);
logininfor.setOs(os);
logininfor.setMsg(message);
// 日志状态
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
{
logininfor.setStatus(Constants.SUCCESS);
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus(Constants.FAIL);
}
// 插入数据
SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
}
};
}
public static TimerTask recordOper(final SysOperLog operLog)//操作日志记录
{
return new TimerTask()
{
@Override
public void run()
{
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
}
};
}
}
2.2 操作日志
同样的,一般管理系统也是标配操作日志的,这样方便管理员去查看每个登录用户做了什么操作,但是如果每记录一次操作,就去调用一次记录方法,收集参数,会造成大量的代码重复,但是我们希望代码中只有业务操作,这是就需要注解
@Log
来帮忙解决问题了
在需要被记录日志的controller方法上添加@Log注解,使用方法如下
@Log(title = "用户管理", businessType = BusinessType.INSERT)
支持参数如下
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String | 空 | 操作模块 |
businessType | BusinessType | OTHER | 操作功能(OTHER其他 INSERT新增 UPDATE修改 DELETE删除 GRANT授权 EXPORT导出 IMPORT导入 FORCE强退 GENCODE生成代码 CLEAN清空数据) |
operatorType | OperatorType | MANAGE | 操作人类别(OTHER其他 MANAGE后台用户 MOBILE手机端用户) |
isSaveRequestData | boolean | true | 是否保存请求的参数 |
我们可以打开项目中的注解接口 Log |
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log//自定义操作日志记录注解
{
public String title() default "";//模块
public BusinessType businessType() default BusinessType.OTHER;//功能
public OperatorType operatorType() default OperatorType.MANAGE;//操作人类别
public boolean isSaveRequestData() default true;//是否保存请求的参数
}
关于自定义操作功能使用流程
- 在
BusinessType
中新增业务操作类型如
/**
* 测试
*/
TEST,
- 在
sys_dict_data
字典数据表中初始化操作业务类型(即sql文件中创建字典表的语句)
insert into sys_dict_data values(25, 10, '测试', '10', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');
- 在
Controller
中使用注解
@Log(title = "测试标题", businessType = BusinessType.TEST)
2.3 操作日志的实现
逻辑实现代码为 com.ruoyi.framework.aspectj.LogAspect ,我们进去可以看到,是一个切面类
// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.Log)")//表明,当注解是Log时进去
继续往下看,注解中指定了一个返回值 returning = "jsonResult"
,这里指的是 管理系统中-系统管理-日志管理-操作日志-点击右边的详情弹出的窗口中的返回参数
再往下看,有两个方法,分别处理请求和处理异常的,里面都是调用了 handleLog()
方法,所以我们来看看其实现,大家自行查看其代码,结合着操作日志详细弹出窗口来理解
- 获取注解
- 获取用户
- 数据库日志记录,如url,ip地址等
- 异常检测,如果出现异常,则状态设置为错误和拿到错误消息
- 设置方法名称、请求方式和参数
- 保存到数据库
查询操作详细记录可以登录系统(系统管理-操作日志)
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) ↩︎