从约定编程到理解Spring AOP

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 想要理解Spring 源码光有OOP 的思想是不够的,还要有AOP 的思想武装自己,才能更好的理解Spring 的源码

概述

提到Spring 不得不赞叹其IOC 跟AOP 实现之精巧,设计之伟大。平时工作中我们也会通过AOP 去实现一些功能,如对接口环形日志打印,通用的防止重复提交的功能。我觉得第一次感受到AOP 的强大就是其对数据库事务的冗余代码的解放。


在了解Spring aop 之前我们先了解一下约定编程。


约定编程

下面基于Java Proxy实现一个小demo 体验一下什么是约定编程,其实这也是与AOP 有着异曲同工之妙的。

/*** 首先创建一个接口*/publicinterfaceUserService {
/*** say hello* @param name 名字*/voidsayHello(Stringname);
}
/*** 接着创建一个类实现接口*/publicclassUserServiceImplimplementsUserService{
@OverridepublicvoidsayHello(Stringname) {
System.out.println("name:"+name+" hello");
    }
}

以上是准备工作,接下来实现一个约定的接口

/*** 首先创建约定编程接口* 定义约定的方法*/publicinterfaceInterceptor {
/*** 事前方法** @return 是否*/booleanbefore();
/*** 事后方法*/voidafter();
/*** 取代原有事件方法** @param invocation 回调参数,可以通过proceed 方法 调用原有目标方法* @return 原有事件返回结果* @throws InvocationTargetException* @throws IllegalAccessException*/Objectaround(Invocationinvocation)throwsInvocationTargetException,IllegalAccessException;
/*** 事后返回方法,事件没有发生异常执行*/voidafterReturning();
/*** 事后异常方法,当事件发生异常后执行*/voidafterThrowing();
}
/*** 接着创建约定编程接口的实现* 具体实现自己想要的功能*/publicclassMyInterceptorimplementsInterceptor{
@Overridepublicbooleanbefore() {
log.info("before.........");
returntrue;
    }
@Overridepublicvoidafter() {
log.info("after ....");
    }
@OverridepublicObjectaround(Invocationinvocation) throwsInvocationTargetException, IllegalAccessException {
log.info("around before.........");
Objectproceed=invocation.proceed();
log.info("around after.........");
returnproceed;
    }
@OverridepublicvoidafterReturning() {
log.info("afterReturning.........");
    }
@OverridepublicvoidafterThrowing() {
log.info("afterThrowing.........");
    }
}
/*** Invocation 类封装具体反射调用方法*/publicclassInvocation {
privateObject[] params;
privateMethodmethod;
privateObjecttarget;
publicInvocation(Objecttarget,Methodmethod,Object[] params){
this.target=target;
this.method=method;
this.params=params;
    }
publicObjectproceed()throwsInvocationTargetException,IllegalAccessException{
returnmethod.invoke(target,params);
    }
}

接下来是重要的核心,使用Java Proxy 来实现动态代理,完成变身大法;

/*** Java Proxy 代理需要实现InvocationHandler 接口用来实现动态代理*/publicclassProxyBeanimplementsInvocationHandler {
/*** 该target变量 存放的是被代理的类*/privateObjecttarget=null;
/*** 该interceptor变量存放目标拦截器*/privateInterceptorinterceptor=null;
/*** 构建代理对象方法* * @param target 被代理的对象* @param interceptor 想要代理时运行的方法对象* @return 整合好的代理对象*/publicstaticObjectgetProxyBean(Objecttarget,Interceptorinterceptor){
ProxyBeanproxyBean=newProxyBean();
proxyBean.target=target;
proxyBean.interceptor=interceptor;
// 通过Proxy 的方法 生成一个代理对象Objectinstance=Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), proxyBean);
returninstance;
    }
/*** 该方法是代理对象在调用被代理对象方法时一定会调用的方法* 在此对被代理方法进行包装的主要逻辑,其实等同于aop 中织入的过程* * @param proxy 目标对象* @param method 代理方法对象* @param args 方法入参* @return 返回被代理的方法的结果* @throws Throwable 抛出异常*/@OverridepublicObjectinvoke(Objectproxy, Methodmethod, Object[] args) throwsThrowable {
booleanexceptionFlag=false;
Invocationinvocation=newInvocation(target,method,args);
Objectobj=null;
try{
if (this.interceptor.before()){
obj=this.interceptor.around(invocation);
            }else {
obj=method.invoke(target,args);
            }
        }catch (Exceptionex){
exceptionFlag=true;
        }
this.interceptor.after();
if (exceptionFlag){
this.interceptor.afterThrowing();
        }else {
this.interceptor.afterReturning();
returnobj;
        }
returnnull;
    }
}

