Java AOP & Spring AOP 原理和实现

转载:http://www.cnblogs.com/hongwz/p/5764917.html

1. AOP

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用”横切”技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

2. AOP的核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象

3、连接点(joinpoint)

被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

4、切入点(pointcut)

对连接点进行拦截的定义

5、通知(advice)

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

《Java AOP & Spring AOP 原理和实现》

织入器通过在切面中定义pointcout来搜索目标(被代理类)的JoinPoint(切入点),然后把要切入的逻辑(Advice)织入到目标对象里,生成代理类

3. AOP的实现原理

我们可以从几个层面来实现AOP。

《Java AOP & Spring AOP 原理和实现》

在编译器修改源代码,在运行期字节码加载前修改字节码或字节码加载后动态创建代理类的字节码,以下是各种实现机制的比较。 

类别 机制 原理 优点 缺点
静态AOP 静态织入 在编译期,切面直接以字节码的形式编译到目标字节码文件中 对系统无性能影响 灵活性不够
动态AOP 动态代理 在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中 相对于静态AOP更加灵活

切入的关注点需要实现接口

对系统有一点性能影响

动态字节码生成 CGLIB 在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中 没有接口也可以织入 扩展类的实例方法为final时,则无法进行织入
自定义类加载器   在运行期,目标加载前,将切面逻辑加到目标字节码里 可以对绝大部分类进行织入 代码中如果使用了其他类加载器,则这些类将不会被织入
字节码转换   在运行期,所有类加载器加载字节码前进行拦截 可以对所有类进行织入  

3.

3.1 动态代理

静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了

动态代理:即在运行期动态创建代理类,使用动态代理实现AOP需要4个角色:

  • 被代理的类:即AOP里所说的目标对象
  • 被代理类的接口
  • 织入器:使用接口反射机制生成一个代理类,在这个代理类中织入代码
  • InvocationHandler切面:切面,包含了Advice和Pointcut

3.1.1 动态代理的演示

例子演示的是在方法执行前织入一段记录日志的代码,其中

  • Business是代理类
  • LogInvocationHandler是记录日志的切面
  • IBusiness、IBusiness2是代理类的接口
  • Proxy.newProxyInstance是织入器
public interface IBusiness {
    void doSomeThing();
}

public interface IBusiness2 {
    void doSomeThing2();
}

public class Business implements IBusiness, IBusiness2 {
    @Override
    public void doSomeThing() {
        System.out.println("执行业务逻辑");
    }

    @Override
    public void doSomeThing2() {
        System.out.println("执行业务逻辑2");
    }
}

package aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 打印日志的切面
 */
public class LogInvocationHandler implements InvocationHandler {

    private Object target;//目标对象

    public LogInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //执行织入的日志,你可以控制哪些方法执行切入逻辑
        if (method.getName().equals("doSomeThing2")) {
            System.out.println("记录日志");
        }
        //执行原有逻辑
        Object recv = method.invoke(target, args);
        return recv;
    }
}

package aop;

import java.lang.reflect.Proxy;


public class Main {
    public static void main(String[] args) {
        //需要代理的类接口,被代理类实现的多个接口都必须在这这里定义
        Class[] proxyInterface = new Class[] {IBusiness.class, IBusiness2.class};
        //构建AOP的Advice,这里需要传入业务类的实例
        LogInvocationHandler handler = new LogInvocationHandler(new Business());
        //生成代理类的字节码加载器
        ClassLoader classLoader = Business.class.getClassLoader();
        //织入器,织入代码并生成代理类
        IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
        proxyBusiness.doSomeThing2();
        ((IBusiness)proxyBusiness).doSomeThing();
    }
}

执行结果:

记录日志
执行业务逻辑2
执行业务逻辑

3.1.2 动态代理的原理

本节将结合动态代理的源代码讲解其实现原理

动态代理的核心其实就是代理对象的生成,即Proxy.newProxyInstance(classLoader, proxyInterface, handler)

让我们进入newProxyInstance方法观摩下,核心代码就三行:

//获取代理类 
Class cl = getProxyClass(loader, interfaces); 
//获取带有InvocationHandler参数的构造方法 
Constructor cons = cl.getConstructor(constructorParams); 
//把handler传入构造方法生成实例 
return (Object) cons.newInstance(new Object[] { h });   

