AOP

啥是AOP?为啥要有AOP呢?OOP用着不香吗?

OOP是面向对象编程,AOP是面向切面编程。OOP全称是Object Oriented Programming,AOP全称是Aspect Oriented Programming,aspect就是面/方面/切面的意思。

假如要在一些核心业务模块添加一些外围业务的操作,如记录日志、权限判断等,这种情况下用AOP最合适了。第一,不会污染核心业务代码,这些代码没有和核心业务代码放在一起,改动时不用改动核心业务模块。第二,这些代码是可插拔的,哪些核心业务模块想用或者不想用,就直接配置或者取消配置即可。AOP的目的是解决外围业务代码与核心业务代码分离的问题,它不会替代OOP。OOP是把编码问题模块化,而AOP是把涉及多个模块的某一类问题统一管理。

AOP的实现技术有多种,其中与java无缝对接的是AspectJ。J就是java的意思。

AspectJ是Eclipse基金会的一个项目。官网地址是https://www.eclipse.org/aspectj/

github地址是https://github.com/eclipse/org.aspectj

AspectJ AOP

要编写AspecJ文件,关键字是aspect,文件后缀是.aj。开发AspectJ项目,IDE需要安装AspectJ Support插件,代码需要引入aspectj相关jar,从maven仓库直接搜即可,且编译器要用特殊的编译器,不能用javac了,javac不认识aj文件,要用ajc(是aspectj compiler的简称)。ajc编译器用于编译.aj文件,它也可以编译java文件。

新建时,选择Aspect,而不是Java Class。

下面以在某个方法执行完毕时打印日志的这个需求为例:

public class HelloWorld {

    public void sayHello() {
        System.out.println("hello world !");
    }

    public static void main(String args[]) {
        HelloWorld helloWord = new HelloWorld();
        helloWord.sayHello();
    }
}

有一个HelloWorld类,想在sayHello()方法执行完毕后打印一条日志。

则我们可以新建切面类,在其中用pointcut关键字定义切点,用after、before等关键字定义后置通知、前置通知。如下:

public aspect MyAspectJDemo {

    /*
      切点
     */
    pointcut recordLog():execution(* HelloWorld.sayHello(..));

    /*
      前置通知
    */
    before():recordLog(){
        System.out.println("sayHello方法执行前记录日志");
    }

    /*
      后置通知
     */
    after():recordLog(){
        System.out.println("sayHello方法执行后记录日志");
    }

}

recordLog是切点名,用pointcut关键字修饰。execution是固定的,括号里面是采用这个关注点的方法,*表示任意返回类型,HelloWorld.sayHello表示Helloworld类的sayHello方法,sayHello后面括号两个点表示任意入参。

除了before代表的前置通知、after代表的后置通知外,还有环绕通知,用around关键字。环绕通知比较特殊,一定要有返回值,否则在编译时会报语法错误,insert "return type" to complete around advice declaration。并且必须调用proceed()方法,否则不会到核心模块的代码中去,这里就好像filter的doFilter操作,不过proceed()是有返回值的,返回值就是包裹的方法的返回值,如果包裹的方法返回值是void,则proceed()方法返回null。

public aspect MyAspectJDemo {

    pointcut recordTimeCost():call(* HelloWorld.sayHello(..));

    Object around():recordTimeCost(){
        System.out.println("计时开始");
        long begin = System.currentTimeMillis();
        Object o = proceed();
        System.out.println("cost " + (System.currentTimeMillis() - begin) + "ms");
        return o;
    }

}

用aspectj关键字定义的类叫做切面,如上面的MyAspectJDemo,在切面中使用pointcut关键字定义切点,使用before、after、around关键字定义前置通知、后置通知、环绕通知。所谓的切点就是那些需要应用切面的方法,也称为目标方法,如上面的sayHello方法。通知,英文是advice,就是那些需要在目标方法或前或后或前后执行的方法,分为前置通知,在目标方法执行前执行,后置通知,在目标方法执行后执行,环绕通知,在目标方法执行前和执行后执行。

