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