AOP

1 背景

AOP(面向切面编程)与Spring的紧密结合助推了Spring取代EJB的进程。基于Spring AOP实现的事务、缓存、日志等组件成为了Spring的重要组成部分。 AOP能让代码更加模块化、功能更内敛,也能让编写的代码更符合OOP风格。关于这段历史,可以看下https://book.douban.com/subject/1436131/。

AOP通过Proxy的方式实现,而代理分为「静态代理」和「动态代理」两种。 通常来讲,一般都认为AspectJ为「静态代理」的实现,「动态代理」的实现为JDK动态代理和CGLib动态代理,一般动态代理的方式在Spring框架中也叫做 Spring AOP。

一些概念:

Aspect: A modularization of a concern that cuts across multiple objects. Transaction management is a good example of a crosscutting concern in J2EE applications. In Spring AOP, aspects are implemented using regular classes (the schema-based approach) or regular classes annotated with the @Aspect annotation (@AspectJ style).

Join point: A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. Join point information is available in advice bodies by declaring a parameter of type org.aspectj.lang.JoinPoint.

Advice: Action taken by an aspect at a particular join point. Different types of advice include “around,” “before” and “after” advice. Advice types are discussed below. Many AOP frameworks, including Spring, model an advice as an interceptor, maintaining a chain of interceptors “around” the join point.

Pointcut: A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP: Spring uses the AspectJ pointcut language by default.

Introduction: (Also known as an inter-type declaration). Declaring additional methods or fields on behalf of a type. Spring AOP allows you to introduce new interfaces (and a corresponding implementation) to any proxied object. For example, you could use an introduction to make a bean implement an IsModified interface, to simplify caching.

Target object: Object being advised by one or more aspects. Also referred to as the advised object. Since Spring AOP is implemented using runtime proxies, this object will always be a proxied object.

AOP proxy: An object created by the AOP framework in order to implement the aspect contracts (advise method executions and so on). In the Spring Framework, an AOP proxy will be a JDK dynamic proxy or a CGLIB proxy. Proxy creation is transparent to users of the schema-based and @AspectJ styles of aspect declaration introduced in Spring 2.0.

Weaving: Linking aspects with other application types or objects to create an advised object. This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime. Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.

Types of advice:

Before advice: Advice that executes before a join point, but which does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).

After returning advice: Advice to be executed after a join point completes normally: for example, if a method returns without throwing an exception.

After throwing advice: Advice to be executed if a method exits by throwing an exception.

After (finally) advice: Advice to be executed regardless of the means by which a join point exits (normal or exceptional return).

Around advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.

1.1 AspectJ vs Spring-AOP