接下来是测试方法

publicclassProxyTest {
publicstaticvoidmain(String[] args) {
UserServiceuserService=newUserServiceImpl();
UserServiceproxyBean= (UserService) ProxyBean.getProxyBean(userService, newMyInterceptor());
// 因为是通过Proxy 实现的代理对象,Java 保证代理对象在调用目标方式时,一定会调用其invoke 方法proxyBean.sayHello("小明");
    }
}

以上就是完成约定编程的案例,其实细心的读者应该也可以发现,我们定义的约定编程的接口,类似于aop 中的通知(事前通知,事后通知,环绕通知等),而MyInterceptor 类相当于我们的切面,构建代理对象时ProxyBean.getProxyBean 方法,像是将目标对象通进行了绑定,最终代理对象调用目标方法时相当于连接点通过动态代理技术及反射技术完成了织入,不过这些都由框架实现了对我们来说透明了,要想探究其本质,则需要深入源码,遨游一番。


Spring AOP 底层实现

AOP 术语介绍

  • 连接点(join point):对应的是具体被拦截的对象,因为Spring只能支持方法,所以被拦截的对象往往就是指特定的方法;
  • 切点(point cut):有时候,切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。
  • 通知(advice):就是按照约定的流程下的方法,分为前置通知(beforeadvice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。
  • 目标对象(target):即被代理对象
  • 引入(introduction):是指引入新的类和其方法,增强现有Bean的功能。
  • 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
  • 切面(aspect):是一个可以定义切点、各类通知和引入的内容,Spring AOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。

基于AOP 实现控制层日志打印

/*** controller 层日志打印切面*/@Component@Aspect@Slf4jpublicclassControllerLogHandler {
@ResourceprivateObjectMapperobjectMapper;
/*** 定义切点 该切点就是项目的controller 方法*/@Pointcut("execution(public * com.esaycodin.learn.controller.*.*(..))")
publicvoidlogPointCut() {
    }
/*** 定义具体逻辑* * @param joinPoint 切点对象传入* @return 结果* @throws Throwable 异常*/@Around("logPointCut()")
publicObjectaround(ProceedingJoinPointjoinPoint) throwsThrowable {
//判断是否忽略打印日志MethodSignaturemethodSignature= (MethodSignature) joinPoint.getSignature();
IgnoreSysLogignoreSysLog=methodSignature.getMethod().getAnnotation(IgnoreSysLog.class);
if (ignoreSysLog!=null) {
returnjoinPoint.proceed();
        }
//解析请求内容ServletRequestAttributesrequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequestrequest=requestAttributes.getRequest();
//打印入参log.info("controller:{}-method:{}-param:{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), objectMapper.writeValueAsString(Arrays.toString(joinPoint.getArgs())));
log.info("remoteAddr:{}-request url:{}-method:{}", request.getRemoteAddr(), request.getRequestURL().toString(), request.getMethod());
longstartTime=System.currentTimeMillis();
Objectresult=joinPoint.proceed();
//执行时长log.info("resp value:{}", objectMapper.writeValueAsString(result));
log.info("cost:{}ms", (System.currentTimeMillis() -startTime));
returnresult;
    }
}
/*** 忽略controller层中请求的切面日志打印注解*/@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documentedpublic@interfaceIgnoreSysLog {
}

以上就是我们对学习了AOP 的一个具体功能实现。接下来,我们基于源码探秘一下Spring AOP 是如何实现的吧。


Spring AOP 源码

本次探秘的始发站就是AnnotationAwareAspectJAutoProxyCreator 类。下图为该类的结构关系图:

image.jpeg

我们可以看到其实现了BeanPostProcessor,也就是说在IOC 管理bean 的时候会调用BeanPostProcessor 的接口;具体可以看源码,这里指出几个关键方法。父类AbstractAutoProxyCreator的postProcessAfterInitialization 方法被框架调用,其执行过程中wrapIfNecessary方法判断bean 是否需要被AOP。我们具体关注getAdvicesAndAdvisorsForBean 方法及createProxy 方法。

wrapIfNecessary 源码:

protectedObjectwrapIfNecessary(Objectbean, StringbeanName, ObjectcacheKey) {
// 处理过不需要进行aop 逻辑if (StringUtils.hasLength(beanName) &&this.targetSourcedBeans.contains(beanName)) {
returnbean;
    } elseif (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
returnbean;
// 不是基础类的bean 且没有标识不需要自动代理的bean 也就是说要过自动代理的流程的bean 走以下逻辑    } elseif (!this.isInfrastructureClass(bean.getClass()) &&!this.shouldSkip(bean.getClass(), beanName)) {
// 获取AOP 相关切面几切面beanObject[] specificInterceptors=this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
if (specificInterceptors!=DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建proxyObjectproxy=this.createProxy(bean.getClass(), beanName, specificInterceptors, newSingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
returnproxy;
        } else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
returnbean;
        }
    } else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
returnbean;
    }
}

