`

aop的几种实现方式

    博客分类:
  • j2se
 
阅读更多
1 AOP各种的实现



AOP就是面向切面编程,我们可以从几个层面来实现AOP。





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




类别

机制

原理

优点

缺点


静态AOP

静态织入

在编译期,切面直接以字节码的形式编译到目标字节码文件中。

对系统无性能影响。

灵活性不够。


动态AOP

动态代理

在运行期,目标类加载后,为接口动态生成代理类,将切面植入到代理类中。

相对于静态AOP更加灵活。

切入的关注点需要实现接口。对系统有一点性能影响。


动态字节码生成

在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中。

没有接口也可以织入。

扩展类的实例方法为final时,则无法进行织入。


自定义类加载器

在运行期,目标加载前,将切面逻辑加到目标字节码里。

可以对绝大部分类进行织入。

代码中如果使用了其他类加载器,则这些类将不会被织入。


字节码转换

在运行期,所有类加载器加载字节码前,前进行拦截。

可以对所有类进行织入。






2 AOP里的公民 
•Joinpoint:拦截点,如某个业务方法。
•Pointcut:Joinpoint的表达式,表示拦截哪些方法。一个Pointcut对应多个Joinpoint。
•Advice:  要切入的逻辑。
•Before Advice 在方法前切入。
•After Advice 在方法后切入,抛出异常时也会切入。
•After Returning Advice 在方法返回后切入,抛出异常则不会切入。
•After Throwing Advice 在方法抛出异常时切入。
•Around Advice 在方法执行前后切入,可以中断或忽略原有流程的执行。 
•公民之间的关系

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


3 AOP的实现机制
  本章节将详细介绍AOP有各种实现机制。


3.1 动态代理
  Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。使用动态代理实现AOP需要有四个角色:被代理的类,被代理类的接口,织入器,和InvocationHandler,而织入器使用接口反射机制生成一个代理类,然后在这个代理类中织入代码。被代理的类是AOP里所说的目标,InvocationHandler是切面,它包含了Advice和Pointcut。


3.1.1 使用动态代理
  那如何使用动态代理来实现AOP。下面的例子演示在方法执行前织入一段记录日志的代码,其中Business是代理类,LogInvocationHandler是记录日志的切面,IBusiness, IBusiness2是代理类的接口,Proxy.newProxyInstance是织入器。
清单一:动态代理的演示
public static void main(String[] args) {
    //需要代理的接口,被代理类实现的多个接口都必须在这里定义
    Class[] proxyInterface = new Class[] { IBusiness.class, IBusiness2.class };
    //构建AOP的Advice,这里需要传入业务类的实例
    LogInvocationHandler handler = new LogInvocationHandler(new Business());
    //生成代理类的字节码加载器
    ClassLoader classLoader = DynamicProxyDemo.class.getClassLoader();
    //织入器,织入代码并生成代理类
    IBusiness2 proxyBusiness = (IBusiness2) Proxy.newProxyInstance(classLoader, proxyInterface, handler);
    //使用代理类的实例来调用方法。
    proxyBusiness.doSomeThing2();
    ((IBusiness) proxyBusiness).doSomeThing();
}

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

    private Object target; //目标对象

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

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

接口IBusiness和IBusiness2定义省略。





   业务类,需要代理的类。
public class Business implements IBusiness, IBusiness2 {

    @Override
    public boolean doSomeThing() {
        System.out.println("执行业务逻辑");
        return true;
    }

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

}




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




  可以看到“记录日志”的逻辑切入到Business类的doSomeThing方法前了。




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里。清单三:查找代理类。
// 缓存的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()方法属于sun.misc包下,Oracle并没有提供源代码,但是我们可以使用JD-GUI这样的反编译软件打开jre\lib\rt.jar来一探究竟,以下是其核心代码的分析。
清单五:代理类的生成过程
//添加接口中定义的方法,此时方法体为空
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 小结
    从前两节的分析我们可以看出,动态代理在运行期通过接口动态生成代理类,这为其带来了一定的灵活性,但这个灵活性却带来了两个问题,第一代理类必须实现一个接口,如果没实现接口会抛出一个异常。第二性能影响,因为动态代理使用反射的机制实现的,首先反射肯定比直接调用要慢,经过测试大概每个代理类比静态代理多出10几毫秒的消耗。其次使用反射大量生成类文件可能引起Full GC造成性能影响,因为字节码文件加载后会存放在JVM运行时区的方法区(或者叫持久代)中,当方法区满的时候,会引起Full GC,所以当你大量使用动态代理时,可以将持久代设置大一些,减少Full GC次数。

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




    本节介绍如何使用Cglib来实现动态字节码技术。Cglib是一个强大的,高性能的Code生成类库,它可以在运行期间扩展Java类和实现Java接口,它封装了Asm,所以使用Cglib前需要引入Asm的jar。 清单七:使用CGLib实现AOP
public static void main(String[] args) {
        byteCodeGe();
    }

    public static void byteCodeGe() {
        //创建一个织入器
        Enhancer enhancer = new Enhancer();
        //设置父类
        enhancer.setSuperclass(Business.class);
        //设置需要织入的逻辑
        enhancer.setCallback(new LogIntercept());
        //使用织入器创建子类
        IBusiness2 newBusiness = (IBusiness2) enhancer.create();
        newBusiness.doSomeThing2();
    }

    /**
     * 记录日志
     */
    public static class LogIntercept implements MethodInterceptor {

        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            //执行原有逻辑,注意这里是invokeSuper
            Object rev = proxy.invokeSuper(target, args);
            //执行织入的日志
            if (method.getName().equals("doSomeThing2")) {
                System.out.println("记录日志");
            }
            return rev;
        }
    }







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





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






    我们使用系统类加载器启动我们自定义的类加载器,在这个类加载器里加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,咱们再看看使用Javassist实现AOP的代码:
清单八:启动自定义的类加载器
//获取存放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
记录日志
执行业务逻辑

 
    其中Bussiness类在本文的清单一中定义。看起来是不是特别简单,CtClass是一个class文件的抽象描述。咱们也可以使用insertAfter()在方法的末尾插入代码,使用insertAt()在指定行插入代码。

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

3.4 字节码转换
    自定义的类加载器实现AOP只能拦截自己加载的字节码,那么有没有一种方式能够监控所有类加载器加载字节码呢?有,使用Instrumentation,它是 Java 5 提供的新特性,使用 Instrumentation,开发者可以构建一个字节码转换器,在字节码加载前进行转换。本节使用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 AOP实战
说了这么多理论,那AOP到底能做什么呢? AOP能做的事情非常多。
•性能监控,在方法调用前后记录调用时间,方法执行太长或超时报警。
•缓存代理,缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
•软件破解,使用AOP修改软件的验证类的判断逻辑。
•记录日志,在方法执行前后记录系统日志。
•工作流系统,工作流系统需要将业务代码和流程引擎代码混合在一起执行,那么我们可以使用AOP将其分离,并动态挂接业务。
•权限验证,方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。


4.1 Spring的AOP
    Spring默认采取的动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。但Spring的AOP有一定的缺点,第一个只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。第二个同类中的互相调用方法将不会使用代理类。因为要使用代理类必须从Spring容器中获取Bean。第三个性能不是最好的,从3.3章节我们得知使用自定义类加载器,性能要优于动态代理和CGlib。
可以获取代理类
public IMsgFilterService getThis()
{
        return (IMsgFilterService) AopContext.currentProxy();
}

public boolean evaluateMsg () {
   // 执行此方法将织入切入逻辑
return getThis().evaluateMsg(String message);
}

@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {


不能获取代理类
public boolean evaluateMsg () {
   // 执行此方法将不会织入切入逻辑
return evaluateMsg(String message);
}

@MethodInvokeTimesMonitor("KEY_FILTER_NUM")
public boolean evaluateMsg(String message) {





4.2 参考资料
•Java 动态代理机制分析及扩展
•CGlib的官方网站
•ASM官方网站
•JbossAOP
•Java5特性Instrumenttation实践
分享到:
评论

相关推荐

    利用C#实现AOP常见的几种方法详解

    在C#中,实现AOP的方法多种多样,以下将详细介绍几种常见的实现方式。 1. **静态织入**: 静态织入是在编译时完成的,它通过编译器或者编译插件(如PostSharp)在目标类的代码中插入拦截逻辑。这种方式的优点是...

    Spring AOP 常用的四种实现方式

    本篇文章将深入探讨Spring AOP的四种常见实现方式。 一、基于接口的代理(Interface-Based Proxy) 这是Spring AOP最基础的实现方式,适用于目标对象实现了特定接口的情况。Spring会创建一个代理对象,该对象实现...

    spring AOP配置的几种方式

    本文主要介绍几种常见的Spring AOP配置方式,并通过具体的示例来说明每种配置的特点。 #### 二、AOP配置所需基本元素 配置AOP时需要以下三个基本元素: 1. **Advice**:这是实际执行的代码,即我们所说的“切面”...

    spring入门学习-6、AOP几种配置方式详解.pdf

    ### Spring AOP 几种配置方式详解 #### 一、Spring AOP 概述 Spring AOP(面向切面编程)是一种强大的编程模式,用于在应用程序中管理横切关注点,如日志记录、安全控制等。Spring 提供了多种方式来支持 AOP 的...

    注解方式实现AOP编程

    总结一下,注解方式实现AOP编程主要包括以下几个步骤: 1. 创建一个`@Aspect`注解的类来定义切面。 2. 使用`@Pointcut`定义切点表达式。 3. 使用`@Before`, `@After`, `@AfterReturning`, `@AfterThrowing`, 或 `@...

    AOP的实现机制

    AOP可以通过多种方式实现,主要分为以下几种: - **静态AOP** - **动态AOP** - 动态代理 - 动态字节码生成 - 自定义类加载器 - 字节码转换 #### 2. AOP里的公民 在AOP领域,有几个重要的概念被称为“公民”:...

    java分页 动态代理 aop实现

    在Java中,实现分页通常有以下几种方式: 1. 手动分页:开发者自行编写SQL查询,通过LIMIT和OFFSET来控制每次返回的数据量。 2. ORM框架支持:如Hibernate、MyBatis等提供分页API,简化了SQL编写。 3. 分页库:例如...

    研究下Spring中AOP的实现?

    Spring AOP有两种实现方式:代理模式和注解驱动。代理模式分为JDK动态代理和CGLIB代理。JDK动态代理适用于实现了接口的目标对象,它通过实现InvocationHandler接口创建代理对象。而CGLIB代理则是在运行时为类生成...

    spring aop xml实现

    在Spring中实现AOP,我们需要在XML配置文件中定义以下几个部分: 1. **配置Spring容器**:首先,确保Spring的配置文件(如`applicationContext.xml`)已经包含了AOP的命名空间,通常添加如下: ```xml xmlns:aop=...

    一个简单的spring AOP的实现

    1. 注解驱动的AOP:这是最常用的实现方式,通过在方法上添加注解(如`@Before`, `@After`, `@Around`, `@AfterReturning`, `@AfterThrowing`)来定义通知,并使用`@Aspect`注解定义切面。 2. XML配置驱动的AOP:...

    Java JDK 实现AOP

    AOP的核心概念主要包括以下几个方面: 1. **关注点(Concern)**:指的是软件工程中一个可管理且可描述的部分,通常与某个特定的概念或目标关联。 2. **主关注点(Core Concern)**:指软件中最为核心的关注点,...

    aop思想的java代码实现

    在这个项目中,提供了三种AOP的实现方式: 1. **非AOP实现**:这个包可能包含了传统的编程方式,业务逻辑与横切关注点混合在一起,没有使用AOP的概念。这通常是AOP引入前的代码结构,便于对比和理解AOP的作用。 2....

    Spring AOP的几种实现方式总结

    下面我们将深入探讨Spring AOP的核心概念以及几种实现方式。 **核心概念** 1. **横切关注点**:这是在应用程序中的某个特定位置(如数据访问、事务管理等)执行的通用任务。例如,日志记录、性能度量等。 2. **切...

    Java中AOP编程实现.rar

    Java中的AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它扩展了传统的面向对象编程,使得开发者可以更方便地处理系统中的横切关注点,如日志、事务管理、权限控制等。这些关注点通常分散在系统...

    EJB+Annotation实现AOP的DEMO

    AOP(Aspect Oriented Programming,面向切面编程)则是一种编程范式,用于处理系统中的横切关注点,如日志、事务管理等,以提高代码的模块化和可维护性。在Java EE环境中,EJB结合AOP可以实现更加灵活和高效的代码...

    spring aop spring aop

    总的来说,Spring AOP提供了一种优雅的方式来管理横切关注点,使代码更整洁,降低了模块间的耦合。在实际开发中,它可以用于日志记录、权限控制、事务管理等多个场景,极大地提高了代码的可维护性和复用性。

    C# 实现的AOP框架

    C#实现的AOP框架可以通过动态代理、编译时织入或运行时织入等方式实现。 C#中常见的AOP框架有Unity、Autofac、Castle Windsor等,它们提供了方便的方式来定义和注入切面。例如,Castle Windsor框架利用其动态代理库...

    Spring-Aop源码实现

    根据给定文件的信息来看,这段内容实际上与Spring-AOP源码实现并无直接关联,而是关于Hadoop的基础介绍及其生态系统中的几个重要组件。然而,既然任务要求是从这些信息中提炼相关知识点,我们将尝试从Hadoop的角度...

Global site tag (gtag.js) - Google Analytics