AOP 你看这一篇就够了

网上不少人在介绍AOP时都这样说:面向切面编程,经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。我的认为这句话是错误。AOP和OOP同样,是一种程序设计思想,而非技术手段。spring

程序设计有六大原则,其中第一原则就是 单一职责原则 。意思就是一个类只负责一件事情。这与OOP的封装特性相得益彰。在这个条件下,咱们的程序会被分散到不一样的类、不一样的方法中去。这样作的好处是下降了类的复杂性,提升了程序的可维护性。可是同时,它也使代码变得啰嗦了。例如,咱们要为方法添加调用日志,那就必须为全部类的全部方法添加日志调用,尽管它们都是相同的。为了解决上述问题,AOP应运而生了。express

AOP旨在将 横切关注点 与业务主体进行分类,从而提升程序代码的模块化程度。横切关注点是一个抽象的概念,它是指那些在项目中贯穿多个模块的业务。上个例子中日志功能就是一个典型的横切关注点。编程

AOP的几种实现方式设计模式

动态代理安全

动态代理是一种设计模式。它有如下特征:性能优化

  • 咱们不须要本身写代理类。
  • 运行期经过接口直接生成代理对象。
  • 运行期间才肯定代理哪一个对象。

如下面这个例子为例,咱们看一下动态代理的类图结构。bash

一般咱们的APP都有一部分功能要求用户登陆以后才能访问。如修改密码、修改用户名等功能。当用户打算使用这些功能时,咱们通常要对用户的登陆状态进行判断,只有用户登陆了,才能正常使用这些功能。而若是用户未登陆,咱们的APP要跳转到登陆页。就以修改密码为例咱们看一下动态代理的类图。架构

AOP 你看这一篇就够了

InvocationHandler是Java JDK提供的动态代理的入口,用来对被代理对象的方法作处理。并发

代码以下:app

public static class LoginCheckHandler implements InvocationHandler { private static <S, T extends S> T proxy(S source, Class<T> tClass) { return (T) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{tClass}, new LoginCheckHandler(source)); }  private Object mSource; LoginCheckHandler(Object source) { this.mSource = source; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(!checkLogin()){ jumpToLoginActivity(); return null; } return method.invoke(mSource, args); } private boolean checkLogin(){ System.out.println("用户未登陆"); return false; } private void jumpToLoginActivity(){ System.out.println("跳转到登陆页"); } } public class Client {  public static void main(String[] args) { IUserSetting source = new UserSetting(); IUserSetting iUserSetting = LoginCheckHandler.proxy(source,IUserSetting.class); iUserSetting.changePwd("new Password"); } }复制代码复制代码

通过这样封装以后,检查登陆跳转登陆页的逻辑做为 横切关注点 就和业务主体进行了分离。当有新的需求须要登陆检查时,咱们只须要经过LoginCheckHandler生成新的代理对象便可。

APT

APT(Annotation Processing Tool)是一种编译期注解处理技术。它经过定义注解和处理器来实现编译期生成代码的功能,而且将生成的代码和源代码一块儿编译成.class文件。经过APT技术,咱们将 横切关注点 封装到注解处理器中,从而实现 横切关注点 与业务主体的分离。更详细的介绍请移步

Android编译期插桩,让程序本身写代码(一)

AspectJ

AspectJ就是一种编译器,它在Java编译器的基础上增长了关键字识别和编译方法。所以,AspectJ能够编译Java代码。它还提供了Aspect程序。在编译期间,将开发者编写的Aspect程序织入到目标程序中,扩展目标程序的功能。开发者经过编写AspectJ程序实现AOP功能。更详细的介绍请移步

Android编译期插桩,让程序本身写代码(二)

Transform + Javassist/ASM

Transform是Android Gradle提供的,能够操做字节码的一种方式。App编译时,源代码首先会被编译成class,而后再被编译成dex。在class编译成dex的过程当中,会通过一系列 Transform 处理。 Javassist/ASM 是一个可以很是方便操做字节码的库。咱们经过它们能够修改编译的.class文件。

横切关注点

影响应用多处的功能(日志、事务、安全)

加强(Advice)

加强定义了切面要完成的功能以及何时执行这个功能。