getProxyClass(loader, interfaces)方法用于获取代理类,它主要做了三件事情:

  • 在当前类加载器的缓存里搜索是否有代理类
  • 没有则生成代理
  • 并缓存在本地JVM里

查找代理类getProxyClass(loader, interfaces)方法:

// 缓存的key使用接口名称生成的List 
Object key = Arrays.asList(interfaceNames); 
synchronized (cache) { 
    do { 
Object value = cache.get(key); 
         // 缓存里保存了代理类的引用 
if (value instanceof Reference) { 
    proxyClass = (Class) ((Reference) value).get(); 
} 
if (proxyClass != null) { 
// 代理类已经存在则返回 
    return proxyClass; 
} else if (value == pendingGenerationMarker) { 
    // 如果代理类正在产生,则等待 
    try { 
cache.wait(); 
    } catch (InterruptedException e) { 
    } 
    continue; 
} else { 
    //没有代理类,则标记代理准备生成 
    cache.put(key, pendingGenerationMarker); 
    break; 
} 
    } while (true); 
}

生成加载代理类:

//生成代理类的字节码文件并保存到硬盘中(默认不保存到硬盘) 
proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); 
//使用类加载器将字节码加载到内存中 
proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length); 

代理类生成过程ProxyGenerator.generateProxyClass()方法的核心代码分析:

//添加接口中定义的方法,此时方法体为空 
for (int i = 0; i < this.interfaces.length; i++) { 
  localObject1 = this.interfaces[i].getMethods(); 
  for (int k = 0; k < localObject1.length; k++) { 
     addProxyMethod(localObject1[k], this.interfaces[i]); 
  } 
} 

//添加一个带有InvocationHandler的构造方法 
MethodInfo localMethodInfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1); 

//循环生成方法体代码(省略) 
//方法体里生成调用InvocationHandler的invoke方法代码。(此处有所省略) 
this.cp.getInterfaceMethodRef("InvocationHandler", "invoke", "Object; Method; Object;") 

//将生成的字节码,写入硬盘,前面有个if判断,默认情况下不保存到硬盘。 
localFileOutputStream = new FileOutputStream(ProxyGenerator.access$000(this.val$name) + ".class"); 
localFileOutputStream.write(this.val$classFile);

通过以上分析,我们可以推出动态代理为我们生产了一个这样的代理类。把方法doSomeThing的方法体修改为调用LogInvocationHandler的invoke方法

public class ProxyBusiness implements IBusiness, IBusiness2 { 

private LogInvocationHandler h; 

@Override 
public void doSomeThing2() { 
    try { 
        Method m = (h.target).getClass().getMethod("doSomeThing", null); 
        h.invoke(this, m, null); 
    } catch (Throwable e) { 
        // 异常处理(略) 
    } 
} 

@Override 
public boolean doSomeThing() { 
    try { 
       Method m = (h.target).getClass().getMethod("doSomeThing2", null); 
       return (Boolean) h.invoke(this, m, null); 
    } catch (Throwable e) { 
        // 异常处理(略) 
    } 
    return false; 
} 

public ProxyBusiness(LogInvocationHandler h) { 
    this.h = h; 
} 

//测试用 
public static void main(String[] args) { 
    //构建AOP的Advice 
    LogInvocationHandler handler = new LogInvocationHandler(new Business()); 
    new ProxyBusiness(handler).doSomeThing(); 
    new ProxyBusiness(handler).doSomeThing2(); 
} 
}

3.1.3 小结

从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题:

  • 第一,代理类必须实现一个接口,如果没实现接口会抛出一个异常
  • 第二,性能影响,因为动态代理是使用反射机制实现的,首先反射肯定比直接调用要慢,其次使用反射大量生成类文件可能引起full gc,因为字节码文件加载后会存放在JVM运行时方法区(或者叫永久代、元空间)中,当方法区满时会引起full gc,所以当你大量使用动态代理时,可以将永久代设置大一些,减少full gc的次数

3.2 CGLIB动态字节码生成

使用动态字节码生成技术CGLIB实现AOP原理是在运行期间目标字节码加载后,生成目标类的子类,将切面逻辑加入到子类中,所以cglib实现AOP不需要基于接口

cglib是一个强大的、高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用cglib前需要引入Asm的jar

3.2.1 使用cglib实现AOP

package cglib;

/**
 * 这个是没有实现接口的实现类
 */
public class BookFacadeImpl {
    public void addBook() {
        System.out.println("增加图书的普通方法。。。");
    }

