spring aop切面编程-学习笔记

AOP 切面编程-学习笔记

AOP切面编程是什么

我们用Spring中对AOP的介绍来讲解什么是切面编程, AOP 侧重于横切关注点,横切关注点横切多个对象,主要用于日志记录、安全性、性能监控和事务管理

“Aspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. Whereas OOP focuses on objects and their interactions, AOP focuses on crosscutting concerns which cut across multiple objects, such as logging, security, performance monitoring, and transaction management.”
我在知乎上见到一个很好的讲切面编程的例子,用洗澡来做例子,男人洗澡,需要脱衣服-> 唱歌->洗头->洗身体, 女人洗澡,需要 脱衣服-> 洗头-> 洗身体-> 洗脸-> 护肤-> 穿衣服。 其中脱衣服,穿衣服是洗澡所用到的用到的流程,可以将其独立出来
在这里插入图片描述

为什么需要用到切面编程

AOP的设计是为了将业务逻辑与其他的功能(日志,事务,安全管理,性能优化)进行分离。将其他功能统一进行分类进行统一管理,这样提高了代码的重用性,可维护性。
当你实现一个功能,如果考虑到该功能对于系统来说,是一个横向逻辑,可以考虑用切面编程。

怎么用切面编程

在Spring AOP中,提供如下注解。
在这里插入图片描述
正常执行顺序: @Around前半部分代码 -> Before -> PointCut指向的函数-> @AfterReturning-> @After -> @Around后半部分代码
异常执行的顺序: @Around前半部分代码 -> Before -> PointCut指向的函数->发生异常-> @Afterthowing-> @After -> @Around后半部分代码

接下来我们的具体的例子中讲解如何使用AOP

正常的流程

  1. 添加依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.3.7.RELEASE</version>
        </dependency>
  1. 创建interface Person
public interface Person {
    public void wash();
}
  1. 创建class Man
package com.shirley.service;

import org.springframework.stereotype.Component;

@Component
public class Man implements Person {

    @Override
    public void wash() {
         sing();
        washHead();
      //  throw new NullPointerException("测试");
        washBody();
    }

    private void sing(){System.out.println("男人开始唱歌");}

    private void washHead(){System.out.println("男人开始洗头");}

    private void washBody(){System.out.println("男人开始洗身体");}
}

  1. 创建class Woman
package com.shirley.service;

import org.springframework.stereotype.Component;

@Component
public class Woman implements Person{

    @Override
    public void wash() {
        washHead();
        washBody();
        doFacial();
    }

    private void washHead(){System.out.println("女人开始洗头");}

    private void washBody(){System.out.println("女人开始洗身体");}

    private void doFacial(){System.out.println("女人开始护肤");}
}
  1. 创建Aspect
    在@pointCut注解中,做了一个mapping, 当执行app.aop.wash.Person 方法中wash()函数前,会执行切片的其他函数(比如:注解@Around(“washPointCut()”)定义的函数, 注解@Before(“washPointCut()”)定义的函数)
    在这里插入图片描述
    在下面的例子中,因为我们希望在man 和woman洗澡前,都使用切面,于是我们将man 和woman 抽象到Person的接口中,在@pointCut做的mapping中,将washPointCut()于Person.wash()做一个对应,这样washPointCut()不仅与Man.wash()对应,也与Woman.wash()对应
package com.shirley.aop.wash;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 定义衣服切面
 */

@Aspect
@Component
public class ClothAspect {

    /**
     * 将下面相同的切点表达式抽取到统一的方法,使用的地方直接引用即可
     */
    @Pointcut("execution(* com.shirley.service.Person.wash(..))")
    public void washPointCut(){}

    /**
     * 前置通知
     */
    @Before("washPointCut()")
    public void takeoffCloth() {
        System.out.println("  --- @Before ---准备洗澡,脱衣服.....");
    }

    /**
     * 后置通知
     */
    @After("washPointCut()")
    public void wearDress() {
        System.out.println("--- @After ---洗澡完成!穿好衣服....");
    }

