SpringAOP面向切面编程应用

前言

一说Spring AOP大家肯定不陌生,它作为Spring Framwork的两大基石之一,在Spring的产品线中有着大量的应用。相信小伙伴们在平时工作的项目中,自己也写过类似的AOP代码。

那么本文主要从Spring AOP运行过程上,结合一定的源码整体上介绍Spring AOP的一个运行过程。
知其然,知其所以然,相信我们使用起来才更有底气。

什么是AOP

AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。

请注意,本文所指、所讲的AOP,只代表AOP在Spring中的应用

AOP能做什么

Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。

比如:登录校验、日志输出等等。。。

前面我们已经大篇幅得聊了Spring IoC技术的原理和源码分析,那么本篇文章将从实践到理论上分析一下Spring得另外一大强大基石技术:Spring AOP

环境准备

导入jar包,基于之前得基础spring-web基础环境,还需导入如下jar包

<!-- AOP相关Jar包 此包其实一般都可以不显导入,但aspectjweaver这个jar是必须的-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>${spring.framework.version}</version>
</dependency>
<dependency>
    <!-- 大名鼎鼎得@Aspect/@Pointcut系列的注解 都在此处 -->
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

对于jar包的导入,这里做一个说明:像spring-core、spring-context、spring-beans这几个基础的jar包,一般都是不需要显示的导入的。因为只要你使用到了它的功能包比如spring-web、spring-aop等都会自动帮你导入进来。

另外说明一点:导入spring-context就自动导入了aop包。而我们最常导入的spring-webmv包,它其实包含了非常多的jar,都不建议再重复导入了。放过pom.xml,让它更清爽点吧
创建一个切面:

一、Spring AOP的实用

@Component
@Aspect
public class HelloAspect {

    @Pointcut("execution(public int an.demo.Calculator.div(..))") //抽取公共切入点 
     public void Pointcut() {
     }

     @Before("Pointcut()")//前置通知
     public void before(JoinPoint joinPoint) {
           System.out.println(joinPoint.getSignature().getName()+
                     " :运行(@Before),参数是:{"+Arrays.toString(joinPoint.getArgs())+"}");
     }
     
