1. Overview
1.概述
We often use logging to document meaningful steps and valuable information during the program execution. It allows us to record data we can use later to debug and analyze the code.
我们经常使用日志来记录程序执行过程中有意义的步骤和有价值的信息。它允许我们记录数据,以便日后用于调试和分析代码。
Additionally, Aspect-Oriented Programming (or AOP for short) is a paradigm that lets us segregate cross-cutting concerns, such as transaction management or logging, throughout the application without cluttering the business logic.
此外,Aspect-Oriented Programming(面向方面编程,简称 AOP)也是一种范式,它能让我们在整个应用程序中隔离事务管理或日志记录等交叉问题,而不会干扰业务逻辑。
In this tutorial, we’ll learn how to implement logging using the AOP and Spring framework.
在本教程中,我们将学习如何使用 AOP 和 Spring 框架实现日志记录。
2. Logging Without AOP
2.无 AOP 的日志记录
When it comes to logging, we usually put logs at the beginning and the end of the methods. This way, we can easily track the application execution flow. In addition, we can capture the values being passed to specific methods and the values they return.
说到日志,我们通常会在方法的开始和结束处放置日志。这样,我们就能轻松跟踪应用程序的执行流程。此外,我们还可以捕获传递给特定方法的值及其返回值。
To demonstrate, let’s create the GreetingService class with the greet() method:
为了演示,让我们创建带有 greet() 方法的 GreetingService 类:
public String greet(String name) {
logger.debug(">> greet() - {}", name);
String result = String.format("Hello %s", name);
logger.debug("<< greet() - {}", result);
return result;
}
Even though the implementation above seems like a standard solution, logging statements can feel like unnecessary clutter in our code.
尽管上述实现看起来是一个标准的解决方案,日志语句在我们的代码中可能会感觉是不必要的杂乱无章。
Furthermore, we introduced additional complexity to our code. Without logging, we could rewrite this method as a one-liner:
此外,我们还增加了代码的复杂性。如果没有日志记录,我们可以将此方法重写为单行代码:
public String greet(String name) {
return String.format("Hello %s", name);
}
3. Aspect-Oriented Programming
3.面向方面的编程
As the name suggests, Aspect-Oriented Programming focuses on aspects rather than objects and classes. We use AOP to implement additional functionality for specific application parts without modifying their current implementations.
顾名思义,面向方面的编程侧重于方面,而不是对象和类。我们使用 AOP 为特定应用部分实现额外功能,而无需修改其当前实现。
3.1. AOP Concepts
3.1.AOP 概念
Before we dive in, let’s examine the basic AOP concepts at a very high level.
在深入学习之前,让我们先从高层次来了解一下 AOP 的基本概念。
- Aspect: The cross-cutting concern or the functionality we’d like to apply throughout the application.
- Join Point: The point of the application flow where we want to apply an aspect.
- Advice: The action that should be executed at a specific join point.
- Pointcut: Collection of join points where an aspect should be applied.
Furthermore, it’s worth noting that Spring AOP only supports join points for method execution. We should consider using compile-time libraries such as AspectJ to create aspects for fields, constructors, static initializers, etc.
此外,值得注意的是,Spring AOP 仅支持方法执行的连接点。我们应考虑使用编译时库(如AspectJ)为字段、构造函数、静态初始化器等创建方面。
3.2. Maven Dependency
3.2.Maven 依赖
To use Spring AOP, let’s add the spring-boot-starter-aop dependency in our pom.xml:
要使用 Spring AOP,让我们在 pom.xml 中添加 spring-boot-starter-aop 依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. Logging With AOP
4.使用 AOP 记录日志
One way to implement AOP in Spring is by using a Spring bean annotated with the @Aspect annotation:
在 Spring 中实现 AOP 的一种方法是使用带有 @Aspect 注解的 Spring Bean:
@Aspect
@Component
public class LoggingAspect {
}
The @Aspect annotation serves as a marker annotation, so Spring won’t automatically treat it as a component. To indicate it should be a bean managed by Spring and detected through component scanning, we also annotate the class with the @Component annotation.
@Aspect 注解用作标记注解,因此 Spring 不会自动将其视为组件。为了表明它应该是一个由 Spring 管理并通过组件扫描检测到的 Bean,我们还使用 @Component 注解对该类进行注解。
Next, let’s define a pointcut. Simply put, pointcuts allow us to specify which join point execution we want to intercept with an aspect:
接下来,让我们定义一个快捷方式。简单地说,点切允许我们指定要截取的连接点执行:
@Pointcut("execution(public * com.baeldung.logging.*.*(..))")
private void publicMethodsFromLoggingPackage() {
}
Here, we defined a pointcut expression that includes only public methods from the com.baeldung.logging package.
在此,我们定义了一个 pointcut 表达式,其中仅包含 com.baeldung.logging 软件包中的 public 方法。
Moving forward, let’s see how to define the advice to log the start and the end of the method execution.
接下来,让我们看看如何定义建议,以记录方法执行的开始和结束时间。
4.1. Using Around Advice
4.1.使用 Around 建议
We’ll start with the more general advice type – the Around advice. It allows us to implement custom behavior before and after the method invocation. Moreover, with this advice, we can decide whether to proceed with the specific join point, return a custom result, or throw an exception.
我们将从更通用的建议类型开始–环绕建议。它允许我们在方法调用前后实现自定义行为。此外,通过该建议,我们可以决定是否继续处理特定的连接点、返回自定义结果或抛出异常。
Let’s define the advice using the @Around annotation:
让我们使用 @Around 注解来定义建议:
@Around(value = "publicMethodsFromLoggingPackage()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
logger.debug(">> {}() - {}", methodName, Arrays.toString(args));
Object result = joinPoint.proceed();
logger.debug("<< {}() - {}", methodName, result);
return result;
}
The value attribute associates this Around advice with the previously defined pointcut. The advice runs around method executions matched by the publicMethodsFromLoggingPackage() pointcut signature.
value 属性将此 Around 建议与先前定义的快捷方式相关联。该建议围绕与 publicMethodsFromLoggingPackage() pointcut 签名匹配的方法执行。
The method accepts a ProceedingJoinPoint parameter. It’s a subclass of the JoinPoint class, allowing us to call the proceed() method to execute the next advice (if it exists) or the target method.
该方法接受一个 ProceedingJoinPoint 参数。它是 JoinPoint 类的子类,允许我们调用 proceed() 方法来执行下一个建议(如果存在)或目标方法。
We call the getArgs() method on the joinPoint to retrieve the array of method arguments. Additionally, we use the getSignature().getName() method to get the name of the method we’re intercepting.
我们调用 joinPoint 上的 getArgs() 方法来检索方法参数数组。此外,我们使用 getSignature().getName() 方法来获取我们要拦截的方法的名称。
Next, we call the proceed() method to execute the target method and retrieve the result.
接下来,我们调用 proceed() 方法来执行目标方法并获取结果。
Finally, let’s call the greet() method we mentioned earlier:
最后,让我们调用前面提到的 greet() 方法:
@Test
void givenName_whenGreet_thenReturnCorrectResult() {
String result = greetingService.greet("Baeldung");
assertNotNull(result);
assertEquals("Hello Baeldung", result);
}
After running our test, we can see the following result in our console:
运行测试后,我们可以在控制台中看到以下结果:
>> greet() - [Baeldung]
<< greet() - Hello Baeldung
5. Using the Least Invasive Advice
5.使用最小侵害性建议
When deciding which type of advice to use, it’s recommended that we use the least powerful advice that serves our needs. If we choose a general advice, such as Around advice, we’re more prone to potential errors and performance issues.
在决定使用哪种类型的建议时,我们建议使用功能最弱的建议来满足我们的需求。如果我们选择通用建议,例如 Around 建议,我们将更容易出现潜在错误和性能问题。
That’s to say, let’s examine how to accomplish the same functionality, but this time using the Before and After advices. Unlike Around advice, they don’t wrap the method execution, thus, there’s no need to explicitly call the proceed() method to continue with join point execution. Specifically, we use these types of advice to intercept methods right before or after execution.
也就是说,让我们来研究如何完成相同的功能,但这次使用的是 Before 和 After 建议。与 Around 建议不同的是,它们不封装方法的执行,因此无需显式调用 proceed() 方法来继续执行连接点。具体来说,我们使用这些类型的建议在执行之前或之后拦截方法。
5.1. Using Before Advice
5.1.使用之前建议
To intercept the method before its execution, we’ll create an advice using the @Before annotation:
为了在方法执行前拦截该方法,我们将使用 @Before 注解创建一个建议:
@Before(value = "publicMethodsFromLoggingPackage()")
public void logBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
logger.debug(">> {}() - {}", methodName, Arrays.toString(args));
}
Similar to the previous example, we used the getArgs() method to get method arguments and the getSignature().getName() method to get the method name.
与上一个示例类似,我们使用 getArgs() 方法获取方法参数,并使用 getSignature().getName() 方法获取方法名称。
5.2. Using AfterReturning Advice
5.2.使用 AfterReturning 建议
Going further, to add a log after the method execution, we’ll create the @AfterReturning advice that runs if a method execution completes without throwing any exception:
更进一步,为了在方法执行后添加日志,我们将创建 @AfterReturning 建议,该建议将在方法执行完成且未抛出任何异常时运行:
@AfterReturning(value = "publicMethodsFromLoggingPackage()", returning = "result")
public void logAfter(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
logger.debug("<< {}() - {}", methodName, result);
}
Here, we defined the returning attribute to get the value returned from the method. Additionally, the value we provided in the attribute should match the parameter’s name. The return value will be passed to the advice method as an argument.
在这里,我们定义了 returning 属性来获取方法返回的值。此外,我们在属性中提供的值应与参数的名称相匹配。返回值将作为参数传递给 advice 方法。
5.3. Using AfterThrowing Advice
5.3.使用 AfterThrowing 建议
On the other hand, to log situations when the method invocation completes with an exception, we could use the @AfterThrowing advice:
另一方面,如果要记录方法调用完成时出现异常的情况,我们可以使用 @AfterThrowing 建议:
@AfterThrowing(pointcut = "publicMethodsFromLoggingPackage()", throwing = "exception")
public void logException(JoinPoint joinPoint, Throwable exception) {
String methodName = joinPoint.getSignature().getName();
logger.error("<< {}() - {}", methodName, exception.getMessage());
}
This time, instead of the return value, we’ll get the thrown exception in our advice method.
这一次,我们将在建议方法中获得抛出的异常,而不是返回值。
6. Spring AOP Pitfalls
6.Spring AOP 陷阱
Lastly, let’s discuss some concerns we should consider when working with Spring AOP.
最后,让我们讨论一下在使用 Spring AOP 时应注意的一些问题。
Spring AOP is a proxy-based framework. It creates proxy objects to intercept method calls and apply logic defined in advice. This can negatively impact the performance of our application.
Spring AOP 是一个基于代理的框架。它创建代理对象来拦截方法调用并应用建议中定义的逻辑。这会对我们应用程序的性能产生负面影响。
To reduce the effect of AOP on performance, we should consider using AOP only when necessary. We should avoid creating aspects for isolated and infrequent operations.
为了减少 AOP 对性能的影响,我们应该考虑只在必要时使用 AOP。我们应避免为孤立和不频繁的操作创建方面。
Finally, if we use AOP for development purposes only, we can create it conditionally, for instance, only if a specific Spring profile is active.
最后,如果我们仅将 AOP 用于开发目的,我们可以有条件地创建它,例如,只有当特定的Spring配置文件处于活动状态时才创建。
7. Conclusion
7.结论
In this article, we learned how to perform logging using Spring AOP.
在本文中,我们学习了如何使用 Spring AOP 执行日志记录。
To sum up, we examined how to implement logging using Around advice as well as Before and After advice. We also explored why it’s important to use the least powerful advice to fit our needs. Finally, we addressed some potential issues Spring AOP brings to the table.
总之,我们研究了如何使用 Around 建议以及 Before 和 After 建议来实现日志记录。我们还探讨了为什么必须使用功能最弱的建议来满足我们的需求。最后,我们讨论了 Spring AOP 带来的一些潜在问题。
As always, the entire code examples can be found over on GitHub.
一如既往,您可以在 GitHub 上找到整个代码示例。