提出需求
这里所说的记录日志,是指插入一条记录到数据库的日志表。只是插入一条数据库记录而已,一点技术含量都没有。我直接在MyBatis的mapper编写插入语句,在Java代码调用mapper方法就可以了。比如我可以这样,
sysBaseAPI
写一行语句,我就能插入一条日志了,
做了10年Java开发了,如果还用这个弱智方法,以后面试怎么吹水呢?不行,我们要简单问题复杂化。我今天要用AOP切面+注解的方式来记录日志。我们要达到的效果是,Controller方法前面写一行@SystemLog注解,就能自动插入一条数据库记录。
/**
其实用注解的方式,和直接在代码里面调用MyBatis mapper,实现的功能是一样的,写的代码量也是一样的,都要写长长的一行,只是代码出现的位置不同而已。 直接在代码里面调用,出现的位置是Java方法体内。注解的方式,出现的位置是Java方法签名之前。注解这个东西很多时候只是一个审美上的选择,有人觉得注解用起来很有逼格,和stream,Lambda之类的同理。由于注解出现在方法签名之前,有的人把它看作是配置而不是代码,造成一种零代码实现业务功能的错觉(无代码时代要来临了)。自己编写注解类,那更是有一种自己在编写框架的错觉,毕竟大多数的注解都是各种框架带的,自己写注解的情况很少。
怎样实现注解类
我们以前水平菜的时候,以为注解类只是一个类,比如@Autowired这个注解,我们打开Autowired类的源码看了半天,也没看出什么东西,不明白为什么这样就能实现自动装配的功能。其实注解类的功能分成两部分实现,一个是注解类,另一个是切面处理类。真正的逻辑都写在切面处理类里面。注解类本身只是定义了几个参数(属性)而已。
注解类代码
我今天写的注解取名为SystemLog,名字随便起,为了显示逼格,你可以起一个看上去像框架官方API的名字。
package
切面处理类代码
切面处理类用到了AspectJ Weaver框架,这个框架把注解所在的那个方法携带的各种数据都封装到一个JoinPoint对象中,然后我们从JoinPoint中可以获取到这些数据。
AspectJ Weaver框架包含在了Spring Boot AOP中。
<dependency>
除了AspectJ Weaver和FastJSON之外, 我这里引入的一些其他类库是具体业务相关的,而不是切面处理所通用的,可以忽略。
package
切面处理类核心的方法是around方法。根据网上资料,around方法是在注解所在的方法执行前和执行后触发执行,我亲自调试,觉得它是在方法执行期间、且还没有return之前(return的前一句)触发。(见我下面画的箭头位置)
/**
* 机构管理 - 新增机构
*/
@RequestMapping(value = "/add", method = RequestMethod.POST)
@SystemLog(logContent = "机构管理-新增机构,机构名:departName,上级机构ID:parentId,备注信息:notes", operateType = OPERATE_TYPE_ADD)
// 用@SystemLog注解实现记录日志
@CacheEvict(value= {CacheConstant.SYS_DEPARTS_CACHE,CacheConstant.SYS_DEPART_IDS_CACHE}, allEntries=true)
public Result<SysDepart> add(@RequestBody SysDepart sysDepart, HttpServletRequest request) {
// 业务逻辑
// 此处完全不出现插入数据库的语句,不调用MyBatis mapper
// 此处触发执行切面处理类around方法 <———————— 就是在这个地方触发
return result;
}
这一套方案简单来说就是,上面这段代码,执行到箭头的位置,会触发@SystemLog注解,去执行切面处理类的around()方法。
凡是和AOP切面处理相关的代码,都不能用JRebel热部署,热部署会造成莫名其妙的问题。所以每次修改代码,建议重启Spring Boot工程。
动态配置注解参数(注解参数中包含变量)
我们使用Java注解都有一个体会,注解的参数里面是不能包含变量的,只能是固定的值。比如@RequestMapping注解,
@RequestMapping
这里的value = "/edit",是一个固定字符串,不能是变量。
而我们在记录日志的时候,可能要根据不同的情况,记录不同的日志内容,用固定字符串是满足不了需求的。
插入的日志内容为“机构管理-新增机构,机构名:浩宇镇,上级机构ID:,备注信息:添加机构成功”,我们注解中配置的参数值为 "机构管理-新增机构,机构名:departName,上级机构ID:parentId,备注信息:notes"。可见这里有三个变量,在记录日志时,dynamicParam()方法会把变量名替换成变量的值。
有三种途径可以给这些变量赋值。
1、注解所在的方法参数中,含有与变量名相同的属性。
这里的sysDepart对象包含了departName和parentId两个属性,这些信息都会包含在切面处理类的JoinPoint对象中,从而能获取到departName和parentId两个变量的值。
2、HttpServletRequest对象中,含有与变量名相同的ParameterName
3、HttpServletRequest对象中,含有与变量名相同的AttributeName
这里的notes变量就是通过Attribute去赋值的,要在方法体内写一句代码给它赋值,
request
我在切面处理类的实现中,已经把以上3种情况都考虑到了,满足其中任何一种,都有相应代码去获取到变量值,从而把变量名替换成变量值。
这里有一个我设计得不合理的地方,我是用String.replaceAll()替换字符串,把变量名替换成变量值,这里的变量名是departName, parentId, notes,假如在HttpServletRequest对象中正好有一个Parameter或者Attribute,名为 depart,或者名为 parent,只要是名字能跟变量名中的一部分匹配上的,我的算法也会把它给替换掉。
比如,我们假设HttpServletRequest对象中有另一个Parameter为 depart = "浩宇",
我的算法并不知道 departName 才是完整的变量名,它匹配到了 depart,认为这就是变量名,于是把 depart 替换成 浩宇。 这样一来,记录的日志内容可能会变成:
机构管理-新增机构,机构名:浩宇Name,上级机构ID:,备注信息:添加机构成功
要改进也很简单,用指定的符号(可以是大括号)把变量名括起来,在replaceAll那里,把大括号也一起替换掉即可。由于前后有大括号包着,只有与变量名完全匹配的参数才会替换。