Spring 切面能够应用 5 种类型的加强:

  • 前置加强(Before) 在目标方法被调用前调用加强功能
  • 后置加强(After) 在目标方法完成以后调用加强, 不关注方法输出是什么
  • 返回加强(After-returning) 在目标方法成功执行以后调用加强
  • 异常加强(After-throwing) 在目标方法抛出异常后调用加强
  • 环绕加强(Around) 在被加强的方法调用以前和调用以后执行自定义行为,即包括前置加强和后置加强。

链接点(Join Point)

应用中每个有可能会被加强的点被称为链接点。

切点(Pointcut)

切点是规则匹配出来的链接点。

切面(Aspect)

切面是加强和切点的结合,定义了在什么时候和何处完成其功能。

引入(Introduction)

引入容许咱们向现有的类中添加新方法和属性。能够在不修改现有的类的状况下,让类具备新的行为和状态。

织入(Weaving)

织入是把切面应用到目标对象中并建立新的代理对象的过程。在目标对象的生命周期里有多个点能够进行织入:

  • 编译器:切面在目标类编译时织入。这种方式须要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载器:切面在目标类加载到 JVM 时被织入。这种方式须要特殊的类加载器(ClassLoader),它能够在目标类被引入应用以前加强该目标类的字节码。AspectJ5 的加载时织入(LTW)支持以这种方式织入。
  • 运行期:切面在应用运行时的某个时刻被织入。通常状况下,在织入切面时,AOP 容器会为目标对象动态地建立一个代理对象。Spring AOP 就是以这种方式织入切面的。

Spring 对 AOP 的支持

Spring 对 AOP 的支持在不少方面借鉴了 AspectJ 项目。目前 Spring 提供了 4 种类型的 AOP 支持:

  • 基于代理的经典 AOP
  • 纯 POJO 切面
  • @AspectJ 注解驱动的切面
  • 注入式 AspectJ 切面

Spring AOP 构建在动态代理基础之上,所以 Spring 对 AOP 的支持局限于方法拦截。

运行时加强

经过在代理中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截被加强方法的调用,再把调用转发给真正的目标 bean。在代理拦截到方法调用时,在调用目标 bean 方法以前,会执行切面逻辑。

直到应用须要代理的 bean 时,Spring 才建立代理对象。若是使用 ApplicationContext 的话,在 ApplicationContext 从 BeanFactory 中加载全部 bean 的时候,Spring 才会建立被代理的对象。

方法级别的链接点

Spring 基于动态代理实现 AOP,因此 Spring 只支持方法链接点。其余的 AOP 框架好比 AspectJ 与 JBoss,都提供了字段和构造器接入点,容许建立细粒度的加强。

切点表达式

Spring AOP 中,使用 AspectJ 的切点表达式来定义切点。Spring 只支持 AspectJ 切点指示器(pointcut designator)的一个子集。

指示器

AspectJ 指示器描述arg( )限制链接点匹配参数为指定类型的执行方法execution( )用于匹配链接点this指定匹配 AOP 代理的 bean 引用的类型target指定匹配对象为特定的类within( )指定链接点匹配的类型@annotation匹配带有指定注解的链接点

编写切点

package concert;public interface Performance { public void perform();}复制代码复制代码

Performance 类能够表明任何类型的现场表演,好比电影、舞台剧等。如今编写一个切点表达式来限定 perform() 方法执行时触发的加强。

execution(* concert.Performance.perform(..))复制代码复制代码

每一个部分的意义以下图所示:

AOP 你看这一篇就够了

也能够引入其余注解对匹配规则作进一步限制。好比

execution(* concert.Performance.perform(..)) && within(concert.*)复制代码复制代码

within() 指示器限制了切点仅匹配 concert 包。

Spring 还有一个 bean() 指示器,容许咱们在切点表达式中使用 bean 的 ID 表示 bean。

execution(* concert.Performance.perform(..)) && bean('woodstock')复制代码复制代码

以上的切点就表示限定切点的 bean 的 ID 为 woodstock 。

