了解AOP

news2024/11/16 3:44:48

1 AOP概述

思考:在一个教务系统中,以下哪些是主要业务逻辑,哪些是次要业务逻辑?

 1.1 关于AOP

AOP(面向方面/切面编程)是对OOP(面向对象编程)的补充,提供另一种关于程序结构的思维方式。OOP中模块化的关键单元是类,而AOP中模块化的单元是方面/切面(Aspect)。方面允许将横切多个类型和对象的关注点(如事务管理)模块化,在AOP的术语中,这种关注点通常叫做“横切关注点”。

AOP允许将次要业务逻辑相关的横切关注点模块化为方面,然后将方面切入到需要的目标对象中。

AOP实现将次要业务逻辑从主要业务逻辑中分离,从而降低程序耦合性和提高代码重用,让开发人员可以专注主要业务逻辑开发。

该思想的核心是:

  • AOP将软件系统分成两部分:核心关注点(主要业务逻辑)和横切关注点(次要业务逻辑)
  • 将次要逻辑(日志记录、安全、事务管理等)从主要/核心业务逻辑中分离出来,做成单独的模块,也就是方面
  • 然后在需要切入次要逻辑的地方切入即可,这样核心业务模块专注于主要业务逻辑即可
  • 通俗地说,是在运行时,动态地将代码切入到类的指定方法或其他指定位置上的一种编程思想

1.2 AOP术语

1.2.1 常用术语

实现AOP的过程中,我们会用到各种各样的组件和过程,我们使用不同的术语称呼不同的组件和过程,这些术语并非由Spring提供,而是在AOP中已经广泛使用的术语,Spring沿用了这些术语:

  1. 切面/方面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是一个关于横切关注点的很好的例子。
  2. 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
  3. 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知。
  4. 切入点(Pointcut):应用通知的连接点。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行。
  5. 目标对象(Target Object): 被一个或者多个方面所通知的对象。也被称做被通知(advised)对象,也就是通知应用到的目标对象。 Spring AOP是通过运行时动态代理实现的,这个对象永远是一个被代理(proxied)对象。
  6. AOP代理(AOP Proxy):代理对象,也就是AOP框架创建的对象,用来实现方面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  7. 织入(Weaving):将方面和目标对象进行关联,并创建被通知对象(代理对象)的过程叫做织入。

对于连接点和切入点,一言以蔽之:

  • 连接点(Join Point):可以应用通知的点,如方法的执行或跑出异常,spring只支持方法执行作为连接点
  • 切入点 (Pointcut):选中要应用通知的连接点构成切入点

1.2.2 通知

通知类型:

  1. 前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
  2. 返回后通知(After returning advice):在某连接点正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回。
  3. 抛出后通知(After throwing advice):在方法抛出异常退出时执行的通知。
  4. 后置/最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
  5. 环绕通知(Around Advice):包围一个连接点执行的通知,如方法调用。这是最强大的一种通知。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

如果不同方面中的两个或两个以上的通知应用到同一个连接点,除非指定了执行顺序,否则这多个通知的执行顺序是未知的,可以通过让方面类实现Ordered接口或使用@Order注解指定方面中通知的执行顺序。顺序数字越小,表示执行的优先级越高

1.2.3 切入点

1.2.3.1 语法格式

定义切入点要定义两部分:

  • 切入点签名:一个方法声明,方法名为切入点名称,方法可以有任意参数,但是返回类型必须是null
  • 切入点表达式:一个表达式,指定切入点覆盖的连接点,这里使用的是AspectJ的切入点写法
// 切入点表达式
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
// 切入点方法签名
public void pointcut1(){}

切入点表达式的格式为:

  • execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点

  • public:访问修饰符,还可以是private等,可以省略

  • int:返回类型
  • com.qdu.service.MathService:类/接口名称
  • add:方法名
  • (int, int):方法参数,直接写参数的类型,多个类型用逗号隔开
  • 异常名:方法定义中抛出指定异常,可以省略

1.2.3.2 通配符

我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?

*:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

execution(* com.qdu.service.impl.StudentServiceImpl.*(String))

execution(* com.qdu.service.StudentService.*(String,String))

..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

execution(* com.qdu.service.StudentService.*(..))

execution(* com.qdu..*.*(..))
1.2.3.3 实例

声明切入点的格式:一个切入点表达式+方法签名

方法名是引用这个切入点的名称,也就是切入点的名称

@Aspect
@Component
public class LogAspect {
    @Pointcut("@within(com.qdu.aop.LogJoinPoint) || @annotation(com.qdu.aop.LogJoinPoint)")
	public void pointcut() {

	}

