面向切面编程(AOP)

面向切面编程(Aspect Oriented Programming, AOP)的概念非常简单,它把系统的一些非业务功能(如日志记录、性能监控、事务管理等)从业务代码中剥离出来定义成切面,再把这些切面作为对业务代码的增强。降低了非业务功能对业务代码的侵入,进一步降低了两者的耦合程度。

如按照图1的普通做法,在方法1~方法3中,都添加了权限验证和日志记录的非业务功能。但这些非业务功能在不同位置的做法其实是一样的,有重复嫌疑;另外,这些非业务功能代码遍布整个业务代码,与核心业务代码的联系过于紧密。

图1:非业务功能侵入业务代码

图2采用AOP编程的方法,将权限验证和日志记录的功能单独拎出来,做成两个切面,然后通过框架机制将其指定为原代码的增强代码。这样的话,从代码实现上显得更加清晰。

图2:非业务功能作为切面对业务代码进行增强

Java的Spring框架对AOP有非常棒的支持,而且使用起来非常流畅;.NET框架也有相应的AOP库可供使用,但从使用体验上远不及Spring框架。

Spring的AOP

使用流程

  • 添加相应的包
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 自定义一个注解,用于指明需要增强的目标方法,也可以通过方法路径来指定
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.testdi;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LHAnnotation {
}
  • 核心:新建一个切面类,用@Aspect指定,定义切面功能逻辑,且必须将其指定为Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.testdi;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
@Pointcut("@annotation(com.example.testdi.LHAnnotation)")
private void AOPTest() {}

@Around("AOPTest()")
public void AOPTestMethod1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("-----------目标方法准备执行-----------");
joinPoint.proceed();
System.out.println("-----------目标方法执行完毕-----------");
}
}
  • 指定对MailServiceImplSend()方法进行增强
1
2
3
4
5
6
7
8
9
10
11
package com.example.testdi;

import org.springframework.stereotype.Component;

@Component
public class MailServiceImpl {
@LHAnnotation
public void Send(String title, String to, String body) {
System.out.println("发邮件了: " + title +","+ to +","+ body);
}
}
  • 需要为主类指定@EnableAspectJAutoProxy,表明开启切面功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.testdi;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = "com.example.testdi")
@EnableAspectJAutoProxy
public class DemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoApplication.class);
var greeting = context.getBean(MailServiceImpl.class);
greeting.Send("第一封邮件", "LiRuohan", "你好,依赖注入!");
context.close();
}
}

切面定义

  • 定义切点:目的是告诉程序要对哪些方法进行增强

    (1)注解方式:这种比较简单,上面的例子就是

    (2)表达式方法:适合批量指定,非常灵活

    下面做个小实验,定义两个邮件发送服务类,看不同的表达式会映射到哪些方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Component
    public class MailServiceImpl {
    public void Send(String text1) {
    System.out.println("MailServiceImpl: "+text1);
    }
    public void Send2(String text1, String text2) {
    System.out.println("MailServiceImpl: " +text1 + "\t" + text2);
    }
    }
    @Component
    public class MailServiceImpl2 {
    public void Send(String text1) {
    System.out.println("MailServiceImpl2: " + text1);
    }
    public void Send2(String text1, String text2) {
    System.out.println("MailServiceImpl2: " + text1 + "\t" + text2);
    }
    }

    public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestaopApplication.class);
    var greeting = context.getBean(MailServiceImpl.class);
    greeting.Send("Hello");
    greeting.Send2("Hello","World");
    var greeting2 = context.getBean(MailServiceImpl2.class);
    greeting2.Send("Hello");
    greeting2.Send2("Hello","World");
    context.close();
    }

    下面表达式会映射到所有的Send方法:

    1
    2
    @Pointcut("execution (* com.example.testaop.*.*(..))")
    private void AOPTest() {}

    下面表达式只会映射到MailServiceImpl2的两个Send方法:

    1
    2
    @Pointcut("execution (* com.example.testaop.MailServiceImpl2.*(..))")
    private void AOPTest() {}

    下面表达式只会映射到MailServiceImpl2Send2方法:

    1
    2
    @Pointcut("execution (* com.example.testaop.MailServiceImpl2.Send2(..))")
    private void AOPTest() {}

    下面表达式只会映射到两个类的Send方法(仅一个参数):

    1
    2
    @Pointcut("execution (* com.example.testaop.*.*(java.lang.String))")
    private void AOPTest() {}

  • 定义通知

    • @Before:方法执行之前
    • @After:方法执行之后
    • @Around:同时定义方法执行之前和执行之后的逻辑
    • @AfterReturning:在目标方法成功执行,不抛出异常后执行,与@AfterThrowing互补,与@After有别
    • @AfterThrowing:抛出异常后的执行逻辑
  • 应用多个切面

可使用@Order()为切面类指定序号,序号小者先执行。

.NET的AOP

目前来看,.NET中免费且与Spring AOP用法较为接近的库是AspectInjector,其用法非常简单,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[Aspect(Scope.Global)]
[Injection(typeof(LogCall))]
public class LogCall : Attribute {
//[Advice(Kind.Before)]
//public void LogEnter([Argument(Source.Name)] string name) {
// Console.WriteLine($"Calling '{name}' method...");
//}

//[Advice(Kind.After)]
//public void LogExit([Argument(Source.Name)] string name) {
// Console.WriteLine($"Finished calling '{name}' method...");
//}

[Advice(Kind.Around)]
public Object Injection(
[Argument(Source.Type)] Type type,
[Argument(Source.Name)] string name,
[Argument(Source.Target)] Func<object[], object> target,
[Argument(Source.Arguments)] object[] arg) {
Console.WriteLine($"Calling '{name}' method...");
// 目标方法执行
var res = target(arg);
Console.WriteLine($"Finished calling '{name}' method...");
return res;
}
}
1
2
3
4
5
6
7
public class LogCallTest {
[LogCall]
public void Calculate(string text) {
Console.WriteLine(text);
Console.WriteLine("Calculated");
}
}
1
2
3
4
5
static void Main(string[] args) {
LogCallTest test = new LogCallTest();
test.Calculate("李浩测试");
Console.ReadKey();
}

总结

学习AOP技术,我认为对这种编程思想的理解更为重要,依托具体框架掌握其使用流程可以帮助我们更直观理解。

AOP技术基于代理模式实现,由框架或库为开发者创建代理类,开发者只需要为框架或库指明增强对象和增强行为即可。

在编程开发中,很多看似高大上的技术,其实都依赖某些巧妙的设计模式实现,设计模式的重要性可见一斑!

评论