日志收集
日志收集
IOC和AOP是Spring体系中两个非常重要的概念,下面就采用AOP的技术为方法添加一个切面,实现接口访问统一日志处理
简单介绍
AOP又名Aspect Oriented Programming 意为 ‘面向切面编程’通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术。
提示
这种在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,动态地将代码在不改变原有的逻辑情况下切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
为什么使用AOP
我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用
但是在分散代码的同时,也增加了代码的重复性。比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
如果将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类,所以采用AOP可以很好的解决这个问题
相关信息
我们可以利用AOP的思想来对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在Spring AOP中业务逻辑仅仅只关注业务本身,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP相当于一个拦截器,去拦截一些处理,例如:当一个方法执行的时候,Spring 能够拦截正在执行的方法,在方法执行的前或者后增加额外的功能和处理,就是我们希望通过使用AOP来对我们的代码进行耦合度的降低 把原本杂乱交错的关系给分离开来 进而改变这些行为的时候不会因为杂乱的关系互相影响。
关键术语
- 横切关注点,从每个方法中抽取出来的同一类非核心业务
- 切面(Aspect),对横切关注点进行封装的类,每个关注点体现为一个通知方法;通常使用 @Aspect 注解来定义切面。
- 通知(Advice),切面必须要完成的各个具体工作,比如我们的日志切面需要记录接口调用前后参数。通知的方式有五种:
- @Before:通知方法会在目标方法调用之前执行
- @After:通知方法会在目标方法调用后执行
- @AfterReturning:通知方法会在目标方法返回后执行
- @AfterThrowing:通知方法会在目标方法抛出异常后执行
- @Around:把整个目标方法包裹起来,在被调用前和调用之后分别执行通知方法
- 连接点(JoinPoint),通知应用的时机,比如接口方法被调用时就是日志切面的连接点。
- 切点(Pointcut),通知功能被应用的范围,比如本篇日志切面的应用范围是所有controller的接口。通常使用 @Pointcut注解来定义切点表达式。
maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--web 针对@Component,将类交由Spring管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--jpa 引入切面的依赖-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
定义切面
@Aspect:作用是把当前类标识为一个切面供容器读取
如果一个类被打上了@Aspect就代表着他是一个切面类
当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:
@Log4j
@Aspect
@Component
public class SysLogAspect {
}
声明 pointcut(切入点)
一个 pointcut 的声明由两部分组成:
一个方法签名, 包括方法名和相关参数 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入advice).
@Log4j
@Aspect
@Component
public class SysLogAspect {
@Pointcut("@annotation(top.jingxc.server.aop.OperationLogger)")
public void controllerAspect() {
}
}
提示
这里没有加限制例如 @execution(* top.jingxc.server.*(..)),表示只匹配top.jingxc.server包下任意方法 这里采用自定义注解的方式@annotation,表示匹配加上改自定义注解的方法全部适用
当然如果要加以限制包,可以两个注解全部加上,中间以&&链接即可 如:value = "(@execution(* top.jingxc.server.*(..))) && @annotation(top.jingxc.server.aop.OperationLogger)"
自定义注解
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLogger {
}
切面实体类
这里只列出了,前置和后置通知,其他的可按需自行添加
@Log4j
@Aspect
@Component
public class SysLogAspect {
@Pointcut("@annotation(top.jingxc.server.aop.OperationLogger)")
public void controllerAspect() {
}
/**
* 前置通知
*
* @param joinPoint
* @throws Throwable
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Map<String, Object> param = new HashMap<>();
String classType = joinPoint.getTarget().getClass().getName();
Class<?> clazz = Class.forName(classType);
String clazzName = clazz.getName();
param.put("clazzName", clazzName);
String methodName = joinPoint.getSignature().getName();
param.put("methodName", methodName);
String[] paramNames = getFieldsName(this.getClass(), clazzName, methodName);
Object[] args = joinPoint.getArgs();
param.put("args", args);
param.put("paramNames", paramNames);
log.info("******进入方法*********\n" + JSON.toJSONString(param)
+ "\n***************************************************************************************");
param = null;
}
/**
* 后置通知 打印返回值日志
*
* @param ret 返回值
* @throws Throwable 异常
*/
@AfterReturning(returning = "ret", pointcut = "controllerAspect()")
public void doAfterReturning(JoinPoint joinPoint, Object ret) throws Throwable {
Map<String, Object> param = new HashMap<>();
String classType = joinPoint.getTarget().getClass().getName();
Class<?> clazz = Class.forName(classType);
String clazzName = clazz.getName();
param.put("clazzName", clazzName);
String methodName = joinPoint.getSignature().getName();
param.put("methodName", methodName);
param.put("ret", ret);
log.info("******返回方法*********\n" + JSON.toJSONString(param)
+ "\n***************************************************************************************");
param = null;
}
/**
* 得到方法参数的名称
*
* @param cls 类
* @param clazzName 类名
* @param methodName 方法名
* @return 参数名数组
* @throws NotFoundException 异常
*/
private static String[] getFieldsName(Class<?> cls, String clazzName, String methodName) throws NotFoundException {
ClassPool pool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(cls);
pool.insertClassPath(classPath);
CtClass cc = pool.get(clazzName);
CtMethod cm = cc.getDeclaredMethod(methodName);
MethodInfo methodInfo = cm.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
String[] paramNames = new String[cm.getParameterTypes().length];
int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
for (int i = 0; i < paramNames.length; i++) {
paramNames[i] = attr.variableName(i + pos); // paramNames即参数名
}
return paramNames;
}
}
使用
在需要打印日志的方法上加上@OperationLogger注解即可,简单实用
- 本文作者: 景兴春
- 本文链接: https://www.jingxc.top/spring/logger.html
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!