最近开发中有一个需求,要求对指定的操作进行日志管理,以备以后查验。为了在保全大部分代码结构的前提下,选择了面向切面编程AOP,同时并不是全部的操作都需要进行日志管理,所以选择了自定义注解。
先简单介绍一下代码情况,在不破坏之前开发的代码结构的情况下,在公共工程中开发了注解和切面工程的大部分代码,在需要使用的工程中只进行配置的部分,根据业务需要还进行一个小小的调整,暂且不表。
第一个部分是定义一个注解,代码如下:
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FcoTest {
String name() default "null";
}
首先@Retention: 定义注解的保留策略
@Retention(RetentionPolicy.SOURCE) // 注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
三者的区别在于注解的作用时间,生命周期的长短,从上到下而言,越靠下生命周期越长。如果需要在运行时动态获取注解信息的话,只能用RUNTIME,而当如果需要在编译时就进行一些预处理的话,就需要选择CLASS,而只是一些检查性操作的话选取SOURCE会比较好一点。
而本次开发中需要深入业务层,所以还是选择了RUNTIME比较合适。
其次@Target 控制了触发注解的类型(定义注解的作用目标)
@Target(ElementType.TYPE) //接口、类、枚举、注解
@Target(ElementType.FIELD) //字段、枚举的常量
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR) //构造函数
@Target(ElementType.LOCAL_VARIABLE) //局部变量
@Target(ElementType.ANNOTATION_TYPE) //注解
@Target(ElementType.PACKAGE) //包
根据需要使用注解的位置使用合适的类型,本次开发主要是用在server层的方法上所以选择了@Target(ElementType.METHOD)。
最后@Document,它说明该注解将被包含在javadoc中。
PS:不知道为什么要加,但是加了也没坏处,就加上了。
第二个部分是定义了一个切面,切入点以及处理操作,代码如下:
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.fco.auth.front.utils.Dql;
import org.fco.auth.front.utils.IdWorkerUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("@annotation(org.fco.auth.front.annotation.FcoTest)")
public void annotationPointCut(){}
@Before("annotationPointCut()")
public void before (JoinPoint joinPoint) {
MethodSignature sign = (MethodSignature)joinPoint.getSignature();
Method method = sign.getMethod();
log.error("the opt id is {}", LogContext.getOperateId());
System.out.println("函数名:" + method.getName());
Object[] params = joinPoint.getArgs();
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Map map = new HashMap<String, Object>();
for (int i = 0; i < names.length; i++) {
map.put(names[i], params[i]);
}
log.error("the params is {}", JSON.toJSONString(map));
// 存库
Dql.fco().insert("saveLog").params(IdWorkerUtils.next(), method.getName(), JSON.toJSONString(map), LogContext.getOperateId()).execute();
}
}
首先@Aspect作用是把当前类标识为一个切面供容器读取
@Component是把普通pojo实例化到spring容器中的一个注解,其实就是相当于让Spring把这个类纳入管理之中。
其次@Pointcut:声明一个切入点,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。SpringAOP只支持Springbean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。注:作为切入点签名的方法必须返回void类型。此处使用的是@annotation的方式。
Spring AOP支持在切入点表达式中使用如下的切入点指示符:
execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。(用的最多)
within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
@target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
@args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
@within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
@annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
modifier-pattern:表示方法的修饰符
ret-type-pattern:表示方法的返回值
declaring-type-pattern?:表示方法所在的类的路径
name-pattern:表示方法名
param-pattern:表示方法的参数
throws-pattern:表示方法抛出的异常
其中后面跟着“?”的是可选项。*可以匹配任何值,可以用(..)表示零个或多个任意的方法参数,使用&&符号表示与关系,使用||表示或关系、使用!表示非关系。在XML文件中使用and、or和not这三个符号。
例如:excecution(* com.tianmaying.service.BlogService.updateBlog(..))
1)execution(* *(..)) //表示匹配所有方法
2)execution(public * org.fco.admin.server.UserService.*(..)) //表示匹配org.fco.admin.server.UserService中所有的公有方法
3)execution(* org.fco.admin.server.*.*(..)) //org.fco.admin.server包及其子包下的所有方法
@annotation(注解的地址)
例如:"@annotation(org.fco.auth.front.annotation.FcoTest)"
除了@annotation和@args外,还有另外两个用于注解的切点函数,分别是@target和@within和@annotation @args函数一样,@target和@within也只接受注解类名作为入参。其中@target(M)匹配任意标注了@M的目标类,而@within(M)匹配标注了@M的类及其子孙类
@target使用@target(注解类型全限定名)匹配当前目标对象类型的执行方法, 必须是在目标对象上声明注解,在接口上声明不起作用。@within(M)的匹配规则经验证,目前发现和 @target(M)的匹配规则是一样的。
PS: @args @target @within没有亲测过,通过多篇他人文章所得。
接下来介绍一下@Before("annotationPointCut()")
@Before是在所拦截方法执行之前执行一段逻辑。
@After 是在所拦截方法执行之后执行一段逻辑。
@Around是可以同时在所拦截方法的前后执行一段逻辑。
括号中是前面定义了切入点签名的方法的方法名。
@Around("annotationPointCut()")
public void around(ProceedingJoinPoint pjp) throws Throwable{
this.printLog("已经记录下操作日志@Around 方法执行前");
pjp.proceed(); // 不可少
this.printLog("已经记录下操作日志@Around 方法执行后");
}
最后介绍一下内部处理的一些问题,通过反射得到方法的method对象,只能得到一些参数类型,返回类型,方法名,必须从joinPoint中获取实际传入的参数数组:
Object[] params = joinPoint.getArgs();
而获取参数名列表则需要 通过String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
而通过method可以获取当前的方法名,用于记录该日志是何操作触发的,以供查验。
第三个部分Application,代码如下:
@SpringBootApplication(scanBasePackages = "org.fco")
保证之前写的注解和面向切面的类被纳入管理。
开发中主要遇到了以下问题:
1.在公共工程中切面代码完成后,没有被需要调用的工程的spring容器纳入管理,最后在需要调用公共工程的Application类中加上了SpringbootApplication的注解
2.在记录操作人时,发现注解上不能使用变量,所以只能对需要调用的工程进行简单的处理,在它们的代码中自动帮公共组件填充一个操作人ID,然后在切面所需要执行的代码中获取这个ID并记录到库中。
3.获取触发这个切面的函数参数值和参数名,通过AOP获取的JoinPoint可以顺利的解决这个问题。
网友评论