    public void deleteBook() {
        System.out.println("删除图书的普通方法。。。");
    }
}

package cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 使用cglib动态代理
 */
public class BookFacadeCglib implements MethodInterceptor {

    private Object target;

    /**
     * 创建代理对象
     *
     * @param target
     * @return
     */
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        //回调方法
        enhancer.setCallback(this);
        //创建代理
        return enhancer.create();
    }

    //回调方法
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        if (method.getName().equals("addBook")) {
            System.out.println("记录增加图书的日志");
        }
        methodProxy.invokeSuper(obj, args);
        return null;
    }
}

package cglib;

/**
 * 测试cglib字节码代理
 */
public class TestCglib {
    public static void main(String[] args) {
        BookFacadeCglib cglib = new BookFacadeCglib();
        BookFacadeImpl bookFacade = (BookFacadeImpl) cglib.getInstance(new BookFacadeImpl());
        bookFacade.addBook();
        bookFacade.deleteBook();
    }
}

执行结果:

记录增加图书的日志
增加图书的普通方法。。。
删除图书的普通方法。。。

3.3 自定义类加载器

如果我们实现了一个自定义类加载器,在类加载到JVM之前直接修改某些类的方法,并将切入逻辑织入到这个方法里,然后将修改后的字节码文件交给虚拟机运行,那岂不是更直接

Javassist是一个编辑字节码的框架,可以让你很简单地操作字节码。它可以在运行期定义或修改Class。使用Javassist实现AOP的原理是在字节码加载前直接修改需要切入的方法,这比使用cglib实现AOP更加高效,并且没有太多限制,实现原理如下图:

《Java AOP & Spring AOP 原理和实现》

我们使用类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑

3.3.1 Javassist实现AOP的代码

清单1:启动自定义的类加载器

//获取存放CtClass的容器ClassPool 
ClassPool cp = ClassPool.getDefault(); 
//创建一个类加载器 
Loader cl = new Loader(); 
//增加一个转换器 
cl.addTranslator(cp, new MyTranslator()); 
//启动MyTranslator的main函数 
cl.run("jsvassist.JavassistAopDemo$MyTranslator", args);

    public static class MyTranslator implements Translator { 

        public void start(ClassPool pool) throws NotFoundException, CannotCompileException { 
        } 

        /* * 
         * 类装载到JVM前进行代码织入 
         */ 
        public void onLoad(ClassPool pool, String classname) { 
            if (!"model$Business".equals(classname)) { 
                return; 
            } 
            //通过获取类文件 
            try { 
                CtClass  cc = pool.get(classname); 
                //获得指定方法名的方法 
                CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
                //在方法执行前插入代码 
                m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
            } catch (NotFoundException e) { 
            } catch (CannotCompileException e) { 
            } 
        } 

        public static void main(String[] args) { 
            Business b = new Business(); 
            b.doSomeThing2(); 
            b.doSomeThing(); 
        } 
    }

输出:

执行业务逻辑2   

记录日志   

执行业务逻辑 

3.3.2 小结

从本节中可知,使用自定义的类加载器实现AOP在性能上有优于动态代理和cglib,因为它不会产生新类,但是它仍存在一个问题,就是如果其他的类加载器来加载类的话,这些类就不会被拦截。

3.4 字节码转换

自定义类加载器实现AOP只能拦截自己加载的字节码,那么有一种方式能够监控所有类加载器加载的字节码吗?

有,使用Instrumentation,它是Java5的新特性,使用Instrument,开发者可以构建一个字节码转换器,在字节码加载前进行转换,本节使用Instrumentation和javassist来实现AOP

3.4.1 构建字节码转换器

首先需要创建字节码转换器,该转换器负责拦截Business类,并在Business类的doSomeThing方法前使用javassist加入记录日志的代码

public class MyClassFileTransformer implements ClassFileTransformer { 

    /** 
     * 字节码加载到虚拟机前会进入这个方法 
     */ 
    @Override 
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) 
            throws IllegalClassFormatException { 
        System.out.println(className); 
        //如果加载Business类才拦截 
        if (!"model/Business".equals(className)) { 
            return null; 
        } 

        //javassist的包名是用点分割的,需要转换下 
        if (className.indexOf("/") != -1) { 
            className = className.replaceAll("/", "."); 
        } 
        try { 
            //通过包名获取类文件 
            CtClass cc = ClassPool.getDefault().get(className); 
            //获得指定方法名的方法 
            CtMethod m = cc.getDeclaredMethod("doSomeThing"); 
            //在方法执行前插入代码 
            m.insertBefore("{ System.out.println(\"记录日志\"); }"); 
            return cc.toBytecode(); 
        } catch (NotFoundException e) { 
        } catch (CannotCompileException e) { 
        } catch (IOException e) { 
            //忽略异常处理 
        } 
        return null; 
}