给本身的Java技术交流群打波广告吧,想要学习Java架构技术的朋友能够加个人群: 710373545,群内每晚都会有阿里技术大牛讲解的最新Java架构技术。并会录制录播视频分享在群公告中,做为给广大朋友的加群的福利——分布式(Dubbo、Redis、RabbitMQ、Netty、RPC、Zookeeper、高并发、高可用架构)/微服务(Spring Boot、Spring Cloud)/源码(Spring、Mybatis)/性能优化(JVM、TomCat、MySQL)

使用注解建立切面

定义切面

在一场演出以前,咱们须要让观众将手机静音且就座,观众在表演以后鼓掌,在表演失败以后能够退票。在观众类中定义这些功能。

@Aspectpublic class Audience {  @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Before("performance()") public void silenceCellPhones() { System.out.println("Silencing cell phones"); } @Before("performance()") public void takeSeats() { System.out.println("Taking seats"); } @AfterReturning("performance()") public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } @AfterThrowing("performance()") public void demandRefund() { System.out.println("Demanding a refund"); }}复制代码复制代码

@AspectJ 注解表名了该类是一个切面。 @Pointcut 定义了一个类中可重用的切点,写切点表达式时,若是切点相同,能够重用该切点。 其他方法上的注解定义了加强被调用的时间,根据注解名能够知道具体调用时间。

到目前为止, Audience 仍然只是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,可是这些注解仍然不会解析,由于目前还缺少代理的相关配置。

若是使用 JavaConfig,在配置类的类级别上使用 @EnableAspectJAutoProxy 注解启用自动代理功能。

@Configuration@EnableAspectJAutoProxy@ComponentScanpublic class ConcertConfig { @Bean public Audience audience() { return new Audience(); } }复制代码复制代码

若是使用 xml ,那么须要引入 <aop:aspectj-autoproxy> 元素。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="concert"/> <aop:aspectj-autoproxy/> <bean class="concert.Audience"/></beans>复制代码复制代码

环绕加强

环绕加强就像在一个加强方法中同时编写了前置加强和后置加强。

@Aspectpublic class Audience { @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}复制代码复制代码

能够看到,这个加强达到的效果与分开写前置加强与后置加强是同样的,可是如今全部的功能都位于同一个方法内。 注意该方法接收 ProceedingJoinPoint 做为参数,这个对象必需要有,由于须要经过它来调用被加强的方法。 注意,在这个方法中,咱们能够控制不调用 proceed() 方法,从而阻塞对加强方法的访问。一样,咱们也能够在加强方法失败后,屡次调用 proceed() 进行重试。

加强方法参数

修改 Perform#perform() 方法,添加参数

package concert;public interface Performance { public void perform(int audienceNumbers);}复制代码复制代码

咱们能够经过切点表达式来获取被加强方法中的参数。

@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))") public void performance(int audienceNumbers){}复制代码复制代码

注意,此时方法接收的参数为 int 型, args(audienceNumbers) 指定参数名为 audienceNumbers ,与切点方法签名中的参数匹配,该参数不必定与加强方法的参数名一致。

引入加强

切面不只仅可以加强现有方法,也能为对象新增新的方法。 咱们能够在代理中暴露新的接口,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其余对象。实际上,就是一个 bean 的实现被拆分到多个类中了。 定义 Encoreable 接口,将其引入到 Performance 的实现类中。

public interface Encoreable { void performEncore();}复制代码复制代码

建立一个新的切面

@Aspectpublic class EncoreableIntroducer { @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable;}复制代码复制代码

咱们使用了 @Aspect 将 EncoreableIntroducer 标记为一个切面,可是它没有提供前置、后置或环绕加强。经过 @DeclareParents 注解将 Encoreable 接口引入到了 Performance bean 中。

@DeclareParents 注解由三部分组成:

  • value 属性指定了哪一种类型的 bean 要引入该接口。在上述代码中,类名后面的 + 号表示是 Performance 的全部子类型,而不是它自己。
  • defaultImpl 属性指定了为引入功能提供实现的类。
  • @DeclareParents 注解所标注的静态属性指明了要引入的接口。

一样地,咱们在 Spring 应用中将该类声明为一个 bean:

<bean class="concert.EncoreableIntroducer" />复制代码复制代码

Spring 的自动代理机制将会获取到它的声明,并建立相应的代理。而后将调用委托给被代理的 bean 或者被引入的实现,具体取决于调用的方法属于被代理的 bean 仍是属于被引入的接口。

