AOP面向切面编程

1.面向切面编程AOP入门案例(注解版)

步骤:

①导入aop相关坐标

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.4</version>
</dependency>

说明:spring-context坐标依赖spring-aop坐标

②定义dao接口与实现类

public interface BookDao{
    public void save();
    public void update();
}
@Repository
public class BookDaoImpl implements BookDao {

    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }

    public void update(){
        System.out.println("book dao update ...");
    }
}

③定义通知类,制作通知

public class MyAdvice {
    public void before(){
        System.out.println(System.currentTimeMillis());
    }
}

④定义切入点

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
}

说明:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑

⑤绑定切入点与通知的关系,指定通知添加到原始连接点的具体执行位置

这里就是制作切面,切面是用来描述通知和切入点之间的关系

public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    //制作切面,这里通知和切入点的关系是before,通知在切入点之前运行
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

注:@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行

⑥定义通知类受spring容器管理,并定义当前类为切面类

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
 
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

注:@Component将MyAdvice定义为Spring加载的Bean;@Aspect将MyAdvice定义为切面类

⑦开启Spring对AOP注解驱动支持@EnableAspectJAutoProxy

@Configuration
@ComponentScan("com.itheima")
//注解开启AOP
@EnableAspectJAutoProxy
public class SpringConfig {
}

2.AOP工作流程

2.1具体步骤

1.Spring容器启动

2.读取所有切面配置中的切入点

注: 这里只会读取已绑定的切入点,如上图中ptx()并未被绑定就不会读取

3.初始化bean,判定bean对应的通知类中的方法是否匹配到任意切入点

  • 匹配失败,创建对象
  • 匹配成功,创建原始对象(目标对象)的代理对象
    • 匹配成功说明需要对其进行增强
    • 当需要对目标对象进行功能增强时,采用的技术是动态代理,所以这里会创建一个代理对象,容器中管理的就是代理对象,而不是原始对象。
    • 在运行的时候,执行的是代理对象的方法,在该方法中会对原始方法进行功能增强

4.获取bean执行方法

  • 获取bean,调用方法并执行,完成操作
  • 根据代理对象的运行模式运行原始方法与增强的内容,完成操作

2.2对于容器中代理对象的理解

  • 匹配成功
    • 如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
  • 匹配失败
    • 如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身
    • 通过字节码文件验证已有对象是否是代理对象,不能直接打印对象,因为这里代理对象重写了原始对象的toString方法

2.3AOP核心概念

  • 目标对象(Target)原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
  • 代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现

小结:SpringAOP的本质是代理模式

3.AOP切入点表达式

3.1描述方式

  • 切入点:要进行增强的方法
  • 切入点表达式:要进行增强的方法的描述方式

描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update()

execution(void com.itheima.dao.BookDao.update())

描述方式二:不建议,紧耦合。执行com.itheima.dao.impl包下的BookDaoImpl实现类中的无参数update()

execution(void com.itheima.dao.impl.BookDaoImpl.update())

3.2切入点表达式

3.2.1标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

execution(public User com.itheima.service.UserService.findById(int))
  • 动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
  • 访问修饰符,还可以是public,private等,可以省略
  • 返回值,写返回值类型
  • 包名,多级包使用点连接
  • 类/接口名称
  • 方法名
  • 参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名,方法定义中抛出指定异常,可以省略

3.2.2通配符描述切入点,快速描述

  • *   :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(public * com.itheima.*.UserService.find*(*))//这里必须要有参
  • ..   :0个或多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(public User com..UserService.findById(..))
  • 匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
  • +    :专用于匹配子类类型
execution(* *..*Service+.*(..))


3.2.3书写技巧

对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让我们用用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就紧耦合了,而Spring的思想在尽量降低紧耦合
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述返回值
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy*,selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

4.AOP通知类型

  • AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
  • AOP通知共分为以下5种类型

前置通知@Before     目标方法之前

后置通知@After        目标方法之后

环绕通知(重点)@Around      目标方法前后都有

返回后通知(了解)@AfterReturning     抛异常时无法运行

抛出异常后通知(了解)@AfterThrowing        抛异常才能运行

@Around(重点,常用)

作用:设置当前通知方法与切入点之间的绑定关系,当通知方法在原始切入点方法前后运行

@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
    System.out.println("around before advice ...");
    Object ret = pjp.proceed();
    System.out.println("arround after advice ...");
    return ret;
}


@Around注意事项(要掌握

  • 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,并通过proceed()得到返回值,实现原始方法调用前后同时添加通知

注:可以实现隔离,即判断对象是否和原始对象相同,从而达到获取权限的目的

  • 通知中如果未使用ProceedIngJoinPoint对原始方法进行调用将跳过原始方法的执行
  • 对原始方法的调用可以不接收返回值,通知方法设置称void即可,如果接收返回值,必须设定为Object类型
  • 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
  • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象

5.案例:测量业务层接口万次执行效率

5.1需求分析

  • 需求:任意业务层接口执行均可显示其执行效率(执行时长)

分析:

①业务层功能:业务接口执行前后分别记录时间求差值得到执行效率

②通知类型选择前后均可增强的类型——环绕通知

5.2代码实现

@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}

    //设置环绕通知,在原始操作的运行前后记录执行时间
    @Around("ProjectAdvice.servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //获取执行的签名对象
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    }
}

补充:当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程

6.AOP通知获取数据

  • 获取切入点方法的参数

    • JoinPoint:适用于前置,后置,返回后,抛出异常通知
    • ProceedJointPoint:适用于环绕通知
  • 获取切入点方法返回值

    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息

    • 抛出异常后通知
    • 环绕通知