3.4.2 注册转换器

使用premain函数注册字节码转换器,该方法在main函数之前执行

public class MyClassFileTransformer implements ClassFileTransformer { 
    public static void premain(String options, Instrumentation ins) { 
        //注册我自己的字节码转换器 
        ins.addTransformer(new MyClassFileTransformer()); 
} 
} 
3.4.3 配置和执行

需要告诉JVM在启动main函数之前,需要先执行premain函数。

首先,需要将premain函数所在的类打成jar包,并修改jar包里的META-INF\MANIFEST.MF文件

Manifest-Version: 1.0 
Premain-Class: bci. MyClassFileTransformer

其次,在JVM的启动参数里加上-javaagent:D:\java\projects\opencometProject\Aop\lib\aop.jar 

3.4.4 输出

执行main函数,你会发现切入的代码无侵入性的织入进去了

public static void main(String[] args) { 
   new Business().doSomeThing(); 
   new Business().doSomeThing2(); 
} 

输出:

model/Business 

sun/misc/Cleaner 

java/lang/Enum 

model/IBusiness 

model/IBusiness2 

记录日志 

执行业务逻辑 

执行业务逻辑2 

java/lang/Shutdown 

java/lang/Shutdown$Lock

4. Spring AOP

4.1 基于XML配置的Spring AOP

JDK动态代理和CGLib都可以采用基于XML的配置实现AOP,其方法包括四步。

1 定义接口,并创建接口实现类

public interface HelloWorld
{
    void printHelloWorld();
    void doPrint();
}
public class HelloWorldImpl1 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl1.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl1.doPrint()");
        return ;
    }
}
public class HelloWorldImpl2 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl2.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl2.doPrint()");
        return ;
    }
}

2 编辑AOP中需要使用到的通知类(横切关注点,这里是打印时间)

 

public class TimeHandler
{
    public void printTime()
    {
        System.out.println("CurrentTime = " + System.currentTimeMillis());
    }
}

在此可以定义前置和后置的打印。

 

配置容器初始化时需要的XML文件

aop01.xml文件内容如下:

<?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"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" />
        <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" />
        <bean id="timeHandler" class="com.xrq.aop.TimeHandler" />
        
        <aop:config>
            <aop:aspect id="time" ref="timeHandler">
                <aop:pointcut id="addAllMethod" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addAllMethod" />
                <aop:after method="printTime" pointcut-ref="addAllMethod" />
            </aop:aspect>
        </aop:config>
</beans>

4 测试代码Test.java如下:

package com.zhangguo.Spring052.aop01;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Test {
 
    public static void main(String[] args)
    {
        ApplicationContext ctx =
            new ClassPathXmlApplicationContext("aop.xml");
        
        HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1");
        HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2");
        hw1.printHelloWorld();
        System.out.println();
        hw1.doPrint();
    
        System.out.println();
        hw2.printHelloWorld();
        System.out.println();
        hw2.doPrint();
    }
}

运行结果为:

CurrentTime = 1446129611993

Enter HelloWorldImpl1.printHelloWorld()

CurrentTime = 1446129611993

 

CurrentTime = 1446129611994

Enter HelloWorldImpl1.doPrint()

CurrentTime = 1446129611994

 

CurrentTime = 1446129611994

Enter HelloWorldImpl2.printHelloWorld()

CurrentTime = 1446129611994

 

CurrentTime = 1446129611994

Enter HelloWorldImpl2.doPrint()

CurrentTime = 1446129611994

 

 

对于需要使用多个aspect,可以使用如下xml定义:

 

        <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>
 

要想让logHandler在timeHandler前使用有两个办法:

1)aspect里面有一个order属性,order属性的数字就是横切关注点的顺序

2)把logHandler定义在timeHandler前面,Spring默认以aspect的定义顺序作为织入顺序pointcut的expression用于匹配方法,增减其粒度可以达到不同的过滤效果。

 <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.print*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.do*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>