    /**
     * 环绕通知
     * @param point
     */
    @Around("washPointCut()")
    public void aroundAdvice(ProceedingJoinPoint point) {
        try {
            System.out.println("--- @Around before ---这里是环绕通知-前");
            point.proceed();
            System.out.println("--- @Around - after ---这里是环绕通知-后");
        } catch (Throwable throwable) {
            System.out.println("--- @Around--catch exception ---这里是catch到异常");
            throwable.printStackTrace();
        }

    }

    @AfterReturning("washPointCut()")
    public void returnAdvice(){
        System.out.println("--- @AfterReturning ---这是方法正常执行完后会执行的通知");
    }

    @AfterThrowing("washPointCut()")
    public void exceptionAdvice(){
        System.out.println("--- @afterThrowing ---方法执行抛出异常时会执行的异常通知");
    }

}

  1. Junit Test case
package com.shirley;

import com.shirley.service.Man;
import com.shirley.service.Woman;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest()
class AspectTest {

    @Test
    public void testAspect() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.shirley");
        Man man = context.getBean(Man.class);
        Woman woman = context.getBean(Woman.class);
        System.out.println("-----------------------男生开始洗澡---------------------------");
        man.wash();

        System.out.println("-----------------------女生开始洗澡---------------------------");
        woman.wash();
    }
}

  1. 执行结果如下
    -----------------------男生开始洗澡---------------------------
    这里是环绕通知-前
    准备洗澡,脱衣服…
    男人开始唱歌
    男人开始洗头
    男人开始洗身体
    这是方法正常执行完后会执行的通知
    洗澡完成!穿好衣服…
    这里是环绕通知-后
    -----------------------女生开始洗澡---------------------------
    这里是环绕通知-前
    准备洗澡,脱衣服…
    女人开始洗头
    女人开始洗身体
    女人开始护肤
    这是方法正常执行完后会执行的通知
    洗澡完成!穿好衣服…
    这里是环绕通知-后

异常的流程

我在Man.wash()中抛出了一个异常

    @Override
    public void wash() {
        sing();
        washHead();
       // throw new NullPointerException("测试");
        washBody();
    }

输出如下

-----------------------男生开始洗澡---------------------------
--- @Around before ---这里是环绕通知-前
  --- @Before ---准备洗澡,脱衣服.....
男人开始唱歌
男人开始洗头
--- @afterThrowing ---方法执行抛出异常时会执行的异常通知
--- @After ---洗澡完成!穿好衣服....
--- @Around--catch exception ---这里是catch到异常
java.lang.NullPointerException: 测试
-----------------------女生开始洗澡---------------------------
--- @Around before ---这里是环绕通知-前
  --- @Before ---准备洗澡,脱衣服.....
女人开始洗头
女人开始洗身体
女人开始护肤
--- @AfterReturning ---这是方法正常执行完后会执行的通知
--- @After ---洗澡完成!穿好衣服....
--- @Around - after ---这里是环绕通知-后

可以设置切面的范围

在上面的例子中,我们知道可以通过@pointCut设置切面的函数。 在上面的例子中,是针对单个函数进行了AOP.

1|execution(public * *(..))	//任意public方法
2|execution(* wash*(..))	//任意名称以wash开头的方法
3|execution(* com.xyz.service.AccountService.*(..))	//com.xyz.service.AccountService接口中定义的任意方法
4|execution(* com.xyz.service.*.*(..))	//com.xyz.service包中定义的任意方法
5|execution(* com.xyz.service..*.*(..))	//com.xyz.service包中及其子包中定义的任意方法

Spring AOP 切面变成的拓展例子

  1. 在每个方法调用时,打印一个日志。该日志包含: 函数传入是参数中的一个ID(每个参数中包含的ID不一样,于是增加了难度,在这里用接口来解决)+annotation中的描述

正常的业务逻辑