    @Before("pointcut()")
	public void beforeAdvice(JoinPoint point) {
		System.out.println("---------前置通知,方法名称: "+point.getSignature().getName());
	}	
	
	@AfterReturning("pointcut()")
	public void AfterReturningAdvice(JoinPoint p) {
		System.out.println("---------返回后通知,方法名称: "+p.getSignature().getName());
	}
}

 在定义切入点表达式中我们发现在@Pointcut中我们用的最多的是execution,上面的例子也用了@within、@annotation,这是标志符。定义切入点表达式时一般会用到以下几种标志符:

  1. execution标志符:指定方法执行作为连接点,具体到方法
  2. within标志符:指定连接点所在的类型,具体到类型(实现类)
  3. target标志符:指定目标对象对应的类型,具体到类型(接口或类)
  4. args标志符:args用于指定方法的参数,根据 方法参数个数 和 类型 去匹配构成切入点的连接点
  5. bean标志符:指定目标方法所在的bean的id或名称
  6. @annotation标志符:指定某个注解修饰的方法构成切入点,具体到指定注解修饰的方法。指定注解的包名.注解名,说明该注解修饰的方法构成切入点
  7.  @within标志符: 指定某个注解修饰的类型中的方法构成切入点,具体到指定注解修饰的类型
//1. execution标志符: 指定方法执行作为连接点,具体到方法
@Pointcut("execution(* com.qdu.service.*.*(..))")
//2. within标志符: 指定连接点所在的类型,具体到类型(实现类)
@Pointcut("within(com.qdu.service.impl.StudentServiceImpl)")
//3. target标志符: 指定目标对象对应的类型,具体到类型(接口或类)
@Pointcut("target(com.qdu.service.StudentService)")
//4. args标志符:args用于指定方法的参数,根据方法参数个数和类型去匹配构成切入点的连接点
//在切入点表达式中可以使用&&、||、!逻辑运算符
@Pointcut("args(String) || args(Integer,Integer)")
//5. bean标志符: 指定目标方法所在的bean的id或名称
@Pointcut("bean(mathServiceImpl) || bean(teacherServiceImpl)")
//6. @annotation标志符: 指定某个注解修饰的方法构成切入点,具体到指定注解修饰的方法
//指定注解的包名.注解名,说明该注解修饰的方法构成切入点
@Pointcut("@annotation(com.qdu.aop.LogJoinPoint)")
//7. @within标志符: 指定某个注解修饰的类型中的方法构成切入点,具体到指定注解修饰的类型
@within(com.qdu.aop.LogJoinPoint) //表示LogJoinPoint注解修饰的类型中的方法构成切入点
1.2.3.4 练习 

给出要应用切入点表达式的方法,试着写出切入点表达式:

  1. 所有方法
  2. 所有公开方法
  3. 所有以play开头的方法
  4. com.qdu.service.impl.StudentServiceImpl类型中的所有方法
  5. com.qdu.service.impl.StudentServiceImpl类型中带一个String类型参数的方法
  6. com.qdu.service.impl.StudentServiceImpl类型中带两个个String类型参数的方法
  7. com.qdu.service.StudentService类型中的所有方法
  8. com.qdu.service包下的所有类型中的所有方法
  9. com.qdu.service包和其子包下所有类型中的所有方法 

以下是答案:

//1. 所有方法
@Pointcut("execution(* *(..))")
public void pointcut1() {
}

//2. 所有公开方法
@Pointcut("execution(public * *(..))")
public void pointcut2() {
}

//3. 所有以play开头的方法
@Pointcut("execution(* play*(..))")
public void pointcut3() {
}

//4. com.qdu.service.impl.StudentServiceImpl类型中的所有方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(..))")
public void pointcut4() {
}

//5. com.qdu.service.impl.StudentServiceImpl类型中带一个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String))")
public void pointcut5() {
}

//6. com.qdu.service.impl.StudentServiceImpl类型中带两个个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String, String))")
public void pointcut6() {
}

//7. com.qdu.service.StudentService类型中的所有方法
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut7() {
}

//8. com.qdu.service包下的所有类型中的所有方法
@Pointcut("execution(* com.qdu.service.*.*(..))")  //注意第二个.*
public void pointcut8() {
}

//9. com.qdu.service包和其子包下所有类型中的所有方法
@Pointcut("execution(* com.qdu.service..*.*(..))")  //注意第一个是..*
public void pointcut9() {
}

2 AOP实现

方面/切面(Aspect)是将次要业务逻辑/次要关注点/横切关注点模块化为方面,在这里我们将日志功能做成一个方面

2.1 AOP配置

2.1.1 基于XML Schema的AOP(使用<aop:config>配置AOP)

在Spring的XML配置文件中使用<aop:aspectj-autoproxy />,这个标签的作用是使@AspectJ注解生效。