根据上面的方法,其实可以分析出,this.getAdvicesAndAdvisorsForBean 方法其实是对整个框架进行搜索Advisors 并确定与该bean 有关的advisor。


匹配bean合适的Advices

沿着getAdvicesAndAdvisorsForBean 该方法走我们会来到AnnotationAwareAspectJAutoProxyCreator 的findCandidateAdvisors 方法往下走会看到一个比较关键的方法就是BeanFactoryAspectJAdvisorsBuilder的buildAspectJAdvisors();该方法实现的功能是

  • 获取所有beanName,并将其从beanFacotry 中提取出来
  • 遍历所有的bean,并找出AspectJ 注解类,进行进一步的处理
  • 对标记为AspectJ注解的类进行提取
  • 将提取结果加入缓存(这一步就是再有其它bean 需要绑定时节省了获取的步骤了)

感兴趣的可以对照源码。

advice获取到了那么接下来就需要找到与当前bean匹配的了,具体的方法是AbstractAdvisorAutoProxyCreator类的findAdvisorsThatCanApply ;当我们找到了匹配的则进入到创建代理的流程。


创建代理

对于代理类的创建及处理,Srping 委托给了ProxyFactory来进行,接下来通过DefaultAopProxyFactory 下的createAopProxy 方法来看Spring 是通过什么来选择代理技术的:

publicAopProxycreateAopProxy(AdvisedSupportconfig) throwsAopConfigException {
if (!config.isOptimize() &&!config.isProxyTargetClass() &&!this.hasNoUserSuppliedProxyInterfaces(config)) {
returnnewJdkDynamicAopProxy(config);
    } else {
Class<?>targetClass=config.getTargetClass();
if (targetClass==null) {
thrownewAopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
        } else {
return (AopProxy)(!targetClass.isInterface() &&!Proxy.isProxyClass(targetClass) ?newObjenesisCglibAopProxy(config) : newJdkDynamicAopProxy(config));
        }
    }
}

从上面可以看出:

  • 如果目标对象实现了接口,默认情况下会采用Java的动态代理实现AOP。
  • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
  • 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JavaJava动态代理和CGLIB之间转换。


如何强制使用CGLIB 做AOP 代理?

  • 在SpringBoot 2.x中,如果需要替换使用Java动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改


Java动态代理和CGLIB字节码生成的区别?

  • Java动态代理只能对实现了接口的类生成代理,而不能针对类。
  • CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。


由此结合开始的约定编程的案例,可以了解到Java Proxy 的调用流程,CGlib 则是通过构建一个链,串联的将方法进行增强。篇幅有限,就不过多介绍CGlib 了。


到此对于Spring AOP 的理解就告一段落了,大家加油。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
22天前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
44 1
|
14天前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
5天前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
35 9
|
1月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
38 13
|
27天前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
2月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
29 0
Spring高手之路22——AOP切面类的封装与解析
|
2月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
43 0
|
2月前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
37 0
|
2月前
|
Java Spring 供应链
Spring 框架事件发布与监听机制,如魔法风暴席卷软件世界,开启奇幻编程之旅!
【8月更文挑战第31天】《Spring框架中的事件发布与监听机制》介绍了Spring中如何利用事件发布与监听机制实现组件间的高效协作。这一机制像城市中的广播系统,事件发布者发送消息,监听器接收并响应。通过简单的示例代码,文章详细讲解了如何定义事件类、创建事件发布者与监听器,并确保组件间松散耦合,提升系统的可维护性和扩展性。掌握这一机制,如同拥有一把开启高效软件开发大门的钥匙。
39 0
|
2月前
|
缓存 安全 Java
Spring AOP 中两种代理类型的限制
【8月更文挑战第22天】
18 0