SpringAOP技术【松思园】

一、SpringAOP理解

SpringAOP称之为“面向切面编程”,这和我们之前编程思想“面向对象编程”是不一样的,当然我们还听过“面向过程的编程”,这些都是一种编程思想,我们先来看一下他们之间的区别:
面向过程编程(opp):面向过程更接近于计算机底层的执行逻辑,关注的是“怎么去做”的过程,他会将一个任务拆分成各个小的步骤,主要通过那个函数进入,逐步的调用哪些函数去执行,着重于细节的过程性内容。
面向对象编程(oop):面向对象关注的是由谁来做,比较接近我们人类的认识,关注的是做任务的对象,他有什么样的特点,能干什么样的事情,通过对象之间的相关配合工作去完成一个个的任务。
面向切面编程(aop):aop是在oop的基础上,延伸而出的,他关注的是在已有的任务执行过程中,如何能动态的插入新的逻辑代码,上下游的相互配合或者单独执行功能,来玩新业务的需要,使得对原有逻辑的伤害到最小。
值得强调的是,很多项目的编程思想,并不是只有一种,他们之间相互配合达到编程的最优效果。

二、SpringAOP的基本概念

  • Aspect(切面):通常是一个类,里面定义切入点和通知;
  • JoinPoint(连接点):表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用;
  • Advice(通知):通知描述了切面何时执行以及如何执行增强处理;
  • Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式;

三、SpringAOP通知的类型

  • 前置通知(Before):在目标方法被调用之前的通知功能;
  • 后置通知(After):在目标方法执行完成后执行,如果目标方法异常,则后置通知不再执行;
  • 异常通知(After-throwing):目标方法抛出异常的时候执行;
  • 最终通知(finally);不管目标方法是否有异常都会执行,相当于try。。catch。。finally中的finally;
  • 环绕通知(round):可以控制目标方法是否执行;

四、JoinPoint 对象

JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。

/*获取参数的值数组*/
Object[] args = point.getArgs();   
                                
/*获取目标对象(被加强的对象)*/
Object target = point.getTarget();

/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
MethodSignature signature = (MethodSignature) point.getSignature();

/*方法名*/
String signatureName = signature.getName();     
                  
/*参数名称数组(与args参数值意义对应)*/
String[] parameterNames = signature.getParameterNames();         

/*获取执行的方法对应Method对象*/
Method method = signature.getMethod();                              

/*获取返回值类型*/
Class returnType = signature.getReturnType();                      

/*获取方法上的注解*/
WebAnnotation webAnnotation = method.getDeclaredAnnotation(WebAnnotation.class);


五、ProceedingJoinPoint 对象

Proceedingjoinpoint 继承了 JoinPoint ,是在JoinPoint的基础上暴露出 proceed 这个方法, 暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。

六、代码示例(基于注解的aop使用)

我们创建了一个maven项目,并通过注解的方式配置 spring aop。

1、pom中引入aop的依赖

<!--spring AOP的包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.1.RELEASE</version>
        </dependency>

2、定义目标类(切面类)

@Slf4j   // 日志框架 slf4j + log4j2
@Component   // 声明这是一个组件
@Aspect     // 声明这是一个切面 bean
public class LogAspect {

	// 日志框架 slf4j + log4j2
	public static final Logger logger =  LoggerFactory.getLogger(LogAspect.class);

	@Pointcut("execution(* springaop.service..*.*(..))")  // 切入点注解,定义程序中需要注入Advice的位置的集合,有十中类型的表达方式:
	/*
	* execution:一般用于指定方法的执行,用的最多。
    * within:指定某些类型的全部方法执行,也可用来指定一个包。
    * this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
    * target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
    * args:当执行的方法的参数是指定类型时生效。
    * @target:当代理的目标对象上拥有指定的注解时生效。
    * @args:当执行的方法参数类型上拥有指定的注解时生效。
    * @within:与@target类似,@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。
    * @annotation:当执行的方法上拥有指定的注解时生效。
    * bean:当调用的方法是指定的bean的方法时生效。
	*/
	public void pointcut() {
	}
}	

3、前置通知 @Before

前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。