<aop:aspectj-autoproxy />

然后在com.qdu.aop包中创建一个LogAspect.class,这个类包含的是次要业务逻辑:日志功能。

@Aspect 
public class LogAspect {}

如果要使用<aop:config>配置而不使用诸如@Aspect注解(纯使用xml)则略微麻烦。

首先方面对应的类应该注册为Spring管理的bean,才能将方面切入到需要的地方

<bean id="logAspect1" class="com.qdu.aop.LogAspect1" />

<bean id="logAspect2" class="com.qdu.aop.LogAspect2" />

  在<aop:config>中:

  • <aop:pointcut>:定义切入点,这样的切入点可以在多个<aop:aspect>中使用。
  • <aop:aspect>:配置一个方面对应的类,ref属性指定方面类bean的id或name,order用于控制通知的执行顺序,值越小越先执行。注意:在aop:aspect标记内定义的切入点只能在该aop:aspect标记中使用
  •  <aop:before>:配置前置通知,method属性指定作为前置通知的方法的名称,pointcut属性用于指定切入点表达式,pointcut-ref属性用于指定引用的切入点的id
  • <aop:after-returning>:配置抛出后通知,throwing属性指定一个参数名(可随便起,尽量有意义),这样可以抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象
  • <aop:after>:用于配置最终通知/后置通知 
  • <aop:around>:用于配置环绕通知
<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))"
		id="pt1" />
		
	<aop:aspect ref="logAspect1" order="2">
		<!-- 在aop:aspect标记内定义的切入点只能在该aop:aspect标记中使用 -->
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
			
		<aop:before method="before1" pointcut-ref="pt1" />
		<aop:before method="before2" pointcut-ref="pt1" />
		
		<aop:after-returning method="afterReturning" pointcut-ref="pt2" 
				returning="returnValue" />
			
		<aop:after-throwing method="afterThrowing" pointcut-ref="pt3" throwing="ex" />

		<aop:after method="after" pointcut-ref="pt3"/>

		<aop:around method="around" pointcut-ref="pt4"/>
	</aop:aspect>

	<aop:aspect ref="logAspect2" order="1">
		<aop:before method="before3" pointcut-ref="pt1" />
		<aop:before method="before4" pointcut-ref="pt1" />
	</aop:aspect>
</aop:config>

2.1.2 @AspectJ风格的AOP(使用@Aspect+@Before等注解)

也可以在SpringConfig配置类中使用@EnableAspectJAutoProxy注解,该注解的作用也是启用对@AspectJ、@Before等注解的支持。而且开启包扫描时不要忘记扫描次要业务逻辑的包(这里是com.qdu.aop)

@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {

}

在LogAspect.java中,除了用@Aspect修饰这个包含次要逻辑的类,也不要忘记@Component

@Aspect 
@Component
public class LogAspect {}

2.1.3 使用Spring API实现Spring AOP

该方法我使用的不多,对此也并不是很理解,仅附上代码,日后待更

Spring的xml配置文件:

<!-- 1. 创建目标对象 -->
<bean id="studentService" class="com.qdu.service.impl.StudentServiceImpl" />

<!-- 2. 创建通知 -->
<bean id="beforeAdvice" class="com.qdu.aop.MyBeforeAdvice" />
<bean id="afterReturningAdvice" class="com.qdu.aop.MyAfterReturningAdvice" />

<!-- 通知器:一种用于指定何种通知应用于何种切入点的组件 -->
<!-- 2. 创建通知器和定义切入点,这里使用正则表达式切入点通知器,该类是一个方便类,允许使用正则表达式指定切入点,并指定在该切入点应用的通知 -->
<bean id="advisor1" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<property name="advice" ref="beforeAdvice" />
	<property name="pattern" value=".*" />
</bean>

<bean id="advisor2" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
	<property name="advice" ref="afterReturningAdvice" />
	<property name="pattern" value=".*play.*" />
</bean>

<!-- 3. 创建代理 -->
<!-- ProxyFactoryBean这个类用于指定代理对象如何生成,需要提供目标对象 、目标对象实现的接口、应用的通知器-->
<bean id="studentServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="target" ref="studentService" />
	<property name="proxyInterfaces" value="com.qdu.service.StudentService" />
	<property name="interceptorNames">
		<list>
			<value>advisor1</value>
			<value>advisor2</value>
		</list>
	</property>
</bean>

2.2 实现各种通知