在stackoverflow上有二者的对比(https://stackoverflow.com/questions/1606559/spring-aop-vs-aspectj):

Spring AOP缺点:

Spring AOP优点:

AspectJ 优点:

AspectJ 缺点:

1.2 代理机制:JDK vs CGLib

Spring 会默认选择前者,JDK代理通过反射API实现目标类接口方式实现,因此只能代理存在接口的类。CGLib使用继承的方式,所以对接口无要求,但是 如果类无法被继承,如final修饰,那么也无法代理。在性能方面,有测试数据说明CGLib在代理创建和代理执行过程中性能更好。

https://stackoverflow.com/questions/10664182/what-is-the-difference-between-jdk-dynamic-proxy-and-cglib

关于JDK Proxy和CGLib的使用可见: https://gist.github.com/premraj10/3a3eac42a72c32de3a41ec13ef3d56ad

https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html

2 Spring AOP编程

从Spring 2.0开始支持 @AspectJ注解风格schema-based定义形式的aop实现方式(二者的使用对比见https://docs.spring.io/spring/docs/2.0.x/reference/aop.html的 6.4.2节,Spring团队更推荐前者)。 也兼容 Spring1.2开始存在的Spring AOP APIs,Spring-Cache和大部分Spring内部基于AOP实现的组件基本使用AOP API的方式。

2.0 ProxyFactoryBean

https://docs.spring.io/spring/docs/3.0.0.M4/reference/html/apbs05.html

2.1 @AspectJ or schema-based

前者通过来启用,后者通过来配置。

关于 @AspectJ风格和Schema-based实行的AOP实现,https://docs.spring.io/spring/docs/2.0.x/reference/aop.html 的6.2和6.3写的很详细。

2.2 Spring AOP APIs

通过AutoProxyCreator来配置。

细节可参考:https://docs.spring.io/spring/docs/2.0.x/reference/aop-api.html。

这里举一个实际的例子。

2.2.1 PointCut

/**
* 定义Pointcut的拦截注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SAAnnotation {
}

/**
* Pointcut
*/
public class SAPointcut extends StaticMethodMatcherPointcut {

    /**
     * 这里会在初始化过程中调用。
     * 单是也发现在首次调用时也会有一次判断
     * @param method
     * @param targetClass
     * @return
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {

        Annotation annotation = AnnotationUtils.getAnnotation(method, SAAnnotation.class);

        // 这里可以加上缓存,多次调用到这里没必要重复判断
        if (null != annotation) {
            return true;
        }

        return false;
    }
}

2.2.2 Interceptor(这是aop联盟的接口,也可以使用Spring的注入MethodBeforeAdvice Advice接口)

public class SAInterceptor implements MethodInterceptor {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 定制方法执行的前后逻辑
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {

        doBefore();

        Object ret;
        try {
            logger.info(LogConstant.LINE);
            logger.info("target invoke...");
            logger.info(LogConstant.LINE);
            ret = invocation.proceed();
        } catch (Throwable ex) {
            throw ex;
        }

        doAfter();

        return ret;
    }

    private void doBefore() {

        logger.info(LogConstant.LINE);
        logger.info(getClass().getSimpleName() + " doBefore");
        logger.info(LogConstant.LINE);
    }

    private void doAfter() {

        logger.info(LogConstant.LINE);
        logger.info(getClass().getSimpleName() + " doAfter");
        logger.info(LogConstant.LINE);
    }
}

2.2.3 Advisor

public class SAAdvisor extends AbstractBeanFactoryPointcutAdvisor{

    private Pointcut pointcut;

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    public void setPointcut(Pointcut pointcut) {
        this.pointcut = pointcut;
    }
}
以上基本就实现了基础设施,下面将这些组装为bean,即可使用

2.2.4 Bean组装

@Configuration
public class SAConfiguration {

    @Bean
    public SAAdvisor saAdvisor() {

        SAAdvisor advisor = new SAAdvisor();
        advisor.setAdvice(saInterceptor());
        advisor.setOrder(Ordered.HIGHEST_PRECEDENCE);
        advisor.setPointcut(saPointcut());

        return advisor;
    }


    @Bean
    public SAInterceptor saInterceptor() {

        SAInterceptor interceptor = new SAInterceptor();

        return interceptor;
    }

    @Bean
    public SAPointcut saPointcut() {

        SAPointcut pointcut = new SAPointcut();

        return pointcut;
    }

}

2.2.5 Selector + Enable注解

public class SASelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        List<String> ret = Arrays.asList(AutoProxyRegistrar.class.getName(),
                SAConfiguration.class.getName());

        return (String[]) ret.toArray();

    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Import(SAConfiguration.class)
public @interface EnableSA {
}

2.3 ProxyFactory

针对明确的Pointcut,譬如单个类或者接口,可以使用 ProxyFactory,下面举个例子:

public class SAProxy implements InitializingBean, FactoryBean{

    /**
     * 这是一个例子, 这里可以是bean 也可以是 interface,取决于SAProxy的实例方式和 MethodInterceptor 的实现方式
     * 如果是interface,那么在MethodInterceptor
     */
    private Object target;
    private MethodInterceptor interceptor;

    public SAProxy(Object target, MethodInterceptor interceptor) {
        this.target = target;
        this.interceptor = interceptor;
    }

    private Object proxy;

    public Object getProxy() {
        return proxy;
    }

    @Override
    public void afterPropertiesSet() throws Exception {

        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(interceptor);

        proxy = proxyFactory.getProxy();
    }


    @Override
    public Object getObject() throws Exception {
        return proxy;
    }

    @Override
    public Class<?> getObjectType() {
        return target.getClass();
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    /**
     * 这是一个例子
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {

        MethodInterceptor interceptor = new SAInterceptor();
        IToBeCached cached = new ToBeCachedClass();


        SAProxy saProxy = new SAProxy(cached, interceptor);

        saProxy.afterPropertiesSet();

        IToBeCached toBeCached = (IToBeCached) saProxy.getProxy();
        System.out.println(toBeCached.toBeCachedOperation("hello"));
    }
}

在Spring中,可以这样组装

@Component
public class SAProxyConfig {

    /**
     * 这是另外一只直接注入bean的方式
     * @param toBeCachedBean
     * @param saInterceptor
     * @return
     */
    @Bean
    public SAProxy proxyBean1(IToBeCached toBeCachedBean, SAInterceptor saInterceptor) {
        SAProxy proxy = new SAProxy(toBeCachedBean, saInterceptor);
        return  proxy;
    }


}

使用方可以直接使用,因为SAProxy是一个FactoryBean,不必担心类型转换问题。

@javax.annotation.Resource(name="proxyBean1")
private IToBeCached bean;

参考文档:

这篇写的也不错:http://tech.lede.com/2017/08/15/rd/server/SpringSpringMVCContainerAndAOPCommonMistakes/