简介
从若依项目学习AOP。
首先,ruoyi对于一个项目的规划已经做得非常好了,我所说的项目规划是指ruoyi-vue中pom项目的规划,包括父项目、子项目、包管理、类管理等等。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的实现吧。
@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-common中com.ruoyi.common.annotation.Log,这里定义了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;
}
日志的基本信息都有了,对于后期发现定位问题很有帮助。
其中,功能和操作人类别都是com.ruoyi.common.enums下定义的枚举。
切面Advices
如果使用的是Ultimate版的IDEA,就可以在前面“新增部门”的add方法上进行智能跳转,直接跳到对应的切面处理方法

ruoyi-framework项目的com.ruoyi.framework.aspectj.LogAspect就是日志切面的具体处理类。
/**
* 操作日志记录处理
*
* @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方法来做的。
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();
}
}
上面每一步都有注释,非常清晰,很容易看明白。
-
获取注解,这一步我是有点疑问的,既然通过
AOP进入该方法,还需要验证是否有这个注解吗?有必要吗,是我太浅薄了吗?有懂的大佬可以跟我说明一下😂 -
获取当前用户,通过封装的
Spring Security工具类获取到。 -
完善日志数据,包括请求地址、用户名、返回参数、
URL、状态、错误信息、类名、方法名、参数信息、注解业务数据等。 -
异步插入数据库。
需要知道getControllerMethodDescription、setRequestValue、getAnnotationLog、argsArrayToString、isFilterObject这些方法没有细说,也没有太大必要,这些都是辅助功能的。
另外一提,我这个版本的ruoyi代码也是有点小问题的,比如这里有一个String拼接可以用StringBuilder优化的问题,可能还有。我希望所有人在在看他人代码学习时,都应该带着思考,带着质疑去看,能发现并提出问题就很好。
最后,这里异步插入数据库很值得关注,这种日志数据落库做成异步对于业务来说是很有必要的,它本身并不属于业务,如果做成同步会影响业务RT,没有什么好处,异步的话不仅能充分利用CPU,而且还不影响业务,是很棒的🥰
结
ruoyi设计的AOP风格大差不差,其他的也可以比较着学习,能够学到精髓,并有一定实践就更好了。