通知(Advice)即方面包含的操作,在spring代码中,通知对应的是方法
 通知有5种类型:

  1. 前置通知(Before Advice):是在目标方法执行之前执行的通知
  2. 返回后通知(After Returning Advice): 是目标方法正常执行返回后执行的通知
  3. 抛出后通知(After Throwing Advice):是目标方法执行发生异常,抛出异常后执行的通知
  4.  后置通知(After Advice):不管目标方法执行是否正常,方法执行完总要执行的通知
  5. 环绕通知(Around Advice):环绕一个方法执行的通知,是最强大的通知

2.2.1 基于XML

2.2.1.1 前置通知
<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />

    <aop:aspect ref="logAspect1" order="2">
        <aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
	    <aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />

        <aop:before method="before1" pointcut-ref="pt1" />
		<aop:before method="before2" pointcut-ref="pt1" />
    </aop:aspect>

</aop:config>

LogAspect1.java:

public class LogAspect1 {

	public void before1(JoinPoint point) {
		System.out.println("..............前置通知1,目标方法:" + point.getSignature().getName());
	}

	public void before2(JoinPoint point) {
		System.out.println("..............前置通知2");
	}
}
2.2.1.2 返回后通知

returning属性用于指定一个参数名,这样可以在返回后通知对应的方法上添加一个该名称的参数,用于接收目标方法的返回值

<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />

    <aop:aspect ref="logAspect1" order="2">
        <aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
	    <aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />

        <aop:after-returning method="afterReturning" pointcut-ref="pt2" 
				returning="returnValue" />

    </aop:aspect>

</aop:config>

LogAspect1.java:

public class LogAspect1 {

    public void afterReturning(JoinPoint point, Object returnValue) {
		System.out.println("..............返回后通知,返回值:" + returnValue);
	}

}
2.2.1.3 抛出后通知

throwing属性用于指定一个参数名,这样可以在抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象

<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />

    <aop:aspect ref="logAspect1" order="2">
        <aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
	    <aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />

        <aop:after-throwing method="afterThrowing" pointcut-ref="pt3" throwing="ex" />

    </aop:aspect>

</aop:config>

LogAspect1.java:

public class LogAspect1 {

    public void afterThrowing(JoinPoint point, Throwable ex) {
		System.out.println("..............抛出后通知,异常消息:" + ex.getMessage());
	}

}
2.2.1.4 最终/后置通知
<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />

    <aop:aspect ref="logAspect1" order="2">
        <aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
	    <aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />

        <aop:after method="after" pointcut-ref="pt3"/>

    </aop:aspect>

</aop:config>

LogAspect1.java:

public class LogAspect1 {

    public void after(JoinPoint point) {
		System.out.println("..............最终通知");
	}

}
2.2.1.5 环绕通知
<aop:config>
	<aop:pointcut
		expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />

    <aop:aspect ref="logAspect1" order="2">
        <aop:pointcut
			expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
	    <aop:pointcut
			expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
		<aop:pointcut
			expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />

        <aop:around method="around" pointcut-ref="pt4"/>

    </aop:aspect>

</aop:config>

LogAspect1.java:

public class LogAspect1 {

    public Object around(ProceedingJoinPoint point) throws Throwable {
		System.out.println("..............环绕通知");
		return point.proceed();
	}

}

2.2.2 基于Java代码与注解

不同于XML配置,使用Java代码配置则是更多地使用Java配置类和注解

首先给出Java配置类,主要是开启包扫描启用AspectJ风格

@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {

}

接下来就是使用AspectJ的切入点表达式来指定应用通知的切入点,通过注解完成

2.2.2.1 前置通知
@Before("execution(* com.qdu.service.StudentService.*(..))")
public void beforeAdvice1(JoinPoint point) {
	System.out.println("~~~~~~~~~~~~~~~~~~~"+point.getSignature().getName()+"方法执行前");
}

这里添加了一个JoinPoint类型的参数,用于获取构成切入点的连接点的信息

这里我们给出一个实际的例子,此时我们有了一个com.qdu.service.MathService接口,并且有了该接口的实现类

我们对实现类中的add方法应用前置通知并获取方法中的一些信息:

   @Before("execution(* com.qdu.service.MathService.*(..))")
   public void beforeAdvice2(JoinPoint point) {
       System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
               +point.getSignature().getName()+"方法调用前");
        
       System.out.println("目标方法名:"+point.getSignature().getName());
       System.out.println("目标方法详细信息:"+point.getSignature().toLongString());
       //getTarget()可以获取目标对象,如果在通知中需要操作目标对象,可获取该对象操作
       System.out.println("目标对象:"+point.getTarget().getClass()); 
       System.out.print("目标方法的参数:");
       //getArgs()用于获取目标方法传入的实际参数,以一个Object[]返回
       Object[] args=point.getArgs();
       for(Object arg:args) {
           System.out.print(arg+" ");
       }
       System.out.println();
   }

