09-AOP(日志收集)
一、AOP(日志收集)
面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,用于在不改变程序主业务逻辑的情况下,通过在运行时动态地添加行为来实现跨越多个模块的功能,如日志收集、性能监控、事务管理等。AOP的核心思想是将这些横切关注点(cross-cutting concerns)分离出来,以增强代码的可维护性和重用性。
AOP概念
切面(Aspect):
- 切面是AOP的核心,定义了横切关注点的行为和属性。一个切面可以包含多个切点(Pointcut)和通知(Advice)。
切点(Pointcut):
- 切点定义了在哪些地方插入横切逻辑,即哪些方法或类需要应用横切关注点。
通知(Advice):
- 通知是切面中实际的横切逻辑,它定义了在切点处执行的具体动作。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。
目标对象(Target Object):
- 目标对象是被增强的对象,它们包含了应用横切关注点的方法或类。
代理(Proxy):
- AOP通过代理模式来实现切面逻辑的插入,代理对象是目标对象的增强版本。
Spring AOP
Spring AOP是Spring框架中的一部分,提供了对AOP的支持。Spring AOP主要基于动态代理(JDK动态代理和CGLIB代理)来实现切面功能。
日志收集示例(不自定义注解)
以下是一个简单的Spring AOP示例,用于日志收集:
1. 引入依赖
在Spring项目的pom.xml
中引入AOP相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 创建切面类
定义一个切面类,用于在方法执行前后收集日志:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
logger.info("Before method: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("After method: " + joinPoint.getSignature().getName() + ", return value: " + result);
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
logger.error("Exception in method: " + joinPoint.getSignature().getName(), error);
}
}
3. 配置Spring AOP
在Spring Boot项目中,Spring AOP通常是自动配置的。只需要确保切面类被Spring扫描到即可。通常通过在主配置类或启动类上添加@EnableAspectJAutoProxy
注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class AopExampleApplication {
public static void main(String[] args) {
SpringApplication.run(AopExampleApplication.class, args);
}
}
4. 创建目标服务类
创建一个示例服务类,用于测试日志收集:
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
public String sayHello(String name) {
return "Hello, " + name;
}
public void throwException() {
throw new RuntimeException("This is a test exception");
}
}
运行示例
运行Spring Boot应用程序后,调用ExampleService
的方法可以看到日志被正确收集:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExampleController {
@Autowired
private ExampleService exampleService;
@GetMapping("/hello")
public String hello(@RequestParam String name) {
return exampleService.sayHello(name);
}
@GetMapping("/exception")
public void exception() {
exampleService.throwException();
}
}
通过访问 /hello?name=World
和 /exception
这两个接口,可以看到日志在方法执行前、后以及抛出异常时被记录下来。
结论
AOP通过分离横切关注点提高了代码的可维护性和模块化。Spring AOP使得在Spring应用中实现日志收集变得简单高效。通过切面、通知和切点的结合,可以灵活地插入各种横切逻辑,而不会干扰主业务逻辑。
二、为什么实现AOP要自定义注解?
自定义注解在AOP(面向切面编程)中使用,可以为切面提供更灵活、更易读的配置方式。通过自定义注解,开发者可以更精确地控制横切关注点的应用。以下是实现AOP时自定义注解的一些原因和示例:
为什么要自定义注解
提高可读性和可维护性:
- 自定义注解可以清晰地标识哪些方法或类需要应用特定的切面逻辑,使代码更具可读性和维护性。
增强灵活性:
- 自定义注解可以携带元数据,使切面逻辑更加灵活。例如,可以在注解中添加参数,以控制不同的日志级别或其他行为。
解耦业务逻辑和切面逻辑:
- 使用自定义注解可以将业务逻辑和横切关注点(如日志记录、事务管理)分离,使得业务代码更加简洁,切面逻辑独立管理。
方便复用:
- 自定义注解可以在多个地方复用,避免了在每个地方重复定义切点表达式,提高了代码复用性。
实现AOP日志收集的自定义注解示例
1. 创建自定义注解
首先,定义一个自定义注解@LogExecution
:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution {
String value() default "";
}
2. 创建切面类
定义一个切面类,使用自定义注解@LogExecution
来标识需要应用日志收集的地方:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("@annotation(com.example.demo.LogExecution)")
public void logExecutionMethods() {}
@Before("logExecutionMethods() && @annotation(logExecution)")
public void logBefore(JoinPoint joinPoint, LogExecution logExecution) {
logger.info("Before method: " + joinPoint.getSignature().getName() + ", Message: " + logExecution.value());
}
@AfterReturning(pointcut = "logExecutionMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("After method: " + joinPoint.getSignature().getName() + ", return value: " + result);
}
@AfterThrowing(pointcut = "logExecutionMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
logger.error("Exception in method: " + joinPoint.getSignature().getName(), error);
}
}
3. 使用自定义注解
在需要日志收集的业务方法上使用自定义注解@LogExecution
:
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
@LogExecution("Saying Hello")
public String sayHello(String name) {
return "Hello, " + name;
}
@LogExecution("Throwing exception")
public void throwException() {
throw new RuntimeException("This is a test exception");
}
}
运行示例
运行Spring Boot应用程序后,调用ExampleService
的方法可以看到日志被正确收集:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExampleController {
@Autowired
private ExampleService exampleService;
@GetMapping("/hello")
public String hello(@RequestParam String name) {
return exampleService.sayHello(name);
}
@GetMapping("/exception")
public void exception() {
exampleService.throwException();
}
}
通过访问 /hello?name=World
和 /exception
这两个接口,可以看到日志在方法执行前、后以及抛出异常时被记录下来。
总结
通过自定义注解实现AOP,可以更灵活地控制切面的应用,提高代码的可读性和可维护性。自定义注解不仅简化了切点定义,还增强了业务逻辑和横切逻辑的解耦,使得AOP的使用更加方便和高效。
三、为什么定义了注解就可以收集?其原理是什么?
注解的底层原理涉及Java反射机制和字节码操作。Spring AOP通过动态代理或字节码操作来实现切面编程。以下是注解在日志收集中起作用的详细原理:
1. 注解的定义和使用
注解本质上是元数据,可以附加到代码的类、方法、字段等元素上。注解本身不会执行任何逻辑,但可以通过反射机制在运行时获取这些元数据,并根据这些元数据执行相应的逻辑。
2. Spring AOP的工作原理
Spring AOP主要有两种实现方式:基于代理(Proxy-based AOP)和基于AspectJ的编译时织入(Compile-time Weaving)或加载时织入(Load-time Weaving)。
基于代理的AOP
- JDK动态代理:适用于接口代理。Spring AOP会为目标对象创建一个代理对象,代理对象实现了目标对象的接口,并在方法调用前后执行切面逻辑。
- CGLIB代理:适用于类代理。Spring AOP会生成目标类的子类,并在子类的方法调用前后执行切面逻辑。
基于AspectJ的AOP
AspectJ是一个功能更强大的AOP框架,支持编译时织入和加载时织入。Spring AOP可以与AspectJ集成,通过AspectJ的注解和编译器实现更复杂的切面逻辑。
3. 注解在AOP中的作用
在Spring AOP中,注解通常用于定义切入点(Pointcut)。切入点是一个表达式,定义了在哪些连接点(Join Point)上应用切面逻辑。通过注解,可以更灵活地指定切入点。
4. 具体实现步骤
1. 定义注解
定义一个自定义注解@Loggable
,用于标识需要日志记录的方法。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
}
2. 定义切面
定义一个切面类LoggingAspect
,在切面类中使用@Pointcut
注解定义切入点,使用@Before
、@After
、@AfterReturning
和@AfterThrowing
注解定义增强逻辑。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
// 定义一个切入点,匹配所有带有@Loggable注解的方法
@Pointcut("@annotation(Loggable)")
public void loggableMethods() {
// 方法体可以为空,因为这是一个标识切入点的方法
}
// 在方法执行之前记录日志
@Before("loggableMethods()")
public void logBefore(JoinPoint joinPoint) {
logger.info("Executing method: " + joinPoint.getSignature().getName());
}
// 在方法执行之后记录日志
@After("loggableMethods()")
public void logAfter(JoinPoint joinPoint) {
logger.info("Method executed: " + joinPoint.getSignature().getName());
}
// 在方法成功返回之后记录日志
@AfterReturning(pointcut = "loggableMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("Method returned: " + joinPoint.getSignature().getName() + " with result = " + result);
}
// 在方法抛出异常之后记录日志
@AfterThrowing(pointcut = "loggableMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
logger.error("Method threw exception: " + joinPoint.getSignature().getName(), error);
}
}
3. 使用注解
在需要日志记录的方法上使用@Loggable
注解。
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
@Loggable
public String sayHello(String name) {
return "Hello, " + name;
}
@Loggable
public void throwException() {
throw new RuntimeException("This is a test exception");
}
}
5. 运行时机制
- Spring容器启动:Spring容器启动时,会扫描所有的Bean,并根据配置创建代理对象。
- 代理对象创建:对于带有切面逻辑的Bean,Spring AOP会创建代理对象。代理对象会拦截方法调用,并在方法调用前后执行切面逻辑。
- 方法调用拦截:当调用带有
@Loggable
注解的方法时,代理对象会拦截调用,并根据切面定义执行日志记录逻辑。
通过这种方式,注解和AOP结合使用,可以在不修改业务逻辑的情况下实现日志收集。注解提供了灵活的标识机制,而AOP提供了强大的拦截和增强功能。
四、为什么定义注解和不定义注解都可以实现日志收集?
实现日志收集可以通过定义注解和不定义注解两种方式。两种方式都能实现相同的目标,但各有优缺点和适用场景。下面将详细解释为什么这两种方式都能实现日志收集,并比较它们的不同点。
定义注解实现日志收集
原理
自定义注解:
- 通过定义自定义注解(如
@LogExecution
),可以显式地标识需要进行日志收集的方法。
- 通过定义自定义注解(如
切面类和切点:
- 切面类会使用注解表达式(如
@annotation(com.example.LogExecution)
)定义切点。 - 切面中的通知方法会在匹配切点的方法执行前、后或异常时执行日志收集逻辑。
- 切面类会使用注解表达式(如
示例
// 自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution {
String value() default "";
}
// 切面类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("@annotation(com.example.LogExecution)")
public void logExecutionMethods() {}
@Before("logExecutionMethods() && @annotation(logExecution)")
public void logBefore(JoinPoint joinPoint, LogExecution logExecution) {
logger.info("Before method: " + joinPoint.getSignature().getName() + ", Message: " + logExecution.value());
}
@AfterReturning(pointcut = "logExecutionMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("After method: " + joinPoint.getSignature().getName() + ", return value: " + result);
}
@AfterThrowing(pointcut = "logExecutionMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
logger.error("Exception in method: " + joinPoint.getSignature().getName(), error);
}
}
// 使用注解的业务方法
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
@LogExecution("Saying Hello")
public String sayHello(String name) {
return "Hello, " + name;
}
@LogExecution("Throwing exception")
public void throwException() {
throw new RuntimeException("This is a test exception");
}
}
优点
- 清晰标识:自定义注解清晰地标识了哪些方法需要日志收集,提高了代码的可读性和维护性。
- 灵活性:可以通过注解携带元数据,以定制化日志收集行为。
缺点
- 额外代码:需要定义注解和在目标方法上使用注解。
不定义注解实现日志收集
原理
- 切面类和切点:
- 切面类直接通过方法表达式(如
execution(* com.example..*(..))
)定义切点。 - 切面中的通知方法会在匹配切点的方法执行前、后或异常时执行日志收集逻辑。
- 切面类直接通过方法表达式(如
示例
// 切面类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Pointcut("execution(* com.example..*(..))")
public void logExecutionMethods() {}
@Before("logExecutionMethods()")
public void logBefore(JoinPoint joinPoint) {
logger.info("Before method: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "logExecutionMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
logger.info("After method: " + joinPoint.getSignature().getName() + ", return value: " + result);
}
@AfterThrowing(pointcut = "logExecutionMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
logger.error("Exception in method: " + joinPoint.getSignature().getName(), error);
}
}
// 业务方法
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
public String sayHello(String name) {
return "Hello, " + name;
}
public void throwException() {
throw new RuntimeException("This is a test exception");
}
}
优点
- 简单:无需定义额外的注解和在目标方法上使用注解。
- 广泛应用:可以应用于整个包或类的所有方法,方便统一处理。
缺点
- 缺乏灵活性:无法精确控制日志收集的范围,所有匹配的切点都会应用日志收集逻辑。
- 可读性差:不容易看出哪些方法会被增强,可能需要查看切面类中的切点定义。
总结
定义注解和不定义注解两种方式都可以实现日志收集,其原理都是通过AOP切面拦截目标方法,然后在方法执行前后或抛出异常时执行日志收集逻辑。选择哪种方式取决于具体的需求和场景:
- 定义注解:适用于需要精确控制哪些方法需要日志收集,增强代码可读性和灵活性的场景。
- 不定义注解:适用于需要广泛应用日志收集逻辑,简化开发工作的场景。