Spring AOP示例以及原理揭秘

1、什么是AOP

AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。

AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

2、AOP的相关概念解释

  1. Aspect 切面,实现特定功能的切入点和通知的集合,例如实现日志功能的切面

  2. PointCut 切入点,限定在哪些类的哪些方法上执行切入逻辑

  3. Advice 通知,限定切入逻辑的执行时机,在原有逻辑之前执行还是原有逻辑之后执行,或是在抛出异常时执行。通知包含切入点

  4. JoinPoint 连接点,被通知拦截的业务逻辑方法

  5. Introduction 引入, 给被通知拦截的业务类增加一个新的方法

  6. Target object 目标对象,通知要拦截的业务逻辑对象

  7. AOP proxy AOP代理,AOP框架创建的实现通知拦截功能的代理类,Spring中代理要么是JDK代理要么是CGLIB代理

  8. Weave 编织,将通知和通知拦截的类连接起来的过程,可以在编译时进行编织也可以在运行时进行编织

一个特定横向切割多个类的功能使用一个切面实现,一个切面中可以定义多个通知,每个通知中都要包含切入点。切入点指定被拦截方法的匹配规则,而通知则在这些匹配规则的基础上指定了拦截逻辑的执行时机。AOP代理生效后,连接点的执行就会被通知拦截。

3、基于注解的Spring AOP示例代码解析

1、定义一个切面
@Aspect
@Component
public class LogAspect {

使用@Aspect注解定义一个切面,使用@Componet注解把这个切面交给Spring容器管理。

2、定义一个切入点
@Pointcut(value = "execution(public * com.ncepu.cloudyispringframework.aopdemo.service.*.*(..))")
    private void allPublicServiceMethods() {}

切面定义完成后,就可以在切面中定义切入点。使用@Pointcut定义切入点。定义切入点时,需要在@Pointcut注解中使用选择器提供一个匹配规则来匹配被拦截的方法。

Spring AOP支持如下选择器:

  1. execution: 匹配连接点
  2. within: 某个类里面
  3. this: 指定AOP代理类的类型
  4. target:指定目标对象的类型
  5. args: 指定参数的类型
  6. bean:指定特定的bean名称,可以使用通配符(Spring自带的)
  7. @target: 带有指定注解的类型
  8. @args: 指定运行时传的参数带有指定的注解
  9. @within: 匹配使用指定注解的类
  10. @annotation:指定方法所应用的注解

这里主要说明下execution选择器。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

execution(方法修饰符(可选)  返回类型  类路径(可选) 方法名  参数  异常模式(可选)) 

execution选择器语法主要包含了权限修饰符、返回类型、全限定类型、方法名、参数、抛出的异常。

其中,返回类型、方法名、参数是必选的。再回头看看示例代码中的选择器。

execution(public * com.ncepu.cloudyispringframework.aopdemo.service.*.*(..))

匹配了所有修饰符是public,在com.ncepu.cloudyispringframework.aopdemo.service报下,任意类名,任意方法名,0个或多个参数的方法。

3、定义通知
// 方法执行之前打印进入日志
    @Before("com.ncepu.cloudyispringframework.aopdemo.LogAspect.allPublicServiceMethods()")
    public void enterMethodLogger(JoinPoint joinPoint) {
        Class<?> aClass = joinPoint.getTarget().getClass();
        Logger logger = getLogger(aClass);

        logger.info("before");
    }

上述代码使用@Before注解定义了一个通知

com.ncepu.cloudyispringframework.aopdemo.LogAspect.allPublicServiceMethods()

这段代码是在通知中引用了之前定义的切入点,之前定义切入点的时候是将@PointCut注解放在allPublicServiceMethods方法上,所以这里直接引用这个方法就引用了这个切入点。

除了@Before通知,我还定义了@Around和@After通知

 // 方法执行之后打印日志
    @After("com.ncepu.cloudyispringframeworkdemo.aopdemo.LogAspect.allPublicServiceMethods()")
    public void exitMethodLogger(JoinPoint joinPoint) {
        Class<?> aClass = joinPoint.getTarget().getClass();
        Logger logger = getLogger(aClass);

        logger.info("after");
    }