输出如下图所示:

2.2.2.2 返回后通知
@AfterReturning("execution(* com.qdu.service.*.*(..))")
public void afterReturningAdvice(JoinPoint p) {
	System.out.println("..................."+p.getSignature().getName()+"方法执行返回后");
}

@AfterReturning注解的returning属性用于指定一个方法参数名,该名称对应的参数用于接收目标方法的返回值。如果希望表示任何类型的返回值,请使用Object类型

@Pointcut("execution(* com.qdu.service..*(..))")
public void pointcut1() {
}

@AfterReturning(value = "pointcut1()", returning = "returnValue")
public void afterReturningAdvice(JoinPoint point, Object returnValue) {
	System.out.println("----------------------------------------" 
	        + point.getSignature().getName() 
	        + "方法调用返回后,返回值:"+returnValue);
}

 调用后运行结果如下:

 当然,若方法执行出现异常则不执行返回后通知

 2.2.2.3 抛出后通知

使用该类型的通知来收集异常信息。抛出后通知不会阻止异常的发生,只是在目标方法发生异常后做点事情。

一旦目标方法发生异常,会抛出异常对象,如果希望在抛出后通知中获取该异常对象,从而获取异常信息,可以通过throwing指定一个参数名,并在抛出后通知对应的方法上添加一个该名称的参数用于接收异常对象

如果指定异常类型是某个类型,则只有发生该类型异常的方法才会应用通知;如果希望能表示所有类型的异常,可以使用Exception或Throwable定义

@Aspect
@Component
public class LogAspect {

	@Pointcut("execution(* com.qdu.service.MathService.*(..))")
	public void pointcut1() {
	}

	@AfterThrowing(value = "pointcut1()",throwing="ex")
	public void afterThrowingAdvice(JoinPoint point,Throwable ex) {
		System.out.println("----------------------------------------" 
	            + point.getSignature().getName() + "方法抛出异常后,异常消息:"
				+ex.getMessage());
	}
}

运行一个会抛出异常的方法,如下图所示:

 2.2.2.4 后置通知

不管目标方法正常执行返回还是抛出异常都会执行的通知

@Aspect
@Component
public class LogAspect {

	@Pointcut("execution(* com.qdu.service.MathService.*(..))")
    public void pointcut1() {
    }

    
    @After("pointcut1()")
    public void afterAdvice(JoinPoint point) {
        System.out.println("----------------------------------------" 
        		+ point.getSignature().getName()
                + "方法执行后,参数值:"
        		+point.getArgs()[0]+"和"+point.getArgs()[1]);
    }
}

 执行结果:

2.2.2.5 环绕通知 

1. 环绕通知是包围一个方法的通知,它可以控制方法的执行

 默认情况下,环绕通知会拦截目标方法的执行,也就是导致目标方法不会执行;如果希望能够获取连接点的信息和控制目标方法的执行,可以添加一个ProceedingJoinPoint类型的参数

@Aspect
@Component
public class LogAspect1 {

	@Pointcut("execution(* com.qdu.service.*.playGames(..))")
	public void pointcut1() {
	}

	@Around("pointcut1()")
	public void aroundAdvice(ProceedingJoinPoint point) throws Throwable{
		System.out.println("--------------------环绕通知,调用目标方法前");
		//调用ProceedingJoinPoint的proceed()方法让目标方法执行
		//这里获取连接点,也就是目标方法的参数,返回的是一个Object[]
		//[0]表示获取方法的第一个参数,实际的参数是String类型,所以这里转成字符串
		String gameName=point.getArgs()[0].toString();
		//可以根据一个条件决定是否让目标方法执行
		if(gameName.contains("绝地")||gameName.contains("求生")) {
			System.out.println("打什么绝地求生,好好学习~~~");
		} else {
			point.proceed();
		}
		System.out.println("~~~~~~~~~~~~~~~~~~~~环绕通知,调用目标方法后");
	}
}

运行结果如下:

2. 环绕通知不仅可以控制目标方法的执行,还可以控制目标方法的返回值

 如果环绕通知返回类型为void,会导致有返回值的目标方法的返回值为null。可以在环绕通知中将目标方法的值返回,让目标方法的值得以正常返回

@Aspect
@Component
public class LogAspect2 {

    @Pointcut("execution(* com.qdu.service.MathService.*(..))")
    public void pointcut1() {
    }

    
    @Around("pointcut1()")
    public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable{
        System.out.println("--------------------环绕通知,目标方法:" 
        		+ point.getSignature().getName());
        
        //proceed()方法调用会导致目标方法会调用,返回的值就是目标方法的返回值
        Object returnValue=point.proceed();
        //环绕通知这里返回的值就是最终返回的值
        return returnValue;
    }
}

