切面编程(1): AOP

1 简述

切面编程AOP,是通过一种非侵入式方式,对目标类的方法进行影响或逻辑增强。通过AOP框架,可以更简洁的解决该问题。

切面编程,常给人一种感觉,它可以不知不觉对当前类的方法,做后期增强、补充处理。

切面编程真正的价值,在于它也是一种抽象设计方法,就像面向对象编程的核心是对的抽象、设计。切面编程也一样,核心是对切面的抽象、设计。

spring aop只是解决"切面"问题的一种方法、工具。就像C语言,也可以进行面向对象的设计、开发,后续文章会介绍其它基于"切面思想"的手段、方法。

下面是两种设计方法的定义:
OOP是对业务流程中对象(属性、方法)的抽象封装,基本单位是
AOP是对业务流程中横向逻辑的提取封装,基本单位是切面

在抽象设计中,最核心的是切面,但二者都必须基于业务、流程的设计、定义。

spring aop构建在动态代理基础之上,通过JDK动态代理和CGLIB两种方式来实现代理类。
代理类可以对目标类进行业务逻辑增强,也就是织入切面逻辑。

注:spring aop只针对类的方法进行逻辑增强。

2 术语

像大多数技术一样,切面编程也有自己特有的术语,如:连接点、切点、通知、切面、织入等,有些不太直观。

这些术语主要是想表达,在何地何时,织入增量逻辑。一般情况下,只需要理解切点、通知、切面这3个核心术语就足够了。

切点(pointcut)
它解决在何地,也就是在那些类的方法,织入切面逻辑,它的主要职责就是,通过类、方法、参数3个维度,以及这3个维度上的注解,来具体的匹配、定位类的方法。

通知(advice)
它解决在何时,也就是切点所匹配方法执行的前、后、异常等时候,执行切面逻辑。

切面(aspect)
可以这么理解,通知和切点,以及增量逻辑,共同定义了切面的全部内容,也就是,它是什么,在何时、何处完成其切面逻辑。

简化公式:切面 = 切点 + 通知 + 增量逻辑。

3 规则

Spring可以通过 xml配置 或 @Aspect注解 两种风格,来定义切面。

@Aspect注解风格,是将切面作为带有切面注解的普通Java类来声明的一种风格。

以下是具体规则。

3.1 切点

它是解决在什么地方,也就是如何来定位类的方法的范围问题,这就需要用到切点表达式,以下是规则:

execution: 用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切点指定器。
within: 将匹配限制在某些类型内的连接点(使用Spring AOP时,执行在匹配类型内声明的方法)。
this: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。
target: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。
args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中参数是给定类型的实例。

@target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中执行对象的类有一个给定类型的注解。
@args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中实际传递的参数的运行时类型有给定类型的注解。
@within: 将匹配限制在具有给定注解的类型中的连接点(使用Spring AOP时,执行在具有给定注解的类型中声明的方法)。
@annotation: 将匹配限制在连接点的主体(Spring AOP中正在运行的方法)具有给定注解的连接点上。

上面是官方文档的定义,看着有些晕。

本质理解,就是如何通过类、方法、参数等维度,让切点去匹配具体的method,归纳如下:

a. 通过execution表达式来匹配方法,@annotation表达式来匹配方法的注解。(最常用)
b. 通过within表达式来匹配,@within表达式来匹配类的注解
c. 通过args表达式来匹配方法参数实例,@args表达式来匹配方法参数类型上的注解。(区别于在方法上定义的参数注解)

3.2 通知

它是解决在什么时候,也就是来定义,在切点所匹配的方法,执行的前、后、异常等时候,来执行切面逻辑,以下是规则:

@Before: 通知方法会在目标方法之前执行。
@After:通知方法会在目标方法正常返回或抛出异常后执行。
@AfterReturning: 通知方法会在目标方法正常返回后执行。 
@AfterThrowing: 通知方法会在目标方法抛异常后执行。 
@Around: 通知方法会将目标方法封装起来。

3 实践

3.1 配置

开启
启用 @Aspect注解 支持,需要添加 @EnableAspectJAutoProxy注解 配置,如下:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig { }

提示:spring boot项目已默认打开,不添加,@Aspect也会起作用。

依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3.2 例子-切点

匹配类、接口、方法:

	/*
	 * 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法 。
	 */
	@Pointcut("execution(public * mtr.demo..*.hello(..))")
	public void pointCut1() {
	}
	
	/*
	 * 匹配方法: 仅匹配 mtr.demo包下,所有bean的hello方法 。
	 */
	@Pointcut("execution(public * mtr.demo.*.hello(..))")
	public void pointCut2() {
	}
	
	/*
	 * 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法, 且第一个参数必须是String类型。
	 */
	@Pointcut("execution(public * mtr.demo..*.hello(String, ..))")
	public void pointCut3() {
	}
	
	/*
	 * 匹配类: 匹配包 mtr.demo、及子包下,所有bean的方法
	 */	
	@Pointcut("within(mtr.demo..*)")
	public void pointCut4() {
	}
	
	/*
	 * 匹配接口: 匹配包 mtr.demo、及子包下,所有实现UserService接口bean的方法
	 */
	@Pointcut("within(mtr.demo..*) && this(mtr.demo.service.UserService)")
	public void pointCut5() {
	}
	
	/*
	 * 组合形式
	 */
	@Pointcut("pointCut1() && pointCut5()")
	public void pointCut6() {
	}