6.1获取参数

①JointPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数

@Before("pt()")
public void before(JointPoint jp){
	Object[] args = jp.getArgs();
	System.out.println(Arrays.toString(args));
}

②ProceedJointPoint:JointPoint的子类,@Around采用ProceedJointPoint来获取参数,其余通知类型采用JointPoint来获取参数

public Object around(ProceedingJointPoint pjp) threos Throwable{
	Object[] args = pjp.getArgs();
	System.out.println(Arrays.toString(args));
	Object ret = pjp.proceed();
	return ret;	
}

6.2获取返回值

只有返回后AfterReturing环绕Around这两个通知类型可以获取

①抛出异常后通知可以获取切入点方法中的异常信息,使用形参可以接收对应的异常对象

@AfterReturning(Value = "pt()",returning = "ret")
public void afterReturning(String ret){
	System.out.println("afterRetruning advice ..."+ ret);
}

@Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);
 
        //proceed方法的返回值ret,就是原始方法的返回值
        System.out.println(ret);
        return ret;
    }

一些小tips:

参数名

afterReturning方法形参类型建议为Object

参数类型可以写成String,为了能匹配更多的参数类型,建议写成Object类型

若参数列表中有JoinPoint,那么在获取原始方法参数时,必须放第一个

6.3获取异常

只有抛出异常后AfterThrowing环绕Around这两个通知类型可以获取

 @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }

 @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }

@Around环绕通知获取异常只需将异常捕获,在catch方法中就可以获取到原始方法的异常信息

7.案例:百度网盘密码数据兼容处理

分析:

  • 在业务方法执行前对之前对所有的输入输出参数进行格式处理——trim()
  • 使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
@Around("DataAdvice.servicePt()")
public Object trimString(ProceedingJoinPoint pjp) throws Throwable{
	Object[] args = pjp.getArgs();
	//对原始参数的每一个参数进行操作
	for(int i = 0; i < args.leng ; i++){
	//如果是字符串数据
	if(args[i].getClass().equals(String.class)){
	//取出数据,trim()操作后,更新数据
	args[i] = args[i].toString().trim();
	}
	}
	return pjp.proceed(args);
}

AOP总结

  • 概念:AOP面向切面编程,是一种编程范式
  • 作用:在不惊动原始设计的基础上为方法进行功能增强
  • 核心概念
    • 代理(Proxy):SpringAOP的本质核心就是采用代理模式实现的
    • 连接点(JoinPoint):在SpringAop中,理解为任意方法的执行
    • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
    • 通知(Advice):若干个方法的共性,在切入点处执行,最终体现为一个方法
    • 切面(Aspect):描述通知与欺辱点对应关系
    • 目标对象(Target):被代理的原始对象称为目标对
    • 切入点表达式标准格式,动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名 )
      • excution(* com.hcx.service.*Service.*(..))
    • 切入点表示描述统配符:
      • 作用:用于快速描述,范围描述
      • *:匹配任意符号(常用)
      • …:匹配多个连续任意符号(常用)
      • +:匹配子类类型
    • 切入点表达式书写技巧
      • 按标准规范开发
      • 查询操作的返回值建议在使用 * 匹配
      • 减少使用…去描述包
      • 对接口进行描述,使用表示模块名,例如UserService的匹配描述为Service
      • 方法名书写保留动词,例如get,使用表示名称,例如getById匹配描述为getBy
      • 参数更卷实际情况灵活调整
    • 通知类型
      • 前置通知
      • 后置通知
      • 环绕通知(重点)
        • 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
        • 环绕通知可以隔离原始方法的调用执行
        • 环绕通知返回设置为Objec类型
        • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
      • 返回后通知
      • 抛出异常后通知
    • AOP通知获取数据
  • 获取切入点方法的参数
    • JoinPoint:适用于前置,后置,返回后,抛出异常后通知,设为方法发第一个形参
    • ProceedJointPoint:适用于环绕通知
  • 获取切入点方法返回值
    • 返回后通知
    • 环绕通知
  • 获取切入点方法运行异常信息
    • 抛出异常后通知
    • 环绕通知
       


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的AOP面向切面编程的测试代码示例,使用Spring框架实现: 首先,创建一个切面类 `LoggingAspect`,用于定义切面逻辑: ```java import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void beforeAdvice(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature().getName()); } @After("execution(* com.example.service.*.*(..))") public void afterAdvice(JoinPoint joinPoint) { System.out.println("After method: " + joinPoint.getSignature().getName()); } } ``` 然后,创建一个测试服务类 `UserService`,用于演示AOP的应用: ```java import org.springframework.stereotype.Service; @Service public class UserService { public void createUser(String username) { System.out.println("Creating user: " + username); } public void deleteUser(String username) { System.out.println("Deleting user: " + username); } } ``` 最后,创建一个Spring Boot应用程序,并在启动类中进行配置: ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @EnableAspectJAutoProxy public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); UserService userService = context.getBean(UserService.class); userService.createUser("John"); userService.deleteUser("John"); } } ``` 在上述示例中,`LoggingAspect` 切面类使用 `@Before` 和 `@After` 注解分别定义了在目标方法执行前和执行后的逻辑。切面逻辑会应用于 `UserService` 类中的所有方法。 当运行应用程序时,可以看到切面逻辑在方法执行前和执行后打印了相应的日志消息。 这是一个简单的AOP面向切面编程的示例,你可以根据实际需求进行更复杂的切面逻辑定义和应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值