实际上,AspectJ从1.5开始(06年左右),就引入了@Aspect、@Pointcut、@Before、@After、@Around等注解,使我们可以用注解开发,大大提高了开发效率。这些注解都在aspectjweaver.jar中。

上例如果用注解风格开发,则可以改造成:

@Aspect
public class MyAspectJDemo {

    @Pointcut(value = "execution(* HelloWorld.sayHello(..))")
    public void xx() {
    }

    @After(value = "xx()")
    public void recordLog() {
        System.out.println("sayHello方法执行后记录日志");
    }

}

把aj文件改成java文件(源文件后缀名可以不改,但是要用class关键字替代aspect关键字),用@Aspect标注切面类,用@Pointcut注解替代pointcut关键字,@Pointcut的value属性值中不能再用call引导目标方法了,必须用execution。用@After注解替代after关键字。注意,此时还是必须用ajc编译器,用javac编译器不行。

这里额外指出一下,使用@Around注解时,标注的方法的参数必须是ProceedingJoinPoint类型。通过这个ProceedingJoinPoint实例的getTarget(),我们可得到当前目标方法所在类的class实例,进而得到类名。通过这个ProceedingJoinPoint实例的getSignature()方法得到Signature实例,调用其getName()方法可以得到方法名。这样就解决了上面的第1个疑问。再多说一点,在其他通知注解,如@Before、@AfterThrowing等标注的方法中,我们可以给方法一个JoinPoint类型的入参,在方法内部就可以调用这个JoinPoint实例获取当前目标方法的类名、方法名了。注意是JoinPoint类型,不是ProceedingJoinPoint类型,ProceedingJoinPoint是JoinPoint的子接口。

到现在还有一个概念没有讲,那就是织入。所谓织入,就是指把切面应用到目标方法的过程,英文是weaving。

织入可以分为静态织入和动态织入。

AspecJ采用的是编译期织入,用acj编译器把切面编译成class字节码后,在目标类编译时织入,是静态织入的一种。

动态织入指的是在运行时动态地将增强的代码织入到目标方法中,是通过动态代理实现的。Spring AOP采用的就是动态织入。

Spring AOP

考虑到AspectJ对ajc编译器的强依赖性,在spring项目中最好使用Spring AOP,不仅解除了对ajc编译器的依赖,还能够与Spring IOC很好的结合。

从Spring 2.0开始,Spring就支持AspectJ的注解。注意,用Spring AOP时,还必须引入aspectjweaver.jar,用这个jar包中的注解类呢。

Spring AOP的代码在AspectJ AOP注解风格写法的基础上稍微改下就可以了。

1、把切面类由aspect MyAspectJDemo改成class MyAspectJDemo,即不再创建aspect文件,而是创建java文件。同样用@Aspect注解标注。

2、用@EnableAspectJAutoProxy标注上面切面类,表示启用AspectJ的各种注解,@EnableAspectJAutoProxy注解是Spring提供的。

3、用@Component标注切面类。

就这样,切面类内部的切点和通知不用做任何改动。

示例:

@Aspect
@EnableAspectJAutoProxy
@Component
public class MySpringAopAspect {

    @Pointcut("execution(* HelloWorld.sayHello(..))")
    public void recordLog() {
    }

    @Before(value = "recordLog()")
    public void xxx() {
        System.out.println("前置通知....");
    }

}

切面:用@Aspect注解标注的一个类,在此类中定义切点和通知。

切点:定义在什么地方切入。用@Pointcut注解标注一个方法,方法名随意,方法体为空。注解的value属性值是切点表达式,用于指定哪些包的哪些类的哪些方法要被切入。

通知:定义"什么时候"和"做什么"。有前置通知、后置通知、环绕通知等。如前置通知,是用@Before注解标注一个方法,注解的value属性值是@Pointcut注解标注的方法名,此前置方法会在目标方法执行前执行。

其实切点表达式不止有execution关键字,一共有9种切点表达式,见https://blog.51cto.com/u_5914679/2092253

 

Spring AOP是如何用动态代理的呢?

动态代理类型

posted on 2017-04-17 23:38  koushr  阅读(553)  评论(0编辑  收藏  举报

导航