匹配注解:

	/*
	 * 匹配方法的注解: 所有bean中被RequestMapping注解的方法
	 */
	@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
	public void pointCut6() {
	}
	
	/*
	 * 匹配类的注解: 所有被RequestMapping注解的bean中的方法
	 */
	@Pointcut("@within(org.springframework.web.bind.annotation.RequestMapping)")
	public void pointCut7() {
	}

3.3 例子-通知

不传递参数:

@Component
@Aspect
public class DemoAspect1 {
	
	/*
	 * 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法 。
	 */	
    @Pointcut("execution(public * mtr.demo..*.hello(..))")
    public void pointCut1(){}
    
    @Before("pointCut1()")
    public void doBefore(JoinPoint joinPoint) {
    	System.out.println("Aspect-doBefore,this.class:" + joinPoint.getThis().getClass());
    }
    
    @After("pointCut1()")
    public void doAfter(JoinPoint joinPoint) {
    	System.out.println("Aspect-doAfter,this.class:" + joinPoint.getThis().getClass());
    }
    
    @AfterReturning(pointcut="pointCut1()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
    	System.out.println("Aspect-doAfterReturning,result:" + result);
    }
    
    @AfterThrowing(pointcut="pointCut1()", throwing="t")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable t) {
    	System.out.println("Aspect-doAfterThrowing,t:" + t.getMessage());
    }
    
    @Around("pointCut1()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
    	Object proceed = null;
		try {
			System.out.println("Aspect-doAround-begin,this.class:" + joinPoint.getThis().getClass());
			proceed = joinPoint.proceed();
			System.out.println("Aspect-doAround-end,this.class:" + joinPoint.getThis().getClass());
		} catch (Throwable e) {
			System.out.println("Aspect-doAround-exception,this.class:" + joinPoint.getThis().getClass());
			throw e;
		}
        return proceed;
    }    
}

传递参数:

@Component
@Aspect
public class DemoAspect2 {
	
	/*
	 * 匹配方法: 匹配mtr.demo.service包、及子包下,所有bean的方法, 且第一个参数必须是String类型。
	 */
	@Pointcut("execution(public * mtr.demo.service..*.*(..)) && args(x,..)")
    public void pointCut1(String x){}
    
    @Before("pointCut1(x)")
    public void doBefore(JoinPoint joinPoint, String x) {
    	System.out.println("Aspect-doBefore,parameter x:" + x);
    }
    
    @After("pointCut1(x)")
    public void doAfter(JoinPoint joinPoint, String x) {
    	System.out.println("Aspect-doAfter,parameter x:" + x);
    }  
}

4 案例

4.1 注入userId

web项目,常需要从会话中解析出userId,然后提供给后续的业务逻辑,以下通过AOP的方式,来处理该问题。

思路:新增一个注解@UserId,然后通过该注解,标记需要接收UserID参数的方法,最后通过AOP的方式,来注入userId值,以下是处理逻辑。

@UserId注解类

@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
	
	String value() default "";
}

切面UserAspect类,负责拦截web接口方法,注入userId值。

@Component
@Aspect
public class UserAspect {
		
	@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)"
			+ "|| @annotation(org.springframework.web.bind.annotation.GetMapping)"
			+ "|| @annotation(org.springframework.web.bind.annotation.PostMapping)"
			+ "|| @annotation(org.springframework.web.bind.annotation.PutMapping)"
			+ "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"						
			)
    public void pointCutForUserId() { }
	
	@Around("pointCutForUserId()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
    	Object result = null;
		try {
			// 请求参数
			Object[] args = joinPoint.getArgs();
			
			// 注入userId
			int indexOfUserId = this.getIndexOfUserIdParameter(joinPoint);
			if (indexOfUserId > -1) {
				args[indexOfUserId] = this.parseUserIdFromSession();
			}

			result = joinPoint.proceed(args);
		} catch (Throwable e) {
			throw e;
		}
        return result;
    }
    
    private int getIndexOfUserIdParameter(ProceedingJoinPoint joinPoint) {
		MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
		Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations();
    	
    	for(int i = 0; i < parameterAnnotations.length; i++) {
    		for(int j = 0; j < parameterAnnotations[i].length; j++) {
    			Annotation a = parameterAnnotations[i][j];
    			if (a.annotationType() == UserId.class) {
    				return i;
    			}    			
    		}
    	}
    	
    	return -1;
    }
    
    private Integer parseUserIdFromSession() {
    	return 123;
    }
}

web接口类UserController

@RestController
@RequestMapping("/user")
public class UserController {

	@GetMapping(value="/get")
	@ResponseBody
	public User get(@UserId Integer userId) {
		System.out.println("paramter userId: " + userId);
		return new User(userId);
	}

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值