CodeGym/Java 博客/随机的/什么是 AOP?面向方面编程的原则
John Squirrels
第 41 级
San Francisco

什么是 AOP?面向方面编程的原则

已在 随机的 群组中发布
个会员
嗨,伙计们和姑娘们!如果不了解基本概念,就很难深入研究构建功能的框架和方法。所以今天我们将讨论这样一个概念——AOP,又名面向方面的编程什么是 AOP? 面向方面编程的原则 - 1这个主题并不容易,也很少直接使用,但许多框架和技术在幕后使用它。当然,有时在采访中,您可能会被要求笼统地描述这是什么样的野兽以及它可以应用到哪里。那么我们就来看看Java中AOP的基本概念和一些简单的例子。那么,AOP代表面向方面的编程,这是一种旨在通过分离横切关注点来增加应用程序不同部分的模块化的范例。为实现这一点,在不对原始代码进行更改的情况下,将额外的行为添加到现有代码中。换句话说,我们可以将其视为在不更改修改后的代码的情况下,将附加功能附加到方法和类之上。为什么这是必要的?迟早,我们会得出结论,典型的面向对象方法不能总是有效地解决某些问题。当那一刻到来时,AOP来拯救我们并为我们提供额外的工具来构建应用程序。额外的工具意味着软件开发的灵活性增加,这意味着解决特定问题的更多选择。

应用AOP

面向切面编程旨在执行横切任务,可以是任何可能通过不同方法重复多次的代码,不能完全结构化到一个单独的模块中。因此,AOP允许我们将它保留在主代码之外并垂直声明它。一个例子是在应用程序中使用安全策略。通常,安全贯穿应用程序的许多元素。更重要的是,应用程序的安全策略应该同样适用于应用程序的所有现有部分和新部分。同时,正在使用的安全策略本身可以发展。这是使用AOP 的最佳场所。另外,另一个例子是记录. 使用 AOP 方法进行日志记录而不是手动添加日志记录功能有几个优点:
  1. 日志记录的代码很容易添加和删除:您需要做的就是添加或删除某些方面的一些配置。

  2. 所有用于日志记录的源代码都保存在一个地方,因此您无需手动搜索所有使用它的地方。

  3. 可以在任何地方添加日志记录代码,无论是在已经编写的方法和类中还是在新功能中。这减少了编码错误的数量。

    此外,当从设计配置中删除一个方面时,您可以确定所有跟踪代码都已消失并且没有遗漏任何内容。

  4. 方面是独立的代码,可以改进并一次又一次地使用。
什么是 AOP? 面向方面编程的原则 - 2AOP 还用于异常处理、缓存和提取某些功能以使其可重用。

AOP的基本原理

为了进一步探讨这个主题,让我们首先了解一下 AOP 的主要概念。 建议——从连接点调用的附加逻辑或代码。建议可以在连接点之前、之后或代替连接点执行(下面将详细介绍)。可能的建议类型
  1. 之前——这种类型的建议在目标方法(即连接点)执行之前启动。将方面用作类时,我们使用@Before注释将通知标记为之前出现。将方面用作.aj文件时,这将是before()方法。

  2. After — 在方法(连接点)执行完成后执行的建议,无论是在正常执行还是在抛出异常时。

    将方面用作类时,我们可以使用@After注释来指示这是之后的通知。

    将方面用作.aj文件时,这是after()方法。

  3. 返回后——只有当目标方法正常完成且没有错误时才执行此建议。

    当方面表示为类时,我们可以使用@AfterReturning批注将通知标记为在成功完成后执行。

    将方面用作.aj文件时,这将是after() 返回 (Object obj)方法。

  4. 抛出后——此建议适用于方法(即连接点)抛出异常的情况。我们可以使用此建议来处理某些类型的失败执行(例如,回滚整个事务或具有所需跟踪级别的日志)。

    对于类方面,@AfterThrowing注释用于指示在抛出异常后使用此建议。

    将方面用作.aj文件时,这将是after() throwing (Exception e)方法。

  5. 周围——也许是最重要的建议类型之一。它包含一个方法,即一个连接点,我们可以使用它来选择是否执行给定的连接点方法。

    您可以编写在执行连接点方法之前和之后运行的建议代码。

    around advice负责调用连接点方法和返回值(如果该方法有返回值)。换句话说,在这个通知中,你可以简单地模拟目标方法的运行而不调用它,并返回你想要的任何结果作为返回结果。

    给定方面作为类,我们使用@Around注释来创建包装连接点的建议。当使用.aj文件形式的方面时,此方法将是around()方法。

连接点——运行程序(即方法调用、对象创建、变量访问)中应该应用建议的点。换句话说,这是一种用于查找代码注入位置(应该应用建议的地方)的正则表达式。 切入点——一组连接点。切入点确定给定的建议是否适用于给定的连接点。 方面——实现横切功能的模块或类。切面通过在某些切入点定义的连接点应用建议来更改其余代码的行为。换句话说,它是建议和连接点的组合。 介绍— 更改类的结构和/或更改继承层次结构以将方面的功能添加到外部代码。 目标— 建议将应用到的对象。 编织——将方面链接到其他对象以创建建议代理对象的过程。这可以在编译时、加载时或运行时完成。织法分为三种:
  • 编译时编织——如果你有方面的源代码和使用方面的代码,那么你可以直接使用 AspectJ 编译器编译源代码和方面;

  • 编译后编织(二进制编织) ——如果您不能或不想使用源代码转换将方面编织到代码中,您可以采用先前编译的类或 jar 文件并将方面注入其中;

  • 加载时编织——这只是二进制编织,延迟到类加载器加载类文件并为 JVM 定义类。

    需要一个或多个编织类加载器来支持这一点。它们要么由运行时显式提供,要么由“编织代理”激活。