表示timeHandler只会织入HelloWorld接口print开头的方法,logHandler只会织入HelloWorld接口do开头的方法。

 

使用CGLIB生成代理

CGLIB针对代理对象为类的情况使用。在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=”true”/>可强制使用CGLIB生成代理。

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP 

2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP 

3、如果目标对象没有实现类接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

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

* JDK动态代理只能对实现了接口的类生成代理,而不能针对类 

* CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final

CGLIB创建代理主要是创建Enhancer enhancer,并通过AdvisedSupport设置相应的属性,比如目标类rootClass,如果由接口合并接口给代理类,最主要的是设置Callback集合和CallbackFilter,使用CallBackFilter可以根据方法的不同使用不同的Callback进行拦截和增强方法。其中最主要的使用于AOP的Callback是DynamicAdvisedInterceptor。

Spring配置文件中配置的区别:

<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyTargetClass">
<value>true</value>
</property>
</bean>
 
<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.gc.impl.TimeBookInterface</value>
</property>
</bean>

4.2 使用注解配置AOP

使用注解方式开发AOP比较灵活方便,其实现需要如下5个步骤。

1. 配置文件中加入AOP的命名空间

 使用<aop:config/>标签,需要给Spring配置文件中引入基于xml schemaSpring AOP命名空间。完成后的Spring配置文件如下:

<?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-2.5.xsd   
              http://www.springframework.org/schema/aop   
              http://www.springframework.org/schema/aop/spring-aop-2.5.xsd >  
<!--Spring配置信息-->
</beans>  

2. 激活自动代理功能

<?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/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
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-3.0.xsd">
 
<!-- 激活组件扫描功能,在包com.spring.aop.imp及其子包下面自动扫描通过注解配置的组件 -->
<context:component-scan base-package="com.spring.aop.service"/>
<!-- 激活自动代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 用户服务对象 -->
<bean id="userService" class="cn.spring.aop.service.imp.PersonServiceBean" />
 
</beans>

3. 编写实现类

package com.spring.aop.service;  
  
public interface PersonServer {  
  
    public void save(String name);  
    public void update(String name, Integer id);  
    public String getPersonName(Integer id);  
      
}  
package com.spring.aop.service.imp;  
import com.spring.aop.service.PersonServer;  
public class PersonServiceBean implements PersonServer{
    @Override  
    public void save(String name) {  
        System.out.println("我是save方法");  
    //  throw new RuntimeException();  
    }  
  
    @Override  
    public void update(String name, Integer id) {
        System.out.println("我是update()方法");  
    }  
  
    @Override  
    public String getPersonName(Integer id) {
        System.out.println("我是getPersonName()方法");  
        return "xxx";  
    }
}  

4. 编写切面类,并包含@Aspect注解

切面类用于实现切面功能,切面首先是一个IOC中的bean,即加入@Component注解,切面还需要加入@Aspect注解

package com.spring.aop.impl;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
//指定切面的优先级,当有多个切面时,数值越小优先级越高
@Order(1)
//把这个类声明为一个切面:需要把该类放入到IOC容器中。再声明为一个切面.
@Aspect
@Component
public class LoggingAspect {
 
    /**
     * 声明切入点表达式,一般在该方法中不再添加其他代码。
     * 使用@Pointcut来声明切入点表达式。
     * 后面的通知直接使用方法名来引用当前的切入点表达式。
     */
    @Pointcut("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")//此处定义了其切入点
    public void declareJoinPointExpression() {}
 
    /**
    *前置通知,在目标方法开始之前执行。
    *@Before("execution(public int com.spring.aop.impl.PersonServiceBean.add(int, int))")这样写可以指定特定的方法。
     * @param joinpoint
     */
    @Before("declareJoinPointExpression()")
    //这里使用切入点表达式即可。后面的可以都改成切入点表达式。如果这个切入点表达式在别的包中,在前面加上包名和类名即可。
    public void beforeMethod(JoinPoint joinpoint) {
        String methodName = joinpoint.getSignature().getName();
        List<Object>args = Arrays.asList(joinpoint.getArgs());
        System.out.println("前置通知:The method "+ methodName +" begins with " + args);
    }
 