在 XML 中声明切面

更新一下 Audience 类,将它的 AspectJ 注解所有移除。

public class Audience {   public void silenceCellPhones() { System.out.println("Silencing cell phones"); } public void takeSeats() { System.out.println("Taking seats"); } public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } public void demandRefund() { System.out.println("Demanding a refund"); }}复制代码复制代码

声明前置与后置加强

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="takeSeats"/> <aop:after-returning pointcut="execution(* concert.Performance.perform(..))" method="applause"/> <aop:after-throwing pointcut="execution(* concert.Performance.perform(..))" method="demandRefund"/> </aop:aspect> </aop:config></beans>复制代码复制代码

如上所示,就将一个普通方法变为了加强。 大多数的 AOP 配置元素都必须在 <aop:config>元素的上下文内使用。元素名基本上都与注解名相对应。 这里,咱们一样将同一个切点表达式写了四遍,将它提取出来。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:before pointcut-ref="performance" method="silenceCellPhone"/> <aop:before pointcut-ref="performance" method="takeSeats"/> <aop:after-returning pointcut-ref="performance" method="applause"/> <aop:after-throwing pointcut-ref="performance" method="demandRefund"/> </aop:aspect> </aop:config></beans>复制代码复制代码

注意,此时 <aop:pointcut> 标签位于 <aop:aspect> 下层,故只能在该切面中引用。若是想要一个切点可以被多个切面引用,能够将 <aop:aspect> 元素放在 <aop:config> 下第一层。

环绕加强

定义环绕加强方法

public class Audience { public void performance(int audienceNumbers){} public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } }}复制代码复制代码

在 xml 中使用 <aop:around> 指定方法名与切点便可。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:around pointcut-ref="performance" method="watchPerformance"/> </aop:aspect> </aop:config></beans>复制代码复制代码

为加强传递参数

获取参数主要就在于切点表达式。

<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/>复制代码复制代码

这样能在 xml 中定位到一个参数类型为 int ,参数名为 audienceNumbers 的切点。 注意在 xml 中使用了 and 代替 && (在 XML 中, & 符号会被解析为实体的开始)。

引入加强

<aop:declare-parents types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/>复制代码复制代码

types-matching 指定了要匹配的类型,与注解中的 value 值功能相同。

注入 AspectJ 切面

AspectJ 切面提供了 Spring AOP 所不能支持的许多类型的切点。 切面颇有可能依赖其余类来完成它们的工做。咱们能够借助 Spring 的依赖注入把 bean 装配进 AspectJ 切面中。

建立一个新切面。

public aspect CriticAspect { private CriticismEngine criticismEngine; public CriticAspect() { } pointcut performance():execution(* perform(..)); afterReturning() : performance() { System.out.println(criticismEngine.getCriticism()); } public void setCriticismEngine(CriticismEngine criticismEngine) { this.criticismEngine = criticismEngine; }}复制代码复制代码

注入的 CritismEngine 的实现类

public class CriticismEngineImple implements CriticismEngine { public CriticismEngineImple() { } public String getCriticism() { int i = (int) (Math.random() * criticismPool.length); return criticismPool[i]; }  private String[] criticismPool; public void setCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; }}复制代码复制代码

CriticAspect 主要做用是在表演结束后为表演发表评论。 实际上, CriticAspect 是调用了 CriticismEngine 的方法来发表评论。经过 setter 依赖注入为 CriticAspect 设置 CriticismEngine 。

AOP 你看这一篇就够了

在配置文件中将 CriticismEngine bean 注入到 CriticAspect 中。

<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf"> <property name="criticismEngine" ref="criticismEngine"/> </bean>复制代码复制代码

通常状况下,Spring bean 由 Spring 容器初始化,可是 AspectJ 切面是由 AspectJ 在运行期建立的。因此在运行期间,AspectJ 建立好了 CriticAspect 实例,每一个 AspectJ 都会提供一个静态的 aspectOf() 方法,返回切面的的单例。 使用 factory-method 调用 aspectOf() 方法向 CriticAspect 中注入 CriticismEngine 。

相关文章
相关标签/搜索