package com.shirley.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @RecordOperate(desc = " save order", convert = SaveOrderConvert.class)
    public boolean saveOrder(SaveOrder saveOrder) {
        System.out.println("save order " + saveOrder.getID());
        return true;
    }

    @RecordOperate(desc = " update order", convert = UpdateOrderConvert.class)
    public boolean updateOrder(UpdateOrder updateOrder){
        System.out.println("update order " + updateOrder.getOrderId());
        return true;
    }
}

SendOrder.java

package com.shirley.service;

import lombok.Data;

@Data
public class SaveOrder {
    private long ID;
}

UpdateOrder.java

package com.shirley.service;

import lombok.Data;

@Data
public class UpdateOrder {
    private long orderId;
}

通过annotation 触发AOP, 可以重点关注每一步的注释

package com.shirley.aop;

import com.shirley.service.Convert;
import com.shirley.service.OperateLogDO;
import com.shirley.service.RecordOperate;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

@Aspect
@Component
public class OperateAspect {

    @Pointcut("@annotation(com.shirley.service.RecordOperate)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = proceedingJoinPoint.proceed(); // 返回的object 是该函数的返回值。 这里返回是boolean-true

        //调用saveOrder时,触发的AOP,这里拿到的methodSignature= 该method,也就是: saveOrder(SaveOrder)
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        Method method = methodSignature.getMethod();
        //拿到注解
        RecordOperate recordOperate = method.getAnnotation(RecordOperate.class);
        //最后解决拿不到注解的方法,是在注解上增加@Retation(value=Runtime)

        Class<? extends Convert> convert = recordOperate.convert();
        Convert logConvert = null;
        try {
            logConvert = convert.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        //将函数的第一个参数SaveOrder或则UpdateOrder传给SaveOrderlogConvert.convert 或则 UpdateOrderlogConvert.convert
        //将SaveOrder或则updateOrder中的ID, orderID 取出来,放到OperateLogDO中
        OperateLogDO operateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);
        //将annotation 中的desc放入OperateLogDO
        operateLogDO.setDesc(recordOperate.desc());
        operateLogDO.setResult(recordOperate.desc());
        System.out.println("这是打印日志, 本次调用的函数是: " + method.getName()
                + " id 是 " + operateLogDO.getOrderID() + " 描述是 " + operateLogDO.getDesc()
                + " 结果 是 " + operateLogDO.getResult());
        return result;

    }
}

定义接口,方便根据断言中Convert<这里传入的类型,来决定不同的Convert进行取值>,

package com.shirley.service;

public interface Convert <PARAM>{

    OperateLogDO convert(PARAM param);
}

SendOrderConvert.java

package com.shirley.service;

public class SaveOrderConvert implements Convert<SaveOrder>{
    @Override
    public OperateLogDO convert(SaveOrder saveOrder) {
        OperateLogDO operateLogDO = new OperateLogDO();
        operateLogDO.setOrderID(saveOrder.getID());
        return  operateLogDO;
    }
}

UpdateOrderConvert.java

package com.shirley.service;

public class UpdateOrderConvert implements Convert<UpdateOrder>{
    @Override
    public OperateLogDO convert(UpdateOrder updateOrder) {
        OperateLogDO operateLogDO = new OperateLogDO();
        operateLogDO.setOrderID(updateOrder.getOrderId());
        return  operateLogDO;
    }
}

Junit test case

    @Test
    public void testDifferentObjectAspect() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.shirley");
        OrderService orderService = context.getBean(OrderService.class);

        SaveOrder saveOrder = new SaveOrder();
        saveOrder.setID(1);

        UpdateOrder updateOrder = new UpdateOrder();
        updateOrder.setOrderId(2);

        orderService.saveOrder(saveOrder);
        orderService.updateOrder(updateOrder);
    }

Tips

1.暂时发现,被选择pointcut的方法要放在service目录下面,否则注入不成功。 这个应该不是真实的原因,真实的原因有待后续去了解
2. 在通过method拿到注解时,一直拿不到,后面在注解上增加了下面的注解后,这样在运行时才能拿到
@Retention(RetentionPolicy.RUNTIME)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值