    /**
    *后置通知,在目标方法执行之后开始执行,无论目标方法是否抛出异常。
    *在后置通知中不能访问目标方法执行的结果。
     * @param joinpoint
     */
    @After("execution(public int com.spring.aop.impl.PersonServiceBean.*(int, int))")
    public void afterMethod(JoinPoint joinpoint) {
        String methodName = joinpoint.getSignature().getName();
        //List<Object>args = Arrays.asList(joinpoint.getArgs());  后置通知方法中可以获取到参数
        System.out.println("后置通知:The method "+ methodName +" ends ");
    }
     
    /**
    *返回通知,在方法正常结束之后执行。
    *可以访问到方法的返回值。
     * @param joinpoint
     * @param result 目标方法的返回值
     */
    @AfterReturning(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", returning="result")
    public void afterReturnning(JoinPoint joinpoint, Object result) {
        String methodName = joinpoint.getSignature().getName();
        System.out.println("返回通知:The method "+ methodName +" ends with " + result);
    }
     
    /**
    *异常通知。目标方法出现异常的时候执行,可以访问到异常对象,可以指定在出现特定异常时才执行。
    *假如把参数写成NullPointerException则只在出现空指针异常的时候执行。
     * @param joinpoint
     * @param e
     */
    @AfterThrowing(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", throwing="e")
    public void afterThrowing(JoinPoint joinpoint, Exception e) {
        String methodName = joinpoint.getSignature().getName();
        System.out.println("异常通知:The method "+ methodName +" occurs exception " + e);
    }
     
    /**
     * 环绕通知类似于动态代理的全过程,ProceedingJoinPoint类型的参数可以决定是否执行目标方法。
     * @param point 环绕通知需要携带ProceedingJoinPoint类型的参数。
     * @return 目标方法的返回值。必须有返回值。
     */
     /*不常用
    @Around("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")
    public Object aroundMethod(ProceedingJoinPoint point) {
        Object result = null;
        String methodName = point.getSignature().getName();
        try {
            //前置通知
            System.out.println("The method "+ methodName +" begins with " + Arrays.asList(point.getArgs()));
            //执行目标方法
            result = point.proceed();
            //翻译通知
            System.out.println("The method "+ methodName +" ends with " + result);
        } catch (Throwable e) {
            //异常通知
            System.out.println("The method "+ methodName +" occurs exception " + e);
            throw new RuntimeException(e);
        }
        //后置通知
        System.out.println("The method "+ methodName +" ends");
        return result;
    }
    */
}

5. 测试类

package com.spring.aop.aspect;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
import cn.spring.aop.service.imp.UserService;
import cn.spring.mvc.bean.User;
 
/**
 * Spring AOP测试
 * @author Shenghany
 * @date 2013-5-28
 */
public class Tester {
 
    private final static Log log = LogFactory.getLog(Tester.class);
    public static void main(String[] args) {
        //启动Spring容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //获取service组件
        UserService service = (UserService) context.getBean("userService");
        //以普通的方式调用UserService对象的三个方法
        User user = service.get(1L);
        service.save(user);
        try {
            service.delete(1L);
        } catch (Exception e) {
            if(log.isWarnEnabled()){
                log.warn("Delete user : " + e.getMessage());
            }
        }
    }
}

运行可知其达到了AOP的目的。

4.3 零配置实现Spring IoC与AOP

为了实现零配置在原有示例的基础上我们新增一个类User,如下所示:

package com.zhangguo.Spring052.aop05;
 
public class User {
    public void show(){
        System.out.println("一个用户对象");
    }
}

该类并未注解,容器不会自动管理。因为没有xml配置文件,则使用一个作为配置信息,ApplicationCfg.java文件如下:

package com.zhangguo.Spring052.aop05;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
 
@Configuration  //用于表示当前类为容器的配置类,类似<beans/>
@ComponentScan(basePackages="com.zhangguo.Spring052.aop05")  //扫描的范围,相当于xml配置的结点<context:component-scan/>
@EnableAspectJAutoProxy(proxyTargetClass=true)  //自动代理,相当于<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
public class ApplicationCfg {
    //在配置中声明一个bean,相当于<bean id=getUser class="com.zhangguo.Spring052.aop05.User"/>
    @Bean
    public User getUser(){
        return new User();
    }
}

该类的每一部分内容基本都与xml 配置有一对一的关系,请看注释,这样做要比写xml方便,但不便发布后修改。

Advice类代码如下:

package com.zhangguo.Spring052.aop04;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
 
/**
 * 通知类,横切逻辑
 */
@Component
@Aspect
public class Advices {
    //切点
    @Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.a*(..))")
    public void pointcut(){
    }
    
    //前置通知
    @Before("pointcut()")
    public void before(JoinPoint jp){
        System.out.println(jp.getSignature().getName());
        System.out.println("----------前置通知----------");
    }
    
    //最终通知
    @After("pointcut()")
    public void after(JoinPoint jp){
        System.out.println("----------最终通知----------");
    }
    
    //环绕通知
    @Around("execution(* com.zhangguo.Spring052.aop04.Math.s*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println(pjp.getSignature().getName());
        System.out.println("----------环绕前置----------");
        Object result=pjp.proceed();
        System.out.println("----------环绕后置----------");
        return result;
    }
    
    //返回结果通知
    @AfterReturning(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.m*(..))",returning="result")
    public void afterReturning(JoinPoint jp,Object result){
        System.out.println(jp.getSignature().getName());
        System.out.println("结果是:"+result);
        System.out.println("----------返回结果----------");
    }
    
    //异常后通知
    @AfterThrowing(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.d*(..))",throwing="exp")
    public void afterThrowing(JoinPoint jp,Exception exp){
        System.out.println(jp.getSignature().getName());
        System.out.println("异常消息:"+exp.getMessage());
        System.out.println("----------异常通知----------");
    }
}

 

测试代码如下:

package com.zhangguo.Spring052.aop05;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Test {
 
    public static void main(String[] args) {
        // 通过类初始化容器
        ApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationCfg.class);
        Math math = ctx.getBean("math", Math.class);
        int n1 = 100, n2 = 0;
        math.add(n1, n2);
        math.sub(n1, n2);
        math.mut(n1, n2);
        try {
            math.div(n1, n2);
        } catch (Exception e) {
        }
        
        User user=ctx.getBean("getUser",User.class);
        user.show();
    }
 
}

