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注解的常用属性表

一款管理系统,一般都配备有登录日志,这样管理者可以方便查看每个登录用户的登录情况,下面我们来看看是如何实现的吧~
首先我们打开 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) 

支持参数如下

BusinessType OTHER 操作功能(OTHER其他 INSERT新增 UPDATE修改 DELETE删除 GRANT授权 EXPORT导出 IMPORT导入 FORCE强退 GENCODE生成代码 CLEAN清空数据) operatorType OperatorType MANAGE 操作人类别(OTHER其他 MANAGE后台用户 MOBILE手机端用户) isSaveRequestData boolean 是否保存请求的参数 我们可以打开项目中的注解接口 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) ↩︎

  •