运行结果如下:

 看过以上两个例子,我们发现环绕通知可以将以上四种通知结合起来,也可以替代以上四种任何一种通知(但是尽量使用最合适的通知)

下面这个例子中,我们用环绕通知将以上四种通知集成到环绕通知中:

@Aspect
@Component
public class LogAspect {

	@Pointcut("execution(* com.qdu.service.MathService.*(..))")
	public void pointcut1() {
	}


	@Around("pointcut1()")
	public Object aroundAdvice(ProceedingJoinPoint point) {
		
		Object result=null;
				
		try {
			System.out.println("--------这是目标方法执行前执行的代码");
			result=point.proceed();
			System.out.println("~~~~~~~~这是目标方法正常执行返回后会执行的代码");
		} catch (Throwable e) {
			System.out.println("^^^^^^^^这是目标方法执行抛出异常后执行的代码");
		} finally {
			System.out.println("********这是目标方法不管是否出现异常,都会执行的代码");
		}
		return result;
	}
}

运行10/2,发现该环绕通知实现了前置、返回后、后置通知的功能:

 运行10/0,发现该环绕通知实现了前置、抛出后、后置通知的功能:

2.3 多个通知的执行顺序 

前面我们提到,如果多个同样的通知应用到同一个目标方法,执行顺序是未知的,但是可以通过使用@Order注解来控制执行顺序,@Order里的值越小表示顺序越靠前

假如我们有两个LogAspect,LogAspect1的Order值为2,有beforeAdvice1、beforeAdvice2、环绕通知、afterReturningAdvice1、afterReturningAdvice2;LogAspect2的Order值为1,有beforeAdvice3、beforeAdvice4;每个LogAspect都只有一个切入点,且该切入点运用到多个通知

@Aspect
@Component
@Order(2)
public class LogAspect1 {

	@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
	public void pointcut1() {
	}

	@Before("pointcut1()")
	public void doBefore1() {
		System.out.println("..........beforeAdvice1..........");
	}

	@Before("pointcut1()")
	public void doBefore2() {
		System.out.println("..........beforeAdvice2..........");
	}

	@AfterReturning("pointcut1()")
	public void doAfterReturning1() {
		System.out.println("..........afterReturningAdvice1..........");
	}

	@AfterReturning("pointcut1()")
	public void doAfterReturning2() {
		System.out.println("..........afterReturningAdvice2..........");
	}

	@Around(value = "pointcut1()")
	public void aroundAdvice(ProceedingJoinPoint point) throws Throwable {
		System.out.println("..........环绕通知中调用目标方法前的代码..........");
		point.proceed();
		System.out.println("..........环绕通知中调用目标方法后的代码..........");
	}

}
@Aspect
@Component
@Order(1) //值越小表示顺序越靠前
public class LogAspect2 {

    @Pointcut("execution(* com.qdu.service.StudentService.*(..))")
    public void pointcut1() {
    }

    @Before("pointcut1()")
    public void doBefore3() {
        System.out.println("..........beforeAdvice3..........");
    }

    @Before("pointcut1()")
    public void doBefore4() {
        System.out.println("..........beforeAdvice4..........");
    }

}

LogAspect2的Order值小,所以LogAspect2中的通知应该优先执行。所以在4+1个前置通知中,LogAspect2中的beforeAdvice3、beforeAdvice4应优先执行,然后再执行LogAspect1中的

执行结果如下:

 2.4 AOP小练习

  1. 至少使用一次“切入点签名+切入点表达式”的写法定义一个该类中要用的切入点
  2. 定义一个方法,作为一个前置通知,包含目标方法执行前要执行的代码,通知应用到StudentService的所有方法
  3. 定义一个方法,作为一个返回后通知,包含目标方法执行成功返回后要执行的代码,通知应用到com.qdu.service包和子包内的所有类型的所有方法,但是排除divide2方法,并打印方法返回值
  4. 定义一个方法,作为一个抛出后通知,包含目标方法发生异常执行的方法,通知应用到MathService接口的divide开头的方法,并打印error级别的日志信息,要求打印异常消息
  5. 定义一个方法,作为一个环绕通知,应用到名为divide2方法上,用于集中处理异常,如果发生异常,打印error级别的日志信息和异常消息。请确保如果目标方法有返回值,返回值会正常返回

1 + 2.至少使用一次“切入点签名+切入点表达式”的写法定义一个该类中要用的切入点

定义一个方法,作为一个前置通知,包含目标方法执行前要执行的代码,通知应用到StudentService的所有方法

private static Logger logger = LoggerFactory.getLogger(LogAspect1.class);

@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pt1() {
}

