AOP(Aspect-Oriented Programming) 即面向方面编程. 它是一种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想. 用于切入到指定类指定方法的代码片段叫做切面, 而切入到哪些类中的哪些方法叫做切入点.
AOP是OOP的有益补充,OOP从横向上区分出了一个个类,AOP则从纵向上向指定类的指定方法中动态地切入代码. 它使OOP变得更加立体.
Java中的动态代理或CGLib就是AOP的体现.
1. 为什么使用AOP
-
AOP
最为典型的应用实际就是数据库事务的管控。 -
AOP
还可以减少大量重复的工具。
使用Spring AOP可以处理一些无法使用OOP实现的业务逻辑。其实,通过约定,可以将一些业务逻辑织入流程中,并且可以将一些通用的逻辑抽取出来,然后给予默认实现,这样你就只需要完成部分的功能就可以了。
2. AOP术语
-
切面(aspect):是一个可以定义切点、各类通知和引入的内容。
-
连接点(join point):对应的是具体被拦截的对象,因为Spring只支持芳芳,所以被拦截的对象往往就是指特定的方法。
-
切点(point cut):有时候,我们的切面不单单应用于单个方法,也可以是多个类的不同方法,这时,可以通过正则表达式和指示器的规则去定义。
-
通知(advice):按照约定的流程防范,在连接点执⾏行行的动作,如:前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会更具约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。
-
目标对象(target):即被代理对象。
-
AOP代理对象(AOP proxy):AOP 代理理对象,可以是 JDK 动态代理理,也可以是 CGLIB 代理理
-
引入(introduction):是指引入新的类和方法,增强现有Bean的功能。
-
织入(weaving):它是一个通过动态代理技术,为原有服务对象生成动态对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
3. 切点详解
匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(* com.spring.service.*.*(..))来决定。
切入点表达式:
-
execution:用于匹配方法执行的连接点;
-
within:用于匹配指定类型内的方法执行;
-
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;注意this中使用的表达式必须是完整类名,不支持通配符;
-
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是完整类名,不支持通配符;
-
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数类型列表中的参数必须是完整类名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
-
@within:用于匹配所以持有指定注解类型内的方法;注解类型也必须是完整类名;
-
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是完整类名;
-
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是完整类名;
-
@annotation:用于匹配当前执行方法持有指定注解的方法;注解类型也必须是完整类名;
-
bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
-
reference pointcut:表示引用其他命名切入点,只有注解风格支持,XML风格不支持。
4. AOP开发详解
本文根据Spring Boot建议注解开发的特点,这里只讨论@Aspect注解方式实现AOP。
4.1 代码清单1: 用户服务
@Service
public class UserService {
public void sayHello(String name) {
System.out.println("Hello " + name);
}
}
4.2 代码清单2: 定义切面
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(public * me.flygopher.springboot2.user.UserService.sayHello(..))")
public void pointCut() {}
@Before("pointCut()")
public void before() {
System.out.println("MyAspect before ...");
}
@After("pointCut()")
public void after() {
System.out.println("MyAspect after ...");
}
@AfterReturning("pointCut()")
public void afterReturning() {
System.out.println("MyAspect after returning ...");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("MyAspect after throwing ...");
}
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("MyAspect around before ...");
jp.proceed();
System.out.println("MyAspect around after ...");
}
}
1) 实现AOP的切面主要有以下几个要素:
- 使用
@Aspect
注解将一个java类定义为切面类 - 使用
@Pointcut
定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。 - 根据需要在切入点不同位置的切入内容
- 使用
@Before
在切入点开始处切入内容 - 使用
@After
在切入点结尾处切入内容 - 使用
@AfterReturning
在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理) - 使用
@Around
在切入点前后切入内容,并自己控制何时执行切入点自身的内容 - 使用
@AfterThrowing
用来处理当切入内容部分抛出异常之后的处理逻辑
- 使用
2) 举一个复杂一点的execution例子:
@Pointcut("execution(public * me.flygopher.springboot2..*Controller.*(..))")
简单解释下:
”*“表示任意字符串
包名后面的”..“ 表示当前包及子包,
.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
除了使用execution
还有一种另一种用法就是@annotation
。
4.3 代码清单3: 测试方法
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void shouldPrintAspectInfoWhenCallSayHelloMethod() {
userService.sayHello("liangbo");
}
}
运行后的结果如下:
MyAspect around before ...
MyAspect before ...
Hello liangbo
MyAspect around after ...
MyAspect after ...
MyAspect after returning ...
需要注意下around环绕通知,它的结果并不是我期待的结果,我期待的结果应该如下:
MyAspect before ...
MyAspect around before ...
Hello liangbo
MyAspect around after ...
MyAspect after ...
MyAspect after returning ...
4.4 代码清单4: 引入多个切面
@Aspect
@Component
@Order(-1)
public class MyAspect2 {
@Pointcut("execution(public * me.flygopher.springboot2.user.UserService.sayHello(..))")
public void pointCut() {}
@Before("pointCut()")
public void before() {
System.out.println("MyAspect2 before ...");
}
@After("pointCut()")
public void after() {
System.out.println("MyAspect2 after ...");
}
@AfterReturning("pointCut()")
public void afterReturning() {
System.out.println("MyAspect2 after returning ...");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("MyAspect2 after throwing ...");
}
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("MyAspect2 around before ...");
jp.proceed();
System.out.println("MyAspect2 around after ...");
}
}
测试结果如下:
MyAspect2 around before ...
MyAspect2 before ...
MyAspect around before ...
MyAspect before ...
Hello liangbo
MyAspect around after ...
MyAspect after ...
MyAspect after returning ...
MyAspect2 around after ...
MyAspect2 after ...
MyAspect2 after returning ...
注意可以使用注解@Order定义切面执行的顺序,数值越小,before先执行,after后执行。
5. 高级:AOP统一处理Web请求日志
实现如下:
@Aspect
@Component
@Slf4j
public class WebLogAspect {
private ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * me.flygopher.springboot2..*Controller.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 接收到请求,记录请求内容
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
log.info("URL: {}, HTTP_METHOD: {}, ARGS: {}", request.getRequestURI(), request.getMethod(), Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(pointcut = "webLog()")
public void doAfterReturning() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
log.info("URL:{}, HTTP_METHOD:{}, Costs: {} ms \n",
request.getRequestURI(),
request.getMethod(),
System.currentTimeMillis() - startTime.get());
}
}
「真诚赞赏,手留余香」
请我喝杯咖啡?
使用微信扫描二维码完成支付
