ruoyi学习AOP

简介

从若依项目学习AOP

首先,ruoyi对于一个项目的规划已经做得非常好了,我所说的项目规划是指ruoyi-vuepom项目的规划,包括父项目、子项目、包管理、类管理等等。ruoyi项目的规范是让人很舒服,每个模块的职责就很明晰,是非常值得学习的。ruoyi中有很多封装,不管是工具类的,还是配置类的,挺多的。对于项目经验不足的初学者还是很有帮助的。

下面我简单从ruoyi项目出发学习一下ruoyi是如何做AOP的。

AOP概念

AOP的概念对于有一定项目基础的一定不陌生。

“面向切面编程嘛”🤓🤓🤓

面向对象将程序抽象,分为不同模块,各司其职,能有力促进工程开发分工协作,但是不同模块有时会有公共行为,这种行为不适合用继承来实现,维护也比较复杂。切面(AOP)的引入就是为了解决这个问题的,要达到目的就是,在不改变源码的情况下,为不同组件添加公用功能。

ruoyi做法

其实ruoyi有很多地方都用到了AOP,包括数据源确认、数据范围确认、权限认证、限流、防重复提交、Excel表格处理等等,很多的。

它们的原理做法几乎一样,这次就log(日志)学习一下。

切面入口

可以在ruoyi-admin项目中com.ruoyi.web.controller.system.SysDeptController看到@Log

很明显ruoyi是通过自定义注解的方式实现AOP的,当然还有另一种方式:正则。比较注解和正则来说,注解的方式更加方便一些,哪里需要就在哪里添加就好;正则的话,首先对于规范的正则要熟悉,然后包结构要明晰,随意修改很可能出现问题。

这里就不做过多讨论了,来看ruoyi的实现吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/system/dept")
public class SysDeptController extends BaseController {

...

/**
* 新增部门
*/
@PreAuthorize("@ss.hasPermi('system:dept:add')")
@Log(title = "部门管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody SysDept dept) {
if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
}
dept.setCreateBy(getUsername());
return toAjax(deptService.insertDept(dept));
}

...

}

切面注解

这个@Log就是日志切面的标记

@Log(title = "部门管理", businessType = BusinessType.INSERT)

点进去,到达ruoyi-commoncom.ruoyi.common.annotation.Log,这里定义了Log注解,注释也写得非常明白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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;
}

日志的基本信息都有了,对于后期发现定位问题很有帮助。

其中,功能和操作人类别都是com.ruoyi.common.enums下定义的枚举。

切面Advices

如果使用的是Ultimate版的IDEA,就可以在前面“新增部门”的add方法上进行智能跳转,直接跳到对应的切面处理方法

切面处理

ruoyi-framework项目的com.ruoyi.framework.aspectj.LogAspect就是日志切面的具体处理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 操作日志记录处理
*
* @author ruoyi
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.Log)")
public void logPointCut() {
}

/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleLog(joinPoint, null, jsonResult);
}

...

}

@Pointcut配置织入点,这里使用的是自定义注解,所以是("@annotation(com.ruoyi.common.annotation.Log)"),是Log注解的完整类路径。

对于切面如何处理,AOP本身有很多种实现,包括前置、中置、后置、环绕等之类的,可以根据不同的业务做不同的处理,这里是做日志记录,所以是后置处理。除了正常的处理外,这里还包括拦截异常的操作。可以看到都是通过handleLog方法来做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
try {
// 获得注解
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}

// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();

// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
// 返回参数
operLog.setJsonResult(JSON.toJSONString(jsonResult));

operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
if (loginUser != null) {
operLog.setOperName(loginUser.getUsername());
}

if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
} catch (Exception exp) {
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}

上面每一步都有注释,非常清晰,很容易看明白。

  1. 获取注解,这一步我是有点疑问的,既然通过AOP进入该方法,还需要验证是否有这个注解吗?有必要吗,是我太浅薄了吗?有懂的大佬可以跟我说明一下😂

  2. 获取当前用户,通过封装的Spring Security工具类获取到。

  3. 完善日志数据,包括请求地址、用户名、返回参数、URL、状态、错误信息、类名、方法名、参数信息、注解业务数据等。

  4. 异步插入数据库。

需要知道getControllerMethodDescriptionsetRequestValuegetAnnotationLogargsArrayToStringisFilterObject这些方法没有细说,也没有太大必要,这些都是辅助功能的。

另外一提,我这个版本的ruoyi代码也是有点小问题的,比如这里有一个String拼接可以用StringBuilder优化的问题,可能还有。我希望所有人在在看他人代码学习时,都应该带着思考,带着质疑去看,能发现并提出问题就很好。

最后,这里异步插入数据库很值得关注,这种日志数据落库做成异步对于业务来说是很有必要的,它本身并不属于业务,如果做成同步会影响业务RT,没有什么好处,异步的话不仅能充分利用CPU,而且还不影响业务,是很棒的🥰

ruoyi设计的AOP风格大差不差,其他的也可以比较着学习,能够学到精髓,并有一定实践就更好了。