    @Around("com.ncepu.cloudyispringframeworkdemo.aopdemo.LogAspect.allPublicServiceMethods()"
            + "&& args(sleepTimeInMillisecond, ..)")
    public void runTimeRecorder(ProceedingJoinPoint pjp, Long sleepTimeInMillisecond) throws Throwable {
        Class<?> aClass = pjp.getTarget().getClass();
        Logger logger = getLogger(aClass);
        logger.info("around starts");

        long start = System.currentTimeMillis();
        try {
            pjp.proceed();
        } catch (Throwable throwable) {
            logger.info("exception caught in runTimeRecorder");
        }
//        pjp.proceed();
        long end = System.currentTimeMillis();

        logger.info("around ends");
    }

切面、切入点通知都定义好了,现在只需要写一个简单的业务逻辑,写一个启动类就可以完成这个AOP的示例了。

4、业务逻辑类和启动类

业务逻辑类:

@Service("springAOPDemoService")
public class SpringAOPDemoService {

    public void wakeUpSleepyJoe(Long sleepTimeInMillisecond) {
        System.out.println("wake up joe!");
        try {
            Thread.sleep(sleepTimeInMillisecond);
//            throw new IllegalStateException("Joe does not want to wake up!");
        } catch (InterruptedException e) {
            System.out.println();
        }/* catch (IllegalStateException e) {
            System.out.println("exception caught in wakeUpSleepyJoe");
        }*/
        System.out.println("joe is wake");
    }
}

启动类:

public class Starter {
    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);

        SpringAOPDemoService springAOPDemoService = context.getBean("springAOPDemoService", SpringAOPDemoService.class);
        springAOPDemoService.wakeUpSleepyJoe(500L);
    }
}

输出日志:

[main] INFO com.ncepu.cloudyispringframeworkdemo.aopdemo.service.SpringAOPDemoService - around starts
[main] INFO com.ncepu.cloudyispringframeworkdemo.aopdemo.service.SpringAOPDemoService - before
[main] INFO com.ncepu.cloudyispringframeworkdemo.aopdemo.IntroductionDemoImpl - introduction method
wake up joe!
joe is wake
[main] INFO com.ncepu.cloudyispringframeworkdemo.aopdemo.service.SpringAOPDemoService - around ends
[main] INFO com.ncepu.cloudyispringframeworkdemo.aopdemo.service.SpringAOPDemoService - after

示例代码可以到, https://github.com/muou0213/cloudyi-spring-framework-demo.git 下载

从输出日志可以看出来,各中不同的通知类型的执行顺序:
Around proceed()之前代码->Before->Around proceed()之后代码->After

其实从日志中还能看出来代码中使用Introduction(引入)功能,这里就不详细介绍了,感兴趣的可以自己从github上下载代码看看,https://github.com/muou0213/cloudyi-spring-framework-demo.git。

4、Spring AOP实现原理

在Spring容器的基础上实现AOP功能,Spring AOP主要是通过实现BeanPostProcessor接口的postProcessAfterInitialization()方法在Bean注入到Spring容器之后使用代理对象对Bean进行包装.BeanPostProcessor接口是Spring提供的一个对新生成的Bean示例进行再加工的接口,具体信息大家可以自行百度。

Spring AOP中实现这个postProcessAfterInitialization()方法代码如下:

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (this.earlyProxyReferences.remove(cacheKey) != bean) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

上述代码中的wrapIfNecessary(bean, beanName, cacheKey)就是使用代理对bean进行包装。

wrapIfNecessary函数代码如下:

org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}

		// Create proxy if we have advice.
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

wrapIfNecessary函数先判断是否需要对这个bean进行包装,如果需要则调用createProxy()函数进行包装。

在执行createProxy函数之前,需要选择一种动态代理的实现方式,Spring AOP支持JDK和Cglib。选择实现方式的代码如下:

org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy

@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

从上面的代码可以看出,整个选择的过程是通过一个if/else完成的。if如果成立则使用Cglib,否则使用Jdk的动态代理。

下面以JdkDynamicAopProxy为例,看看代理生成过程

org.springframework.aop.framework.JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)

public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isTraceEnabled()) {
			logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
		}
		Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
		findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
		return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
	}

Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);这行代码跟我们自己调用JDK代理使用的代码是一样的。

通过上述的整个流程,Spring AOP就将Spring容器中生成的bean处理成了一个代理bean。之后我们再从Spring容器中取出这个Bean来使用,代理方法就会被调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值