     @After("Pointcut())//后置通知
     public void after(JoinPoint joinPoint) {
           System.out.println(joinPoint.getSignature().getName()+
                     " :结束(@After),参数是:{"+Arrays.toString(joinPoint.getArgs())+"}");
     }
     @AfterReturning(value="Pointcut()",returning="result”)//返回值通知
     public void logReturn(JoinPoint joinPoint,Object result) {
     //注意JoinPoint入参必须放在第一个,否则报错
           System.out.println(joinPoint.getSignature().getName()+
                     " :正常结束(@AfterReturning),返回值是:{"+result+"}");
     }
     @AfterThrowing(value="Pointcut()",throwing="exception”)//异常通知
     public void logException(JoinPoint joinPoint,Exception exception) {
           System.out.println(joinPoint.getSignature().getName()+
                     " :异常结束(@AfterThrowing),异常是:{"+exception+"}");
     }
     
     @Around("execution(public int an.demo.Calculator.div(..)))//环绕通知
     public Object around(ProceedingJoinPoint  joinPoint) throws Throwable {
           System.out.println("环绕前(@Around)");
           Object result = joinPoint.proceed();
           System.out.println("环绕后(@Around),结果是:"+result);
           return result;
     }
}

注意:Spring自己没有定义关于切面相关的注解,而是使用来自org.aspectj这个Jar包里面的注解的(但是没有用它的技术解析,这点需要明白)org.aspectj.lang.annotation还有很多其余的注解,但是这里列出Spring只支持的注解如下:
@Aspect、@Before、@After、@AfterReturning、@AfterThrowing、@Around 其余的注解Spring都是不予解析的(由AspectJ内部技术去解析)

开启AspectJ得支持

@ComponentScan(value = {"demo"})
@Configuration
@EnableAspectJAutoProxy
public class RootConfig {
}

二、@Pointcut切入点表达式全面使用介绍

标准的AspectJ Aop的pointcut的表达式类型是很丰富的,但是Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是11(10+1)种类型的表达式,分别如下:

  • execution:一般用于指定方法的执行,用的最多

  • within:是用来匹配类的,指定类中的所有方法将被拦截 ,也可用来指定一个包

  • this代理对象为指定的类型会被拦截

  • target目标对象为指定的类型被拦截

  • args:用来匹配方法参数的。

  • @target:匹配的目标对象的类有一个指定的注解,在接口上声明的对它不起作用

  • @args:方法参数所属的类型上有指定的注解,被匹配

  • @within:判断被调用的方法所属的类中是否声明了注解,如果有,会被拦截

  • @annotation:用于匹配方法上拥有指定注解的情况。

Pointcut定义时,还可以使用&&、||、! 这三个运算。进行逻辑运算。可以把各种条件组合起来使用

2.1 execution

语法:注解? 修饰符? 返回值类型 类路径匹配 ?方法名(参数列表) 异常列表?

注解:`可选`,方法上持有的注解,如@Deprecated;
修饰符:`可选`,如publicprotected;
返回值类型:`必填`,可以是任何类型模式;* 表示所有类型;
类路径匹配:`可选`,可以是任何类型模式;
方法名:`必填`,可以使用 *  进行模式匹配;
参数列表:`必填`, 其规则如下
                ()   表示方法没有任何参数;
                (..)   表示匹配接受任意个参数的方法,
                (..,java.lang.String)   表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法;
                (java.lang.String,..)    表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法;
                (*,java.lang.String)    表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法;
                
异常列表;可选,以“throws 异常全限定名列表”声明,异常全限定名列表如有多个以“,”分割

类型匹配语法
*:匹配任何数量字符;
..:匹配任何数量字符的重复,如在类路径匹配中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

案例

1、execution(public * *(..)) 拦截任意公共方法

2、execution(* set*(..)) 拦截以set开头的任意方法

3、execution(* com.xyz.service.AccountService.*(..)) 拦截AccountService(类、接口)中定义的所有方法

4、execution(* com.xyz.service.*.*(..)) 拦截com.xyz.service包中所有类中任意方法,不包含子包中的类

5、execution(* com.xyz.service..*.*(..)) 拦截com.xyz.service包或者子包中定义的所有方法

2.2 within

作用:是用来指定类型的,指定类型中的所有方法将被拦截

案例

within(com.xyz.service..*) 拦截service包及子包中任意类的任意方法

2.3 this

作用代理对象为指定的类型会被拦截

案例

public interface HelloService { void hello();}

@Component
public class HelloServiceImpl implements HelloService {
	public   void hello() {
		System.out.println("调用hello方法...");		
	}
}


@Aspect
@Component
public class MyAspect  {

	@Pointcut("this(aop.HelloServiceImpl)")
	private void pointcut() { }

	@Before(value="pointcut()")
	public void before(JoinPoint joinPoint){
		System.out.println("执行before方法....");
	}
}

@ComponentScan(value = {"aop"})
@Configuration
@EnableAspectJAutoProxy
public class RootConfig {     }

public class App {
	public static void main( String[] args ) {
		AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(RootConfig.class);
		System.out.println(annotationConfigApplicationContext.getBean("helloServiceImpl").getClass());
		HelloService helloService = ((HelloService)annotationConfigApplicationContext.getBean("helloServiceImpl"));
		helloService.hello();
		System.out.println(HelloServiceImpl.class.isAssignableFrom(helloService.getClass()));
	}
}

运行输出:

class com.sun.proxy.$Proxy18
调用hello方法...
false

原因分析
@EnableAspectJAutoProxy的属性proxyTargetClass在默认情况下为fasle,其表示若spring创建的对象如果实现了接口,默认使用jdk动态>代理,如果没有实现接口,使用cglib创建代理对象
所以 helloService 是使用jdk动态代理生成的对象,HelloServiceImpl.class.isAssignableFrom(helloService.getClass() 为 false

如果把配置类改成这样

@ComponentScan(value = {"aop"})
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RootConfig {

}

运行输出:

class aop.HelloServiceImpl$$EnhancerBySpringCGLIB$$36334b64
执行before方法....
调用hello方法...
true

原因分析:helloService 为 HelloServiceImpl类型的对象,所以会被拦截

2.4 target

作用:目标对象为指定的类型被拦截

案例

public interface HelloService { void hello();}

@Component
public class HelloServiceImpl implements HelloService {
	public   void hello() {
		System.out.println("调用hello方法...");		
	}
}


@Aspect
@Component
public class MyAspect  {

	@Pointcut("target(aop.HelloServiceImpl)")
	private void pointcut() { }

	@Before(value="pointcut()")
	public void before(JoinPoint joinPoint){
		System.out.println("执行before方法....");
	}
}

@ComponentScan(value = {"aop"})
@Configuration
@EnableAspectJAutoProxy
public class RootConfig {     }

public class App {
	public static void main( String[] args ) {
		AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(RootConfig.class);
		System.out.println(annotationConfigApplicationContext.getBean("helloServiceImpl").getClass());
		HelloService helloService = ((HelloService)annotationConfigApplicationContext.getBean("helloServiceImpl"));
		helloService.hello();
		System.out.println(HelloServiceImpl.class.isAssignableFrom(helloService.getClass()));
	}
}

运行输出:

class com.sun.proxy.$Proxy18
执行before方法....
调用hello方法...
false

this 和 target 的不同点
this作用于代理对象,target作用于目标对象
this表示目标对象被代理之后生成的代理对象和指定的类型匹配会被拦截,匹配的是代理对象
target表示目标对象和指定的类型匹配会被拦截,匹配的是目标对象

2.5 args

作用:用来匹配方法参数的。

案例
1、args() 匹配任何不带参数的方法。
2、args(java.lang.String) 匹配任何只带一个参数,而且这个参数的类型是String的方法。
3、args(…) 带任意参数的方法。
4、args(java.lang.String,…) 匹配带任意个参数,但是第一个参数的类型是String的方法。
5、args(…,java.lang.String) 匹配带任意个参数,但是最后一个参数的类型是String的方法。

2.6 @target

作用:匹配的目标对象的类有一个指定的注解,在接口上声明的对它不起作用

案例
1、@target(com.ms.aop.MyAnnotation) 匹配目前类上存在@MyAnnotation注解,调用该目标对象的任意方法都会被拦截

注意:如果父类匹配到了,不会影响到子类

2.7 @args

作用:方法参数所属的类型上有指定的注解,被匹配

案例
1、@args(com.ms.aop.jargs.demo1.Anno1) 匹配1个参数,且第1个参数所属的类中有Anno1注解

2、@args(com.ms.aop.jargs.demo1.Anno1,com.ms.aop.jargs.demo1.Anno2) 匹配2个参数,且2个参数所属的类型上都有指定的注解

3、@args(com.ms.aop.jargs.demo2.Anno1,…) 匹配多个参数,且第一个参数所属的类中有Anno1注解

2.8 @within

@within : 判断被调用的方法所属的类中是否声明了注解,如果有,会被拦截

比如有一个ClassA上使用了注解MyAnno标注,并且定义了一个方法a(),那么在调用ClassA.a()方法时将匹配该Pointcut;如果有一个ClassB上没有MyAnno注解,但是它继承自ClassA,同时它上面定义了一个方法b(),那么在调用ClassB().b()方法时不会匹配该Pointcut,但是在调用ClassB().a()时将匹配该方法调用,因为a()是定义在父类型ClassA上的,且ClassA上使用了MyAnno注解。但是如果子类ClassB覆写了父类ClassA的a()方法,则调用ClassB.a()方法时也不匹配该Pointcut。

案例

public interface HelloService {

	void hello();
}

@AN
public class Father implements HelloService {
	@Override
	public void hello() {
		System.out.println("调用hello方法...");
	}
}

@Component
public class HelloServiceImpl extends  Father implements  HelloService {

}

@Aspect
@Component
public class MyAspect  {

	@Pointcut("@within(aop.AN)")
	private void pointcut() { }


	@Before(value="pointcut()")
	public void before(JoinPoint joinPoint){
		System.out.println("执行before方法....");
	}

}

@ComponentScan(value = {"aop"})
@Configuration
@EnableAspectJAutoProxy
public class RootConfig {

}

public class App {


	public static void main( String[] args ) {


		AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(RootConfig.class);
		System.out.println(annotationConfigApplicationContext.getBean("helloServiceImpl").getClass());
		HelloService helloService = ((HelloService)annotationConfigApplicationContext.getBean("helloServiceImpl"));
		helloService.hello();


	}
}

运行输出

class com.sun.proxy.$Proxy19
执行before方法....
调用hello方法...

@target 和 @within 的不同点
@target(注解A):判断被调用的目标对象中是否声明了注解A,如果有,会被拦截

@within(注解A): 判断被调用的方法所属的类中是否声明了注解A,如果有,会被拦截

@target关注的是被调用的对象,@within关注的是调用的方法所在的类

2.9 @annotation

作用:用于匹配方法上拥有指定注解的情况。

案例
1、@annotation(com.fsx.run.anno.MyAnno) 方法上有@MyAnno注解的都会被匹配

三、@EnableAspectJAutoProxy原理浅析

基于注解的方式实现AOP需要在配置类中添加注解@EnableAspectJAutoProxy。也就是说,如果要使注解版的AOP功能起作用的话,那么就得需要在配置类上添加@EnableAspectJAutoProxy注解。我们先来看下@EnableAspectJAutoProxy注解的源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	 //若为true,表示强制指定了要使用CGLIB
	 //fasle:表示若spring创建的对象如果实现了接口,默认使用jdk动态代理,如果没有实现接口,使用cglib创建代理对象
	boolean proxyTargetClass() default false;
	
	boolean exposeProxy() default false;
}

看到@Import(AspectJAutoProxyRegistrar.class),我们应该知道其作用是注册AspectJAutoProxyRegistrar这个bean

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry) {

        //如果容器中存在不存在名字为org.springframework.aop.config.internalAutoProxyCreator的自动代理创建器
        //那么spring就默认创建一个AnnotationAwareAspectJAutoProxyCreator自动代理创建器
        //重要代码
		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			// 若为true,表示强制指定了要使用CGLIB,那就强制告知到时候使用CGLIB的动态代理方式
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			// 告知,强制暴露Bean的代理对象到AopContext
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}
}

AspectJAutoProxyRegistrar是一个ImportBeanDefinitionRegistrar,他会在容器中所有BeanDefinition注册后,执行他的registerBeanDefinitions方法,其代码中 AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry) 是核心代码

public abstract class AopConfigUtils {

	public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
				BeanDefinitionRegistry registry, @Nullable Object source) {
	
			return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
	}

	
	private static BeanDefinition registerOrEscalateApcAsRequired(
			Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

		// 很显然,这里如果我们自己定义了这样一个自动代理创建器,也是Ok的
		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
			BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
			if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
				int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
				int requiredPriority = findPriorityForClass(cls);
				if (currentPriority < requiredPriority) {
					apcDefinition.setBeanClassName(cls.getName());
				}
			}
			return null;
		}
       //若用户自己没有定义,那就用系统定义好的吧  AnnotationAwareAspectJAutoProxyCreator
		RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
		beanDefinition.setSource(source);
		// 此处注意,增加了一个属性:最高优先级执行
		beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
		registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
		return beanDefinition;
	}
}

这样一来,我们就成功的注入了一个Bean:AnnotationAwareAspectJAutoProxyCreator 基于注解的自动代理创建器
AnnotationAwareAspectJAutoProxyCreator是一个BeanPostProcessor,他是在其postProcessAfterInitialization方法中把对象进行代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值