@Before("pt1")
public void beforeAdvice(JoinPoint point) {
    logger.debug("~~" + point.getSignature().getName() + "方法调用前");
}

3. 定义一个方法,作为一个返回后通知,包含目标方法执行成功返回后要执行的代码,通知应用到com.qdu.service包和子包内的所有类型的所有方法,但是排除divide2方法,并打印方法返回值

@AfterReturning(
    value = "within(com.qdu.service..*) && !execution(* divide2(..))",
    returning = "rv"
)
public void afterReturningAdvice(JoinPoint point, Object rv) {
    logger.debug("**" + point.getSignature().getName() + "方法正常执行返回后,方法返回值" + rv);
}

4. 定义一个方法,作为一个抛出后通知,包含目标方法发生异常执行的方法,通知应用到MathService接口的divide开头的方法,并打印error级别的日志信息,要求打印异常消息

@AfterThrowing(
    value = "execution(* com.qdu.service.MathService.divide*(..))",
    throwing = "ex"
)
public void afterThrowingAdvice(JoinPoint point, Throwable ex) {
    logger.debug("--" + point.getSignature().getName() + "方法发生异常后,异常消息:" + ex.getMessage());
}

5. 定义一个方法,作为一个环绕通知,应用到名为divide2方法上,用于集中处理异常,如果发生异常,打印error级别的日志信息和异常消息。请确保如果目标方法有返回值,返回值会正常返回

@Around("execution(* divide2(..))")
public Object aroundAdvice(ProceedingJoinPoint point) {
    Object returnValue = null;

    try {
        returnValue = point.proceed();
    } catch(Throwable e) {
        logger.error("发生异常,目标方法:" + point.getSignature().getName() + ",异常信息:" + e.getMessage());
    }

    return returnValue
}

运行结果如下:

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1315304.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FC 可视化功能菜单主代码

[FC][BestVisible][Config].asm ;文件头配置 NES_16KB_PRG_SIZE 8 NES_8KB_CHR_SIZE 16 PRG_BANK_MASK NES_16KB_PRG_SIZE * 2 - 1 ;bank号掩码 PRG_BANK_E000 NES_16KB_PRG_SIZE * 2 - 1 PRG_BANK_C000 …

《Kotlin核心编程》笔记:集合、序列与内联函数

集合的高阶函数API map 操作 val list listOf(1, 2, 3, 4, 5, 6) val newList list.map { it * 2 }当然&#xff0c;在 Java 8 中&#xff0c;现在也能像Kotlin那样去操作集合了。 上面的方法实际上就是一个高阶函数&#xff0c;它接收的参数实际上就是一个函数&#xff0…

sourcetree使用详解

介绍 SourceTree 是 Windows 和Mac OS X 下免费的 Git 和 Hg 客户端管理工具&#xff0c;同时也是Mn版本控制系统工具。支持创建、克隆、提交、push、pull 和合并等操作。——百度百科 是一款比较好用的图形化GUI的git、hg管理工具。还有一些其他的可视化代码管理工具&#x…

【webstrom】【idea】修改git历史提交记录

webstrom修改git历史提交记录 历史记录中有3条提交记录 此时2中的提交记录需要更新&#xff0c;我们可以在2中右击&#xff0c;选择“从这里执行交互式变基” 在弹框中选择需要修改提交记录2右击&#xff0c;然后选择“停止以编辑” 启动变基 更改2中内容 提交对2的更改 …

美国 AGU 发布 AI 应用手册,明确 6 大指导方针

爆发性的 AI 应用&#xff1a;风险与机遇并存 在空间和环境科学领域&#xff0c;AI 工具的应用越来越广泛——诸如天气预报和气候模拟&#xff0c;能源及水资源管理等等。可以说&#xff0c;我们正在经历前所未有的 AI 应用爆发&#xff0c;面对其中的机遇与风险&#xff0c;更…

《PySpark大数据分析实战》-11.Spark on YARN模式安装Hadoop

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…

Power BI - 5分钟学习增加索引列

每天5分钟&#xff0c;今天介绍Power BI增加索引列。 什么是增加索引列&#xff1f; 增加索引列就是向表中添加一个具有显式位置值的新列&#xff0c;一般从0或者从1开始。 举例&#xff1a; 首先&#xff0c;导入一张【Sales】样例表(Excel数据源导入请参考每天5分钟第一天)…

目前最火的大模型训练框架 DeepSpeed 详解来了

目前&#xff0c;大模型的发展已经非常火热&#xff0c;关于大模型的训练、微调也是各个公司重点关注方向&#xff0c;但是大模型训练的痛点是模型参数过大&#xff0c;动辄上百亿&#xff0c;如果单靠单个GPU来完成训练基本不可能。所以需要多卡或者分布式训练来完成这项工作。…

