若依项目学习笔记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;//是否保存请求的参数
}

关于自定义操作功能使用流程

  1. BusinessType 中新增业务操作类型如
/**
 * 测试
 */
TEST,
  1. 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', '测试操作');
  1. Controller 中使用注解
@Log(title = "测试标题", businessType = BusinessType.TEST)

2.3 操作日志的实现

逻辑实现代码为 com.ruoyi.framework.aspectj.LogAspect ,我们进去可以看到,是一个切面类

// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.Log)")//表明,当注解是Log时进去

继续往下看,注解中指定了一个返回值 returning = "jsonResult",这里指的是 管理系统中-系统管理-日志管理-操作日志-点击右边的详情弹出的窗口中的返回参数

再往下看,有两个方法,分别处理请求和处理异常的,里面都是调用了 handleLog() 方法,所以我们来看看其实现,大家自行查看其代码,结合着操作日志详细弹出窗口来理解

  1. 获取注解
  2. 获取用户
  3. 数据库日志记录,如url,ip地址等
  4. 异常检测,如果出现异常,则状态设置为错误和拿到错误消息
  5. 设置方法名称、请求方式和参数
  6. 保存到数据库

查询操作详细记录可以登录系统(系统管理-操作日志)


  1. 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability) ↩︎

posted @ 2020-12-01 23:19  刘条条  阅读(1706)  评论(0编辑  收藏  举报