/**
	 * 前置通知
	 **/
	@Before("pointcut()")
	public void before(JoinPoint joinPoint) {
		//获取参数
		/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		/*方法名*/
		String signatureName = signature.getName();
		/*参数名称数组(与args参数值意义对应)*/
		String[] parameterNames = signature.getParameterNames();
		/*获取参数的值数组*/
		Object[] args = joinPoint.getArgs();
		for (Object ss : args) {
			System.out.print("【参数】"+ss);
		}

		log.info("befor通知>>>【方法名】"+signatureName);
	}

4、后置通知 @After

在目标方法执行之后执行的通知。
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。

/**
	 * 后置通知
	 */
	@After("pointcut()")
	public void after() {
		log.info("after通知>>>>>>>>>>>>>>>>>>>>>>>>");
	}

5、后置通知——增强处理 @AfterReturning

AfterReturning增强处理将在目标方法正常完成后被织入,
(1) pointcut/value:这两个属性的作用是一样的,它们都属于指定切入点对应的切入表达式。一样既可以是已有的切入点,也可直接定义切入点表达式。当指定了pointcut属性值后,value属性值将会被覆盖。
(2) returning:该属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。

	/**
	 * 后置通知,增强处理
	 * */
	@AfterReturning(pointcut = "pointcut()", returning = "res")
	public void afterReturn(Object res) {
		log.info("afterReturning通知>>>>>>>>>>>>>>>>>>>>"+res);
	}

6、异常通知

在目标方法抛出异常时执行的通知
可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位
另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。

	/**
	 * 异常通知
	 * */
	@AfterThrowing(pointcut = "pointcut()",throwing = "err")
	public void afterThrow(Throwable err){
		log.info("afterThrowing>>>>>>>>>>>>>【异常:】" +err);
	}

7.环绕通知 @Around