Android 移动端编译 cityhash动态库

最近做项目&#xff0c; 硬件端 需要 用 cityhash 编译一个 动态库 提供给移动端使用&#xff0c;l 记录一下 编译过程 city .cpp // // Created by Administrator on 2023/12/12. // // Copyright (c) 2011 Google, Inc. // // Permission is hereby granted, free of charg…

Win11黑屏死机怎么办?

Win11黑屏死机是一个令人烦恼的问题&#xff0c;特别是对于那些计算机知识并不充裕的人来说。那么Win11死机黑屏怎么办呢&#xff1f;下面我们就来了解一下。 方案一&#xff1a;卸下外部硬盘驱动器 有些时候&#xff0c;电脑的外部硬件可能会导致电脑黑屏问题。在这种情况下&…

【️Java和C++主要的区别有哪些?各有哪些优缺点?】

✅Java和C主要的区别有哪些&#xff1f;各有哪些优缺点&#xff1f; ✅Java和C分别代表两种类型的语言✅ C是编译型语言✅ Java是解释型语言✅ 两者更多的主要区别如下&#xff1a; ✅知识拓展✅Java与C的参数方法有什么区别&#xff1f; ✅Java和C分别代表两种类型的语言 Java…

生成小程序URLlink链接遇到的坑

这里写自定义目录标题 前端生成小程序URL link背景用户打开小程序的常用方法短链接短链接优缺点优点缺点 生成短链接步骤 可能会遇到的问题&#xff1a;其他 注意&#x1f4e2; 前端生成小程序URL link ![h5打开小程序](https://img-blog.csdnimg.cn/direct/a4cfe3ef6d184c6d9…

打造微信私域有什么优势?

随着线上竞争愈发激烈&#xff0c;获客成本逐步攀升&#xff0c;越来越多的企业都开始打造属于自己的私域流量池。而作为目前全球拥有超过10亿活跃用户的平台&#xff0c;微信无疑是构建私域的理想选择。那么使用微信来打造私域流量有哪些优势呢? 打造微信私域的优势包括但不…

【Docker六】Docker-consul

目录 一、docker-consul概述 1、服务注册和发现&#xff1a; 1.1、服务注册和发现概念 1.2、服务注册和发现工作机制&#xff1a; 1.3、服务注册与发现的优点&#xff1a; 2、docker-consul概念 2.1、consul的主要特点&#xff1a; 二、consul架构部署&#xff1a; 1、…

VS Code连接远程Linux服务器调试MPI程序

1.在 VS Code 上安装扩展 C/C 2.通过 VS Code 连接远程 Linux 服务器 3.通过 VS Code 在远程 Linux 服务器上安装扩展 C/C 4.打开远程 Linux 服务器上的文件夹 【注】本文以 /root/ 为例。 5.创建项目文件夹&#xff0c;并在项目文件夹下创建MPI程序 6.点击左侧菜单栏的…

Docker构建镜像时空间不足:/var/lib/docker,no space left on device

背景 在一次更新业务服务功能后&#xff0c;重新在服务器上构建微服务镜像&#xff0c;在构建镜像时报错空间不足&#xff1a; /var/lib/docker, no space left on device 赶紧用 df -h 看了下磁盘使用情况&#xff0c;果然&#xff0c; devicemapper 已经满了。。由于需要紧急…

PyQt6 QToolBar工具栏控件

锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计44条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话版…

数据库 02-03 补充 SQL的子查询(where,from),子查询作为集合来比较some,exists,all(某一个,存在,所有)

子查询&#xff1a; where字句的子查询&#xff1a; 通常用in关键字&#xff1a; 举个例子&#xff1a; in关键字&#xff1a; not in 关键字&#xff1a; in 也可以用于枚举集合&#xff1a; where中可以用子查询来作为集合来筛选元祖。 some&#xff0c;all的运算符号…

【JavaScript】闭包的理解

闭包是指在一个函数内部创建另一个函数&#xff0c;并且内部函数可以访问外部函数的变量、参数以及其他内部函数&#xff0c;即使外部函数已经执行完毕。这种机制使得内部函数保留了对外部作用域的引用&#xff0c;即使外部作用域已经不再活跃。 为什么闭包重要&#xff1f; 闭…

Unity | Shader基础知识(第六集:语法<如何加入外部颜色资源>)

目录 一、本节介绍 1 上集回顾 2 本节介绍 二、语法结构 1 复习 2 理论知识 3 Shader里声明的写法 4 Properties和SubShader毕竟不是一家人 三、 片元着色器中使用资源 四、代码实现 五、全部代码 六、下集介绍 相关阅读 Unity - Manual: Writing Surface Shaders…