文章目录
- 1、什么是AOP
- 2、AOP的相关概念解释
- 3、基于注解的Spring AOP示例代码解析
- 1、定义一个切面
- 2、定义一个切入点
- 3、定义通知
- 4、业务逻辑类和启动类
- 4、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的相关概念解释
- Aspect 切面,实现特定功能的切入点和通知的集合,例如实现日志功能的切面
- PointCut 切入点,限定在哪些类的哪些方法上执行切入逻辑
- Advice 通知,限定切入逻辑的执行时机,在原有逻辑之前执行还是原有逻辑之后执行,或是在抛出异常时执行。通知包含切入点
- JoinPoint 连接点,被通知拦截的业务逻辑方法
- Introduction 引入, 给被通知拦截的业务类增加一个新的方法
- Target object 目标对象,通知要拦截的业务逻辑对象
- AOP proxy AOP代理,AOP框架创建的实现通知拦截功能的代理类,Spring中代理要么是JDK代理要么是CGLIB代理
- 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来使用,代理方法就会被调用。