Spring-AOP

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方法");
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值