Spring

摘要

AOP是通过对程序结构的另一种思考,补充了OOP。在OOP中,最关键的模块是类,而在AOP中最关键的模块是切面。Spring AOP是基于代理的原理实现的。Spring AOP使用JDK的动态代理或者CGLIB来创建代理实例。

一、什么是AOP

AOP(Aspect-oriented Programming,面向切面编程)是通过对程序结构的另一种思考,补充了OOP(Object-oriented Programing,面向对象编程)。在OOP中,最关键的模块是类,而在AOP中最关键的模块是切面。AOP关注的是横跨不同类的逻辑的封装。

AOP是Spring框架的一个核心组件,Spring AOP的使用有两种风格,分别是:Schema(XML)和@AspectJ

AOP在Spring中的应用有:

  • 提供了声明式的企业级服务(如最重要的声明式事务管理)
  • 让用户自定义切面对OOP设计补充。

AOP的一些概念

  • Aspect(切面):横跨不同类的关注部分的封装抽象
  • Join Point(连接点):程序运行中的一些节点,比如方法的执行、异常。在Spring中,Join Point 通常代表一个方法的执行
  • Advice(增强):在Join Point上的处理,类型可以有"around"、"before"、"after"。Spring将Advice构建成拦截器,围绕着Join Point维护着一条拦截链
  • Pointcut(切点):和Join Point相对应起来,Pointcut是一个匹配规则,匹配上Pointcut的Join Point会在上面执行Advice
  • Introduction(引入):为一个类型添加方法或字段,Spring AOP 允许引入新的接口(和对应实现)到目标对象上
  • Target Object(目标对象,Advised Object):织入Advice后的对象,因为Spring AOP使用动态代理实现后,目标对象通常是一个代理对象
  • Weaving(织入):链接Aspect和其它对象并生成Advice Object的工程。

二、AOP使用

以下是分别使用@AspectJ和Schema风格定义一个AOP的例子,此例子用于在服务执行出错时,重试执行服务。(高并发下的乐观锁异常处理)

要注意的是下面所执行的服务是幂等的才可以重试的。后面会展示如何通过定义一个幂等注解,并对幂等注解进行过滤配置。

1、@AspectJ风格

@AspectJ注解风格是从AspectJ项目中引入的,但是AOP还是只使用Spring AOP,而不是依赖AspectJ的编译器和织入器。

使用Java代码启用Aspect:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

或者使用XML配置启用Aspect:

<!-- 声明自动为spring容器中那些配置@Aspect切面的bean创建代理 -->
<aop:aspectj-autoproxy/>

定义Aspect、Pointcut和Advice:

// 注解Aspect
@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    // 定义Pointcut和Advice
    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

配置Aspect对应的Bean:

<!-- 配置Bean -->
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

定义幂等注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

为前面的Aspect添加幂等注解过滤:

@Around("com.xyz.myapp.SystemArchitecture.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    ...
}

2、Schema(XML)支持

定义Aspect、Pointcut和Advice:

public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    // 环绕增强,用于失败重试
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

配置Aspect、Pointcut和Advice:

<aop:config>
    <!-- 定义切面 -->
    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
        <!-- 定义切入点 -->
        <aop:pointcut id="idempotentOperation"
            expression="execution(* com.xyz.myapp.service.*.*(..))"/>
        <!-- 定义环绕增强 -->
        <aop:around
            pointcut-ref="idempotentOperation"
            method="doConcurrentOperation"/>
    </aop:aspect>
</aop:config>

<!-- 定义Bean -->
<bean id="concurrentOperationExecutor"
    class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>

定义幂等注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

为前面的Aspect添加幂等注解过滤:

<aop:pointcut id="idempotentOperation"
        expression="execution(* com.xyz.myapp.service.*.*(..)) and
        @annotation(com.xyz.myapp.service.Idempotent)"/>

3、两种AOP声明风格的对比

Aspect同样是一个面向切面的框架,Spring通过Aspect无缝地将AOP和IoC整合在一起。

使用Spring AOP还是AspectJ

Spring AOP 在开发中不需要引入AspectJ的编译器和织入器,如果想在Spring的Bean引入增强。如果不是管理Spring容器,则需要AspectJ了。

在Spring AOP中使用@AspectJ还是XML

  • 所有切面、切点、增强都写在一个或几个配置文件里便于维护,且不需要修改Java代码;
  • XML风格AOP仅支持"singleton"切面实例模型,而采用AspectJ风格的AOP则没有这个限制;
  • XML风格的AOP不支持命名切入点的的声明,而AspectJ没有这个限制(@Pointcut);

参考:简单说说SpringAOP与Aspectj的不同,以及使用SpringAOP所需的最小jar依赖

三、AOP的代理机制

Spring AOP是基于代理的原理实现的。Spring AOP使用JDK的动态代理或者CGLIB来创建代理实例。JDK动态代理是JDK的内置代理,而CGLIB则是一个开源的类定义库(重新打包到spring-core中)。

关于代理的概念,参考:27--静态代理模式和JDK、CGLIB动态代理

Spring AOP使用ProxyFactory来创建代理,类似代码如下:

public class Main {
    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.adddInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy(); // 返回jdk或者cglib生成的代理对象
        // this is a method call on the proxy!
        pojo.foo();
    }
}

如果代理对象实现了至少一个接口,则会使用JDK动态代理。如果代理对象没有实现任何接口,则会使用CGLIB进行代理。

使用CGLIB的一些限制:

  • final类和方法不能使用增强(子类不能不能重写final方法)
  • 从Spring 4.0开始,代理对象的构造函数不再被调用两次,因为CGLIB代理实例通过Objenesis创建。只有JVM不允许构造函数绕过时,才可能看到来自Spring AOP支持的双重调用和相应的调用日志。

参考:Objenesis,另一种实例化对象的方式

强制使用CGLIB:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

<!-- 或者 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

参考资料

作者:Emile

个人主页:http://www.guanjianzhuo.com/

欢迎访问、评论、留言!

原文地址:https://www.cnblogs.com/guanjianzhuo/p/10949474.html