AOP简介
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。可通过运行期动态代理实现程序功能的统一维护的一种技术。
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。利用 AOP可以对业务逻辑的各个部分进行隔离,从而
使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样会使主业务逻辑变的混杂不清。
例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但它
们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。
面向切面编程有什么好处?
- 减少代码重复
- 专注于业务代码实现
tips: 面向切面编程只是面向对象编程的一种补充。
AOP编程相关术语
切面(aspect)
切面泛指交叉业务逻辑,上图中的日志处理、模块处理可以理解为切面。常用的切面是通知(Advice),实际就对主业务逻辑的一种增强。
连接点(JoinPoint)
连接点指的是可以被切面织入的具体方法,通常是业务接口中的方法。
切入点(Pointcut)
切入点指的一个或多个连接点的集合,通过切入点指定一组方法。
tips: 被标记为final的方法是不能作为切入点与连接点的,因为最终的是不能被修改的,也就不能被增强。
目标对象(Target)
目标对象指的是将要被增强的对象,即是包含主业务逻辑类的对象。
通知(Advice)
通知表示切入的执行时间,换个角度来说,通知定义了增强代码切入到目标代码的时间点,比如是在目标方法之前执行还是之后执行。
通知的类型不同,执行的时间点不同。
tips: 切入点定义了切入的位置, 通知定义了切入的时间。
AspectJ对AOP的实现
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。
AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己
的框架中。在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。
AspectJ中的通知类型
AspectJ 中常用的通知有五种类型:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
切入点表达式
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)
说明:
modifiers-pattern? :访问权限类型
ret-type-pattern :返回值类型
declaring-type-pattern? : 包名类名
name-pattern(param-pattern): 方法名(参数列表)
throws-pattern? :抛出异常类型
tips: 其中带?的表示可选部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名,表示中使用的符号:
*
: 表示0至多个任意字符
..
: 用在方法参数中,表示任意多个参数。用在包名后表示当前包及其子包的路径。
+
: 用在类名后,表示当前类及其子类。用在接口表示当前接口及其实现类。
例:
指定切入点为:任意公共方法。
execution(public * *(..))
指定切入点为:任何一个以“set” 开头的方法。
execution(* set*(..))
指定切入点为: service包中任意类的任意方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:service包及其子包中任意类的任意方法。
tips:“…”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* com.xyz.service..*.*(..))
指定的切入点为: 任意包下的service子包中任意类的任意方法。
execution(* *..service.*.*(..))
需要导入的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
AspectJ基于注解实现AOP
一、基本实现步骤
①: 定义业务接口个实现类
public interface someService {
void doSome(String name, Integer age);
}
@Service
public class someServiceImpl implements someService {
@Override
public void doSome(String name, Integer age) {
System.out.println("执行doSome方法");
}
}
②: 定义切面类
@Aspect注解,表示当前类是切面类,类中的方法作为通知方法,方法中定义的是具体的增强功能。
@Before注解,表示这个是前置通知, value属性定义切入点表达式,表示切面要执行的位置。
@Aspect
@Component
public class MyAspect {
@Before(value = "execution(* cn.xyz.aop.service.someServiceImpl.doSome(..))")
public void myBefore(){
System.out.println("前置通知: 在目标方法之前执行。");
}
}
③: 在测试环境中进行测试
@SpringBootTest
class SpringAopTestApplicationTests {
@Autowired
private SomeService someService;
@Test
void contextLoads() {
someService.doSome("张山", 20);
}
}
④: 测试结果
前置通知: 在目标方法之前执行。
执行doSome方法
二、@Before前置通知,方法由JoinPoint参数
在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数,该类型的对象本身就是切入点表达式。
通过该参数,可获取切入点表达式、方法签名、目标对象等。
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
在切面类中添加通知方法:
@Before(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doSome2(..))")
public void myBefore2(JoinPoint joinPoint){
System.out.println("连接点的方法定义:"+joinPoint.getSignature());
System.out.println("连接点方法参数个数:"+joinPoint.getArgs().length);
//方法参数信息
for (Object arg : joinPoint.getArgs()) {
System.out.println("参数:"+arg);
}
System.out.println("前置通知:在目标方法之前执行。");
}
测试打印的结果:
连接点的方法定义:void cn.xyz.aop.service.SomeServiceImpl.doSome2(String,Integer)
连接点方法参数个数:2
参数:张山
参数:20
前置通知:在目标方法之前执行。
执行doSome2方法
三、@AfterReturning 后置通知-注解有returning属性
在目标方法之后执行,由于是目标方法之后执行,所以可以获取到目标方法的返回值。
该注解的 returning 属性就是用于指定接收方法返回值的变量名的。
所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。
tips:该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
①:添加一个由返回值的业务方法
@Override
public String doOther(String name, Integer age) {
System.out.println("执行doOther方法");
return "AAAAA";
}
②:在切面类中定义通知
@AfterReturning(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doOther(..))", returning = "result")
public void myAfterReturning(Object result){
//修改目标方法的执行结果
if (result != null){
String s = (String) result;
result = s.toLowerCase();
}
System.out.println("后置通知: 在目标方法之后执行, 修改后的结果:"+ result);
}
③: 测试结果
执行doOther方法
后置通知: 在目标方法之后执行, 修改后的结果:aaaaa
四、@Around环绕通知-增强方法有ProceedingJoinPoint参数
在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。
并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 中有一个**proceed()**方法,用于执行目标方法。
若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。
tips:该增强方法实际是拦截了目标方法的执行。
①:添加业务方法
@Override
public String doFirst(String name, Integer age) {
return "First";
}
②:切面类中定义通知方法
@Around(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕通知: 可以在目标方法之前执行,比如日志");
// 调用proceed()方法执行目标方法
Object proceed = point.proceed();
System.out.println("环绕通知: 可以在目标方法之后执行,比如事务");
System.out.println("通知方法中获取的目标方法结果:"+proceed);
return proceed;
}
③: 测试结果
环绕通知: 可以在目标方法之前执行,比如日志
执行doFirst方法
环绕通知: 可以在目标方法之后执行,比如事务
通知方法中获取的目标方法结果:First
五、@AfterThrowing异常通知-注解中有throwing属性
在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。
当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。
①:添加业务方法,出现异常
@Override
public void doSecond() {
System.out.println("执行doSecond方法"+ 1/0);
}
②:切面类中添加通知方法
tips:1.可以将异常出现的时间、位置、原因等信息存入数据库,日志文件等等
2.可以在异常发生时将异常信息通过短信、邮件发送给开发人员
@AfterThrowing(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doSecond(..))", throwing = "ex")
public void myAfterThrowing(Throwable ex){
System.out.println("异常通知: 在目标方法出现异常时执行。异常原因:"+ex.getMessage());
}
③:测试结果
异常通知: 在目标方法出现异常时执行。异常原因:/ by zero
java.lang.ArithmeticException: / by zero
...
...
六、@After最终通知
无论目标方法是否抛出异常,该增强均会被执行。
①:添加业务方法
@Override
public void doThird() {
System.out.println("执行doThird方法"+ 1/0);
}
②:切面类中添加通知方法
@After(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("最终通知:总是会被执行的方法");
}
③:测试结果
最终通知:总是会被执行的方法
java.lang.ArithmeticException: / by zero
...
...
七、@Pointcut 定义切入点
当较多的通知方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。
这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。
在切面中添加切入点方法(@Pointcut注解), 设置切入点表达式。然后在通知的方法的注解中,使用value属性调用切入点方法。
/* 用于定义和管理切面,简化通知增强方法的定义*/
@Pointcut(value = "execution(* cn.xyz.aop.service.SomeServiceImpl.doThird(..))")
private void myPointcut(){
//
}
@After(value = "myPointcut()")
public void myAfter(){
System.out.println("最终通知:总是会被执行的方法");
}
八、不使用切入点表达式,而是用自定义注解的方式定义
①: 创建一个自定义的注解,用于在目标方法上进行标记
/*自定义注解,用于日志切面*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
String value() default "";
}
②:在切面中定义切入点方法和通知方法
这里不再使用切入点表达式的方式, 在@Pointcut注解的value属性中指定自定义注解的位置。
tips: 不使用切入点方法,也可以直接在通知注解中指定自定义注解的位置,比如@Before。
@Pointcut(value = "@annotation(cn.xyz.aop.annotation.MyLog)")
private void pointcut(){
}
@Before(value = "pointcut()")
public void myBefore(){
System.out.println("前置通知: 在目标方法之前执行。");
}
③: 在目标方法上添加自定义注解,表示将在此方法上添加通知增强方法。
@MyLog("日志切面")
public void doSome(String name, Integer age) {
System.out.println("执行doSome方法");
}