AspectJ — AOP范例的特定实现,它实现了执行横切任务的能力。文档可以在这里找到。

Java 中的示例

接下来,为了更好地理解AOP,我们将查看“Hello World”式的小示例。首先,我会注意到我们的示例将使用编译时织入首先,我们需要在pom.xml文件中添加以下依赖项:
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
通常,特殊的ajc编译器是我们使用方面的方式。IntelliJ IDEA默认不包含它,因此在选择它作为应用程序编译器时,您必须指定 5168 75 AspectJ分发的路径。这是第一种方式。第二种,也就是我使用的,是在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 clean compile。现在让我们直接进入示例。

示例 1

让我们创建一个类。在其中,我们将有一个入口点和一个在控制台上打印传递名称的方法:
public class Main {

  public static void main(String[] args) {
  printName("Tanner");
  printName("Victor");
  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("Hi, ");
  }
}
这个文件有点像一个类。让我们看看这里发生了什么: 切入点是一组连接点; greeting()是这个切入点的名称; : execution指示在执行Main.printName(...)方法的所有 ( * )调用期间应用它。接下来是一个特定的建议 — before() — 在调用目标方法之前执行。: greeting()是这个通知响应的切点。好吧,下面我们看到了方法本身的主体,它是用我们理解的 Java 语言编写的。当我们在这个方面存在的情况下运行main时,我们将得到这个控制台输出:
嗨,Tanner 嗨,Victor Hi,Sasha
我们可以看到,由于一个方面,对printName方法的 每次调用都已被修改。现在让我们看一下作为带有注解的 Java 类的切面是什么样的:
@Aspect
public class GreetingAspect{

  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }

  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
在.aj方面文件 之后,这里的一切都变得更加明显:
  • @Aspect表示这个类是一个切面;
  • @Pointcut("execution(* Main.printName(String))")是所有调用Main.printName时触发的切点,输入参数类型为String
  • @Before("greeting()")是在调用greeting()切点中指定的代码之前应用的建议。
使用此方面运行main不会更改控制台输出:
嗨,Tanner 嗨,Victor Hi,Sasha

示例 2

假设我们有一些方法可以为客户端执行一些操作,我们从main调用这个方法:
public class Main {

  public static void main(String[] args) {
  performSomeOperation("Tanner");
  }

  public static void performSomeOperation(String clientName) {
     System.out.println("Performing some operations for Client " + clientName);
  }
}
让我们使用@Around注解来创建一个“伪事务”:
@Aspect
public class TransactionAspect{

  @Pointcut("execution(* Main.performSomeOperation(String))")
  public void executeOperation() {
  }

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Opening a transaction...");
     try {
        joinPoint.proceed();
        System.out.println("Closing a transaction...");
     }
     catch (Throwable throwable) {
        System.out.println("The operation failed. Rolling back the transaction...");
     }
  }
  }
通过ProceedingJoinPoint对象的proceed方法,我们调用 wrapping 方法来确定它在通知中的位置。因此,上述方法中的代码joinPoint.proceed(); Before,而下面的代码是After。如果我们运行main,我们会在控制台中看到:
开启交易... 为客户 Tanner 执行一些操作 关闭交易...
但是如果我们在我们的方法中抛出异常(模拟失败的操作):
public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
然后我们得到这个控制台输出:
正在打开交易... 正在为客户 Tanner 执行一些操作操作失败。回滚交易...
所以我们最终得到的是一种错误处理能力。

例 3

在我们的下一个示例中,让我们做一些类似登录到控制台的事情。首先,看一下Main,我们在其中添加了一些伪业务逻辑:
public class Main {
  private String value;

  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<some value>");
     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("Successful execution: method — %s method, class — %s class, return value — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Successful execution: method — %s, class — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }

  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Exception thrown: method — %s, class — %s, exception — %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
这里发生了什么? @Pointcut("execution(* *(..))")将加入所有方法的所有调用。 @AfterReturning(value = "methodExecuting()", returning = "returningValue")是将在成功执行目标方法后执行的通知。我们这里有两种情况:
  1. 当方法有返回值时——if (returningValue!= Null) {
  2. 当没有返回值时——else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception")是在发生错误时触发的通知,即当方法抛出异常时。因此,通过运行main,我们将获得一种基于控制台的日志记录:
执行成功:method--setValue,class--Main 执行成功:method--getValue,class--Main,返回值--<some value> 抛出的异常:method--checkValue,class--Main 异常--java.lang.Exception 抛出的异常:method-- main, class — Main, exception — java.lang.Exception
由于我们没有处理异常,我们仍然会得到堆栈跟踪:什么是 AOP? 面向方面编程的原则 - 3您可以在这些文章中阅读有关异常和异常处理的内容:Java 中的异常异常:捕获和处理。今天对我来说就这些了。今天我们认识了AOP,你可以看到这只野兽并不像某些人想象的那么可怕。再见了,大家!
评论
  • 受欢迎
你必须先登录才能发表评论
此页面还没有任何评论