在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。
要注意:只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。

	/**
	 * 环绕通知
	 */
	@Around("pointcut()")
	public void aroud(ProceedingJoinPoint proceedingJoinPoint) {
		Object res = null;
		log.info("aroud通知:begin>>>>>>>>>>>>>");
		// 业务参数
		Object[] args = proceedingJoinPoint.getArgs();
		try {
			// 手动执行真正的业务方法,传入业务参数
			res = proceedingJoinPoint.proceed(args);
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		log.info("aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】" + res);
	}

七、通知执行的先后顺序

LogAspect .class

package com.study.springaop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

/**
 * @project: javaDemoStudy
 * @author: 13965
 * @date:2021/3/1 9:48
 * @description:
 */
@Slf4j // 日志框架 slf4j + log4j2
@Component   // 声明这是一个组件
@Aspect     // 声明这是一个切面 bean
public class LogAspect {

	@Pointcut("execution(* com.study.springaop.service..*.*(..))")  // 切入点注解,定义程序中需要注入Advice的位置的集合,有十中类型的表达方式:
	/*
	 * execution:一般用于指定方法的执行,用的最多。
	 * within:指定某些类型的全部方法执行,也可用来指定一个包。
	 * this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
	 * target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
	 * args:当执行的方法的参数是指定类型时生效。
	 * @target:当代理的目标对象上拥有指定的注解时生效。
	 * @args:当执行的方法参数类型上拥有指定的注解时生效。
	 * @within:与@target类似,@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。
	 * @annotation:当执行的方法上拥有指定的注解时生效。
	 * bean:当调用的方法是指定的bean的方法时生效。
	 */
	public void pointcut() {
	}

	/**
	 * 前置通知
	 **/
	@Before("pointcut()")
	public void before(JoinPoint joinPoint) {
		//获取参数
		/*获取signature 该注解作用在方法上,强转为 MethodSignature*/
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		/*方法名*/
		String signatureName = signature.getName();
		/*参数名称数组(与args参数值意义对应)*/
		String[] parameterNames = signature.getParameterNames();
		/*获取参数的值数组*/
		Object[] args = joinPoint.getArgs();

		for (Object ss : args) {
			log.info("befor通知>>>>>>>>>>>>>>>>【方法名】" + signatureName+"【参数】" + ss);
		}
	}

	/**
	 * 后置通知
	 */
	@After("pointcut()")
	public void after() {
		log.info("after通知>>>>>>>>>>>>>>>>>>>>>>>>");
	}

	/**
	 * 后置通知,增强处理
	 */
	@AfterReturning(pointcut = "pointcut()", returning = "res")
	public void afterReturn(Object res) {
		log.info("afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】" + res);
	}

	/**
	 * 异常通知
	 */
	@AfterThrowing(pointcut = "pointcut()", throwing = "err")
	public void afterThrow(Throwable err) {
		log.info("afterThrowing通知>>>>>>>>>>>>>【异常:】" + err);
	}

	/**
	 * 环绕通知
	 */
	@Around("pointcut()")
	public void aroud(ProceedingJoinPoint proceedingJoinPoint) {
		Object res = null;
		log.info("aroud通知:begin>>>>>>>>>>>>>");
		// 业务参数
		Object[] args = proceedingJoinPoint.getArgs();
		try {
			// 手动执行真正的业务方法,传入业务参数
			res = proceedingJoinPoint.proceed(args);
		} catch (Throwable throwable) {
			throwable.printStackTrace();
		}
		log.info("aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】" + res);
	}

}

AopDemoService.calss service层业务逻辑

package com.study.springaop.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @project: javaDemoStudy
 * @author: 13965
 * @date:2021/3/1 10:29
 * @description:
 */
@Slf4j
@Service
public class AopDemoService {
	
	public String insert(String name) {
		log.info("方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】"+name);
		// int num = 10 / 0;
		return "这是【insert】的返回内容呢";
	}
}

AopDemoServiceTest.class 单元测试

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AopDemoServiceTest {

	@Autowired
	private AopDemoService aopDemoService;
	
	@Test
	public void insert() {
		aopDemoService.insert("demo");
	}

}

通过执行单元测试,我们可以看到,日志中的输出顺序;

2021-03-04 16:11:43.616  INFO 17964 --- [           main] c.s.s.service.AopDemoServiceTest         : Starting AopDemoServiceTest on LAPTOP-QEAM9EMC with PID 17964 (started by 13965 in C:\gitproject\studyProjectWorkSpace\java_syudy_moduls\springaop)
2021-03-04 16:11:43.619  INFO 17964 --- [           main] c.s.s.service.AopDemoServiceTest         : No active profile set, falling back to default profiles: default
2021-03-04 16:11:48.421  INFO 17964 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-03-04 16:11:49.583  INFO 17964 --- [           main] c.s.s.service.AopDemoServiceTest         : Started AopDemoServiceTest in 6.619 seconds (JVM running for 8.024)
2021-03-04 16:11:50.063  INFO 17964 --- [           main] com.study.springaop.LogAspect            : aroud通知:begin>>>>>>>>>>>>>
2021-03-04 16:11:50.068  INFO 17964 --- [           main] com.study.springaop.LogAspect            : befor通知>>>>>>>>>>>>>>>>【方法名】insert【参数】demo
2021-03-04 16:11:50.084  INFO 17964 --- [           main] c.s.springaop.service.AopDemoService     : 方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】demo
2021-03-04 16:11:50.084  INFO 17964 --- [           main] com.study.springaop.LogAspect            : aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】这是【insert】的返回内容呢
2021-03-04 16:11:50.084  INFO 17964 --- [           main] com.study.springaop.LogAspect            : after通知>>>>>>>>>>>>>>>>>>>>>>>>
2021-03-04 16:11:50.084  INFO 17964 --- [           main] com.study.springaop.LogAspect            : afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】null
2021-03-04 16:11:50.109  INFO 17964 --- [       Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

结论:在单个切面类,正常情况下,五大通知的执行顺序为:
环绕前置>>>>>@Before>>>>>目标方法执行>>>>环绕返回>>>>>环绕最终>>>>>@After>>>>>@AfterReturning;
异常的情况下:环绕前置>>>>@Before>>>>目标方法执行>>>>环绕异常>>>>环绕最终>>>>@After>>>>@AfterThrowing

八、通过注解定义切点

通过注解的方式,可以灵活的控制切入的位置,和切入的颗粒大小。
(1) 首先定义一个新的注解(这时项目中有两个切面类分别为:LogAspect 通过 execution 指定方法作为切入点;SecondAspect 通过@annotation 指定注解作为切入点):
MyselfAnnotationDemo.class

package com.study.springaop.annotation;
import com.study.springaop.enums.KeyCodesEnums;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyselfAnnotationDemo {
	// key值
	KeyCodesEnums key();
	// 名称
	String name();
	// 数据值
	String value() default "default";
}

(2) 修改切面类
SecondAspect.class

package com.study.springaop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @project: java_syudy_moduls
 * @author: 13965
 * @date:2021/3/9 10:04
 * @description:
 */
@Slf4j // 日志框架 slf4j + log4j2
@Component   // 声明这是一个组件
@Aspect     // 声明这是一个切面 bean
public class SecondAspect {
@Pointcut("@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")
	public void pointcut() {
	}
}

说明:这里我们使用@annotation 注解来指定 切入点的注入方式是通过 注解声明

(3) 业务service 定义一个方法,添加自定义的注解 MyselfAnnotationDemo

package com.study.springaop.service;

import com.study.springaop.annotation.MyselfAnnotationDemo;
import com.study.springaop.enums.KeyCodesEnums;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @project: javaDemoStudy
 * @author: 13965
 * @date:2021/3/1 10:29
 * @description:
 */
@Slf4j
@Service
public class AopDemoService {

	public String insert(String name) {
		log.info("方法执行过程处理逻辑+——+——+——+——+——+【参数name值为:】"+name);
		// int num = 10 / 0;
		return "这是【insert】的返回内容呢";
	}

// 添加自己定义的注解
	@MyselfAnnotationDemo(key = KeyCodesEnums.FRIDAY,name = "方法annotationMethod")
	public String annotationMethod(String params) {
		log.info("该方法添加注解++++++params" + params);
		return "添加注解的 annotationMethod 方法";
	}

}

说明:这里要注意,我们两个切面类目前都包含了 annotationMethod() 方法的切入点。

(4) 单元测试

@Test
	public void annotationMethodTest() {
		log.info("TEST_BEGIN======annotationMethodTest");
		aopDemoService.annotationMethod("GOOD");
		log.info("TEST_END======annotationMethodTest");
	}

输出结果:

TEST_BEGIN======annotationMethodTest
aroud通知:begin>>>>>>>>>>>>>
befor通知>>>>>>>>>>>>>>>>【方法名】annotationMethod【参数】GOOD
该方法添加注解++++++paramsGOOD
SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】添加注解的 annotationMethod 方法
aroud通知:end>>>>>>>>>>>>>【res业务返回结果=】添加注解的 annotationMethod 方法
after通知>>>>>>>>>>>>>>>>>>>>>>>>
afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】null
TEST_END======annotationMethodTest

说明:首先我们应该看到,通过注解的方式SecondAspect 切面的后置通知正常运行,除此之外,还可以看到,对于一个方法,通过execution指定方法,和通过annotation注解的形式,他们是可以同时运行的。我们也可以在 @Pointcut()表达式中指定切入点的时候 排除一些切入位置:

@Pointcut("execution(* com.study.springaop.service..*.*(..)) && !@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")

这样我们的annotationMethod() 方法,就只会被 SecondAspect 切面进行切入,而在execution表达式时被排除,可以看到单元测试的运行结果:

 TEST_BEGIN======annotationMethodTest
该方法添加注解++++++paramsGOOD
注解对象的解析:key=FRIDAY;name=方法annotationMethod;valuedefault
SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】添加注解的 annotationMethod 方法
TEST_END======annotationMethodTest

九、在切面类中解析注解切入点

package com.study.springaop;

import com.study.springaop.annotation.MyselfAnnotationDemo;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
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.reflect.Method;

/**
 * @project: java_syudy_moduls
 * @author: 13965
 * @date:2021/3/9 10:04
 * @description:
 */
@Slf4j // 日志框架 slf4j + log4j2
@Component   // 声明这是一个组件
@Aspect     // 声明这是一个切面 bean
public class SecondAspect {

@Pointcut("@annotation(com.study.springaop.annotation.MyselfAnnotationDemo)")
	public void pointcut() {
	}

	/**
	 * 后置通知,增强处理
	 */
	@AfterReturning(pointcut = "pointcut()", returning = "res")
	public void afterReturn(JoinPoint joinPoint,Object res) {

		// 从切面织入点处通过反射机制获取织入点处的方法
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		// 获取执行的方法对应Method对象
		Method method = signature.getMethod();
		// 获取方法上的注解
		MyselfAnnotationDemo myselfAnnotationDemo = method.getDeclaredAnnotation(MyselfAnnotationDemo.class);
		log.info("注解对象的解析:key=" + myselfAnnotationDemo.key() + ";name=" + myselfAnnotationDemo.name() + ";value" + myselfAnnotationDemo.value());
		log.info("SecondAspect+afterReturning通知>>>>>>>>>>>>>>>>>>>>【返回值:】" + res);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值