一.简介
aop面向切面编程,我们可以使用它来进行方法执行之前做一些事情,也可以在方法之后做一些事情等等。
消息通知类包括:
1.前置通知(before):目标方法运行之前调用;
2.最终通知(after):在目标方法运行之后调用,无论是否正常执行完成,还是抛出异常,都会执行;
3.后置通知(after-returning):在目标方法正常执行之后执行,如果出现异常,则不会执行;
4.异常拦截通知(after-throwing):在目标方法运行之后,并且出现异常就执行;
5.环绕通知(around):在目标方法执行之前和之后都会调用。
基于aop的这种特性,我们可以用它来做一些全局性的控制,此处就来聊聊使用它做全局日志的控制,并且支持动态日志的实现。
二.具体实现过程
1.定义切点注解
我们可以使用注解的方式定义切点,也可以使用通配符进行配置,此处我们使用注解的方式进行配置。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
//模块名称
ModuleNameEnum moduleName() default ModuleNameEnum.UNKNOWN;
//操作对象
String operaName() default "";
//操作类型
OperaTypeEnum operaType() default OperaTypeEnum.UNKNOWN;
}
@Target:定义了此注解的作用范围,ElementType.METHOD:作用在方法上;
@Retention:定义此注解的生命周期,RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@Documented:定义文档。
上面我们定义了一个注解LogAnnotation,此注解有三个参数,分别用于使用注解时传递对应的日志参数值。
2.定义枚举值
上面的注解中我们有使用枚举值作为参数,使用枚举值的好处就是,以后对枚举值的修改,只用修改枚举类就行,不用逐个去改引用此值的地方。
@AllArgsConstructor
@ToString
public enum ModuleNameEnum implements Value<String> {
ManageUser("人员管理"),
SysUser("用户管理"),
FeedBack("信息管理"),
Publish("发布管理"),
UNKNOWN("未定义");
@JsonValue
@Getter
private final String value;
}
@AllArgsConstructor
@ToString
public enum OperaTypeEnum implements Value<String> {
Insert("新增"),
Update("更新"),
Delete("删除"),
UNKNOWN("未定义");
@JsonValue
@Getter
private final String value;
}
public interface Value<T> {
T getValue();
}
@AllArgsConstructor:使用注解生成类的构造函数;
@ToString:使用注解生成类的tostring方法;
@JsonValue:对于枚举值的格式定义;
@Getter:对枚举值添加获取具体值的方法。
3.定义日志切面类
定义一个日志切面类,对方法进行拦截,对操作进行日志保存。
//@Aspect:标识当前类为一个切面,在spring容器加载的时候,会扫描到它
//@Component:把普通pojo实例化到spring容器中,类似于xml配置时的<bean>
//@Slf4j:定义日志
@Aspect
@Component
@Slf4j
public class LogAspect {
//注入日志service类
@Autowired
XXXLogService xxxLogService;
//节点:使用LogAnnotation注解标识的方式都进行切入,也可以使用通配符配置具体要接入的方法名
@Pointcut("@annotation(xxx.aop.LogAnnotation)")
public void pointCut(){
}
//环绕通知,
@Around("pointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
//方法执行之后的结果值
Object jsonResult = joinPoint.proceed();
try {
//获取请求结果值
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取到注解
LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
//获取注解里面字段的值
String moduleName = annotation.moduleName().getValue();
String operaName = annotation.operaName();
String operaType = annotation.operaType().getValue();
//获取到方法调用时传递的参数名
String[] parameterNames = signature.getParameterNames();
//获取到方法调用时各个参数的值
Object[] args = joinPoint.getArgs();
//使用替代的方式,获取动态日志
operaName = getOperaName(parameterNames,args,operaName);
//创建日志实体类
XxxLog xxxLog = new XxxLog();
xxxLog.setModuleName(moduleName);
xxxLog.setOperaName(operaName);
xxxLog.setOperaType(operaType);
//获取登录用户信息,可以根据自己业务系统的方式进行获取
xxxLog.setCreateUserId(StpUtil.getLoginIdAsString());
//调用保存日志的方法
xxxLogService.insertOne(xxxLog);
} catch (Exception e){
e.printStackTrace();
} catch (Throwable e) {
e.printStackTrace();
}
//返回方法执行之后的结果值
return jsonResult;
}
/**
* @Description: 使用替代的方式实现动态日志
* @Author: zhanglizeng
* @Param: [parameterNames, args, operaName]
* @Return: java.lang.String
*/
private String getOperaName(String[] argNames, Object[] args, String operaName) {
//先把所有的参数进行遍历,作为替代的key,参数值作为替代的value
Map<Object, Object> map = new HashMap<Object, Object>();
for(int i = 0;i < argNames.length;i++){
//参数和参数值是一一对应的,可以这样进行设置值
map.put(argNames[i],args[i]);
}
try {
//此处我们使用了operaName的值包含参数key的字符方式,例operaName="添加用户{{name}}"
//而name作为方法调用时传递的一个参数名,在argNames[]数组中可以取到它,
//name的参数值在args[]数组中可以取到它,然后我们使用字符替代的方式即可把operaName的值变为operaName="添加用户张三"
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object k = entry.getKey();
Object v = entry.getValue();
operaName = operaName.replace("{{" + k + "}}", JSONObject.toJSONString(v));
}
}catch (Exception e){
e.printStackTrace();
}
return operaName;
}
}
@Aspect:标识当前类为一个切面,在spring容器加载的时候,会扫描到它;
@Component:把普通pojo实例化到spring容器中,类似于xml配置时的;
@Slf4j:定义日志;
@Pointcut:定义切点;
@annotation(xxx.aop.LogAnnotation):定义切点的入口是注解,并且指明注解的目录,此处也可以使用通配符的方式配置切点的入口,此处我们使用注解的方式;
@Around(“pointCut()”):环绕通知,环绕的方法是上面切点的方法;
4.方法中配置注解
我们只需要在需要进行日志记录的方法上,添加我们的注解即可进行日志的添加。
@LogAnnotation(moduleName= ModuleNameEnum.ManageUser,operaName="添加用户{{userName}}",operaType=OperaTypeEnum.Insert)
public XxxUser addUser(String currentUserId,String userName) {
XxxUser xxxUser = new XxxUser();
xxxUser.setUserName(userName);
xxxUserDao.addUser(xxxUser);
return xxxUser;
}
@LogAnnotation:使用注解标识调用此方法时需要执行aop的切面;可以在此处设置注解中字段的值,我们使用{{}}这样的特殊符号来作为替代字符的标识;注意{{}}里面的值需要与参数名保持一致,这样在后续替代的时候才能匹配的上。例如此处传递的userName值为张三,则替代后的operaName值为:添加用户张三。
执行结果截图: