大家好!如果不理解基本概念,就很难深入研究构建功能的框架和方法。所以今天我们要讨论其中一个概念——AOP,即面向方面编程。这不是一个简单的话题,也不经常直接使用,但许多框架和技术都在幕后使用它。当然,有时在面试过程中,您可能会被要求笼统地告诉您这是什么动物以及它可以在哪里使用。那么我们来看看Java中AOP的基本概念和一些简单的例子。因此,AOP(面向方面的编程)是一种范式,旨在通过分离横切关注点来提高应用程序各个部分的模块化程度。为此,需要在现有代码中添加额外的行为,而无需更改原始代码。换句话说,我们似乎在方法和类之上挂了额外的功能,而不对修改后的代码进行修改。为什么这是必要的?我们迟早会得出这样的结论:通常的面向对象方法并不总能有效地解决某些问题。这时, AOP就派上用场了,它为我们提供了构建应用程序的额外工具。额外的工具意味着开发的灵活性增加,因此解决特定问题有更多选择。
AOP的应用
面向方面的编程旨在解决横切问题,横切问题可以是任何以不同方式重复多次的代码,这些代码不能完全结构化为单独的模块。因此,通过AOP,我们可以将其保留在主代码之外并垂直定义它。一个例子是在应用程序中应用安全策略。通常,安全性涉及应用程序的许多元素。此外,应用程序安全策略必须同等地应用于应用程序的所有现有部分和新部分。同时,所使用的安全策略本身也可以发展。这就是AOP可以派上用场的地方。另一个例子是日志记录。与手动插入日志记录相比,使用AOP方法进行日志记录有几个优点:- 日志代码很容易实现和删除:您只需要添加或删除某些方面的几个配置即可。
- 所有日志记录的源代码都存储在一个地方,无需手动查找所有使用的地方。
- 用于日志记录的代码可以添加到任何地方,无论是已经编写的方法和类还是新功能。这减少了开发人员错误的数量。
此外,当您从设计配置中删除某个方面时,您可以绝对确定所有跟踪代码都已删除并且没有丢失任何内容。 - 方面是可以反复重用和改进的独立代码。
AOP的基本概念
为了进一步分析这个话题,我们首先来了解一下AOP的主要概念。 建议是从连接点调用的附加逻辑、代码。该建议可以在连接点之前、之后或代替连接点执行(更多信息见下文)。可能的建议类型:- 之前(Before) ——这种类型的通知在目标方法执行之前启动——连接点。当使用方面作为类时,我们使用@Before注释来将建议类型标记为之前的类型。当使用方面作为.aj文件时,这将是before()方法。
- 之后(After) ——方法执行完成后执行的通知——连接点,无论是在正常情况下还是在抛出异常时。
当使用方面作为类时,我们可以使用@After注释来指示这是后面的提示。
当使用方面作为.aj文件时,这将是after()方法。 - 返回后- 仅当目标方法正常工作且没有错误时才会执行这些提示。
当方面表示为类时,我们可以使用@AfterReturning注释将建议标记为在成功完成后执行。
当使用方面作为 .aj 文件时,这将是after()方法返回 (Object obj) 。 - 抛出后- 此类建议适用于方法(即连接点)抛出异常的情况。我们可以使用此建议对失败的执行进行某些处理(例如,回滚整个事务或使用所需的跟踪级别进行日志记录)。
对于切面类,@AfterThrowing注解用于指示在抛出异常后使用此建议。当以.aj
文件 形式使用切面时,这将是方法 - after() 抛出(异常 e)。 - around可能是围绕方法(即连接点)的最重要的建议类型之一,例如,我们可以使用它来选择是否执行给定的连接点方法。
您可以编写在连接点方法执行之前和之后运行的建议代码。around通知的
职责包括调用连接点方法以及如果该方法返回某些内容则返回值。也就是说,在本技巧中,您可以简单地模仿目标方法的操作而不调用它,并返回您自己的结果作为结果。 对于类形式的方面,我们使用@Around注释来创建包装连接点的提示。当使用方面作为.aj文件时,这将是around()方法。
- 编译时织入- 如果您有切面的源代码以及使用切面的代码,则可以直接使用 AspectJ 编译器编译源代码和切面;
- 编译后编织(二进制编织) - 如果您不能或不想使用源代码转换将方面编织到代码中,您可以采用已编译的类或 jar 并注入方面;
- 加载时编织只是二进制编织,延迟到类加载器加载类文件并为 JVM 定义类为止。
为了支持这一点,需要一个或多个“weave 类加载器”。它们要么由运行时显式提供,要么由“编织代理”激活。
Java 中的示例
接下来,为了更好地理解AOP,我们将看一下 Hello World 级别的小例子。让我立即指出,在我们的示例中,我们将使用编译时编织。首先,我们需要将以下依赖项添加到pom.xml中:<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
通常,使用特殊的Ajs编译器来使用方面。IntelliJ IDEA默认情况下没有它,因此在选择它作为应用程序编译器时,您需要指定AspectJ发行版的路径。您可以在本页阅读更多有关选择Ajs作为编译器的方法。这是第一种方法,第二种方法(我使用的)是将以下插件添加到pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
之后,建议从Maven重新导入并运行mvn cleancompile。现在让我们继续看示例。
示例1
让我们创建一个Main类。在其中我们将有一个启动点和一个在控制台中打印传递给它的名称的方法:public class Main {
public static void main(String[] args) {
printName("Толя");
printName("Вова");
printName("Sasha");
}
public static void printName(String name) {
System.out.println(name);
}
}
没什么复杂的:他们传递了名称并将其显示在控制台中。如果我们现在运行它,控制台将显示:
托利亚·沃瓦·萨莎
好吧,是时候利用 AOP 的力量了。现在我们需要创建一个文件方面。它们有两种类型:第一种是扩展名为.aj的文件,第二种是使用注释实现AOP功能的常规类。我们首先看一个扩展名为.aj的文件:
public aspect GreetingAspect {
pointcut greeting() : execution(* Main.printName(..));
before() : greeting() {
System.out.print("Привет ");
}
}
这个文件有点类似于一个类。让我们弄清楚这里发生了什么: 切入点- 一个切点或一组连接点; greeting() — 该切片的名称; : 执行- 执行时* - 全部,调用 - Main.printName(..) - 这个方法。接下来是具体的建议 - before() - 在调用目标方法之前执行:greeting() - 该建议所反应的切片,下面我们看到方法本身的主体,它是用 Java 编写的我们理解的语言。当我们在存在此方面的情况下运行main时,我们将在控制台中得到以下输出:
你好托利亚你好沃瓦你好萨莎
我们可以看到对printName方法的每次调用都被一个方面修改了。现在让我们看看切面是什么样子,但它是一个带有注释的 Java 类:
@Aspect
public class GreetingAspect{
@Pointcut("execution(* Main.printName(String))")
public void greeting() {
}
@Before("greeting()")
public void beforeAdvice() {
System.out.print("Привет ");
}
}
在.aj方面文件 之后,一切都更加明显:
- @Aspect表示给定的类是一个方面; @Pointcut("execution(* Main.printName(String))")是一个切入点,在所有使用String类型的传入参数对Main.printName的调用上触发;
- @Before("greeting()") - 在调用在greeting()切入点描述的代码之前应用的建议。
你好托利亚你好沃瓦你好萨莎
例子2
假设我们有一些为客户端执行某些操作的方法,并从main调用此方法:public class Main {
public static void main(String[] args) {
makeSomeOperation("Толя");
}
public static void makeSomeOperation(String clientName) {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
}
}
使用@Around注释,让我们做一些类似“伪交易”的事情:
@Aspect
public class TransactionAspect{
@Pointcut("execution(* Main.makeSomeOperation(String))")
public void executeOperation() {
}
@Around(value = "executeOperation()")
public void beforeAdvice(ProceedingJoinPoint joinPoint) {
System.out.println("Открытие транзакции...");
try {
joinPoint.proceed();
System.out.println("Закрытие транзакции....");
}
catch (Throwable throwable) {
System.out.println("Операция не удалась, откат транзакции...");
}
}
}
使用ProceedingJoinPoint对象的proceed方法,我们调用包装器的方法来确定它在棋盘中的位置,并相应地调用上面方法中的代码joinPoint.proceed(); - 这是之前,下面是 -之后。如果我们运行main我们将进入控制台:
打开交易...为客户端执行一些操作 - Tolya 关闭交易....
如果我们向我们的方法添加一个异常抛出(突然操作失败):
public static void makeSomeOperation(String clientName)throws Exception {
System.out.println("Выполнение некоторых операций для клиента - " + clientName);
throw new Exception();
}
然后我们将在控制台中得到输出:
正在打开事务...正在为客户端执行一些操作 - Tolya 操作失败,事务已回滚...
原来是对失败的伪处理。
例子3
作为下一个示例,让我们执行一些类似登录控制台的操作。首先,让我们看看Main,我们的伪业务逻辑发生在其中:public class Main {
private String value;
public static void main(String[] args) throws Exception {
Main main = new Main();
main.setValue("<некоторое meaning>");
String valueForCheck = main.getValue();
main.checkValue(valueForCheck);
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public void checkValue(String value) throws Exception {
if (value.length() > 10) {
throw new Exception();
}
}
}
在main中,使用setValue我们将设置内部变量 value 的值,然后使用getValue我们将获取该值,在checkValue中我们将检查该值是否超过 10 个字符。如果是,则会抛出异常。现在让我们看一下记录方法操作的方面:
@Aspect
public class LogAspect {
@Pointcut("execution(* *(..))")
public void methodExecuting() {
}
@AfterReturning(value = "methodExecuting()", returning = "returningValue")
public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
if (returningValue != null) {
System.out.printf("Успешно выполнен метод - %s, класса- %s, с результатом выполнения - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
returningValue);
}
else {
System.out.printf("Успешно выполнен метод - %s, класса- %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName());
}
}
@AfterThrowing(value = "methodExecuting()", throwing = "exception")
public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
System.out.printf("Метод - %s, класса- %s, был аварийно завершен с исключением - %s\n",
joinPoint.getSignature().getName(),
joinPoint.getSourceLocation().getWithinType().getName(),
exception);
}
}
这里发生了什么? @Pointcut("execution(* *(..))") - 将连接到对所有方法的所有调用; @AfterReturning(value = "methodExecuting()", returned = "returningValue") - 将在目标方法成功完成后执行的建议。我们这里有两个案例:
- 当方法有返回值时if (returningValue != null) {
- 当没有返回值时else {
Main 类的 setValue 方法执行成功 Main 类的 getValue 方法执行成功,执行结果 <some value> Main 类的 checkValue 方法执行成功,因异常异常终止 - java.lang.Exception 方法 - main、class-Main、因异常崩溃 - java.lang.Exception
好吧,由于我们没有处理异常,所以我们还将获得它的堆栈跟踪:您可以在以下文章中阅读有关异常及其处理的信息:Java 中的异常和异常及其处理。这就是我今天的全部内容。今天我们认识了AOP,你会发现这个野兽并不像它所描绘的那么可怕。 再见了,大家!
GO TO FULL VERSION