面向切面编程(Aspect Oriented Programming, AOP)的概念非常简单,它把系统的一些非业务功能(如日志记录、性能监控、事务管理等)从业务代码中剥离出来定义成切面,再把这些切面作为对业务代码的增强。降低了非业务功能对业务代码的侵入,进一步降低了两者的耦合程度。
如按照图1的普通做法,在方法1~方法3中,都添加了权限验证和日志记录的非业务功能。但这些非业务功能在不同位置的做法其实是一样的,有重复嫌疑;另外,这些非业务功能代码遍布整个业务代码,与核心业务代码的联系过于紧密。
图2采用AOP编程的方法,将权限验证和日志记录的功能单独拎出来,做成两个切面,然后通过框架机制将其指定为原代码的增强代码。这样的话,从代码实现上显得更加清晰。
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("-----------目标方法执行完毕-----------"); } }
|
- 指定对
MailServiceImpl
的Send()
方法进行增强
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() {}
|
下面表达式只会映射到MailServiceImpl2
的Send2
方法:
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.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技术基于代理模式实现,由框架或库为开发者创建代理类,开发者只需要为框架或库指明增强对象和增强行为即可。
在编程开发中,很多看似高大上的技术,其实都依赖某些巧妙的设计模式实现,设计模式的重要性可见一斑!