测试结果能够满足要求。

5. Spring 中的advice和aspect的区别

5.1 含义区别

— /切 面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用springAdvisor或拦截器实现。 
— 通知(Advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”“before”“throws”通知。 
— 切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。 

— 连接点/织入点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。 

所以“<aop:aspect>”实际上是定义横切逻辑,就是在连接点上做什么,“<aop:advisor>”则定义了在哪些连接点应用什么<aop:aspect>Spring这样做的好处就是可以让多个横切逻辑(即<aop:aspect>定义的)多次使用,提供可重用性。 

1Advisor是一种特殊的AspectAdvisor代表spring中的Aspect 
2、区别:advisor只持有一个Pointcut和一个advice,而aspect可以多个pointcut和多个advice

 

5.2 advisor和aspect的使用区别

spring的配置中,会用到这两个标签.那么他们的区别是什么呢?

<bean id=”testAdvice” class=”com.myspring.app.aop.MyAdvice”/> //切面代码

分别使用aspect和advisor定义一个aspect如下:

<aop:config>
        <aop:aspect ref="testAdvice" id="testAspect">
            <aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))" id="testPointcut"/>
            <aop:before  method="doBefore" pointcut-ref="testPointcut"/>
        </aop:aspect>
    </aop:config>
 
    <aop:config>
        <aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))"  id="mypoint"/>
        <aop:advisor advice-ref="testAdvice" pointcut-ref="mypoint"/>
    </aop:config>

使用代码如下:

package com.myspring.app.aop;
 
import java.lang.reflect.Method;
 
import org.aspectj.lang.JoinPoint;
import org.springframework.aop.MethodBeforeAdvice;
 
/**
 * 方法前置通知
 * @author Michael
 *
 */
@Component("myAdvice")//如果是自动装配,在定义切面的时候直接写在ref属性里就可以了
public class MyAdvice implements MethodBeforeAdvice{
    //如果使用aop:advisor配置,那么切面逻辑必须要实现advice接口才行!否则会失败!
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知");
    }
 
    //如果是<aop:aspect>配置,编写一般的方法就可以了,然后在切面配置中指定具体的方法名称!
    public void doBefore(JoinPoint point) throws Throwable {
 
    }
}

两者的区别在于:

1. 如果使用aop:advisor配置,那么切面逻辑必须要实现advice接口才行!否则会失败。
2. 如果是aop:aspect配置,编写一般的方法就可以了,然后在切面配置中指定具体的方法名称。

    原文作者:AOP
    原文地址: https://blog.csdn.net/KingCat666/article/details/76911704
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