Spring从入门到精通—AOP与AspectJ的关系?原生JDK和CGLib手动实现AOP?

1、AOP和AspectJ概述(Spring AOP是什么?AOP有什么用?)

1.1 AOP简介
  1. 在软件行业,AOP为Aspect Oriented programming 的缩写,意为:面向切面编程,它是一种编程思想。AOP是OOP(面向对象编程)思想的延续。AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码的编写方式(例如性能监视、事务管理、安全检查、缓存、日志记录等)
  2. AOP的核心思想:基于代理思想,对目标对象创建代理对象,在不修改原目标对象的情况下,通过代理对象,调用增强功能代码,从而对原有业务方法的功能进行增强。
1.2 AOP的作用
  1. 利用AOP可以对业务逻辑的各个部分进行分离,从而使得业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。
  2. AOP采用横向抽取机制,取代了传统继承体系的纵向机制。
  3. AOP的经典应用场景:事务管理、性能监视、安全、缓存、日志…..
1.3 Spring AOP编程的两种方式
  1. Spring AOP 使用纯Java代码实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。
  2. AspectJ是一个基于Java的AOP框架,Spring2.0开始,Spring AOP引入AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入。
1.4 AspectJ是什么?能做什么?

AspectJ是一个易用且功能强大的AOP框架,并不是Spring的一部分。AspectJ的全称是Eclipse AspectJ,其官网地址是:www.eclipse.org/aspectj/. AspectJ的特性如下:

  • 是一种基于Java平台的面向切面编程的语言
  • 兼容Java,可以无缝扩展
  • 易学易用,可以单独使用,也可以整合到其他框架中,单独使用AspectJ需要使用专门的编译器ajc
1.5 AspectJ和Spring AOP的区别?

两者的区别如下:
Spring AOP:

  1. 基于动态代理来实现,默认如果使用接口,那么会用JDK提供动态代理实现,如果是方法则使用CGLib提供代理实现。
  2. Spring AOP需要依赖IOC容器来管理,并且只能用于Spring容器,使用纯Java代码实现。
  3. 在性能上,由于Spring AOP是基于动态代理实现的,在容器启动时需要生成代理实例,在方法调用上也会增加桟的深度,使用Spring AOP的性能不如AspectJ好。

AspectJ:

  1. AspectJ属于静态织入,通过修改代码来实现,一般AOP有三种织入的方式:
    • 编译期织入(Compile-time weaving):类A中使用了AspectJ添加了一个属性,类B引用了它,这个场景就需要在编译时进行织入,否则没法编译类B。
    • 编译后织入(Post-compile weaving):当目标代码已经编译成.class字节码文件了的时候可以使用此方法来织入增强的代码
    • 类加载后织入(Load-time weaving):在类加载的时候进行织入,要实现这个时期的织入一般有两种办法:自定义类加载器专门来进行织入;在JVM启动的时候指定AspectJ提供agent:-javaagent:xxx/xxx/aspectjweaver.jar
  2. AspectJ是AOP编程的完全解决方案,Spring AOP则只是致力于解决企业级开发中最普遍的AOP(方法织入)
  3. 由于AspectJ在实际运行之前就完成了织入,因此由AspectJ生成的类是没有额外的运行时开销的。

2、AOP的一些术语【了解】

AOP(Aspect Oriented Programming)像大多数技术一样形成了自己的术语,而且这些术语比较难理解,不论是否理解都对编程影响不太大,但是最起码要清除有这样一个东西。

  1. 连接点(Joinpoint)
    程序执行的某个特定位置:如类开始初始化前、类初始化后、类某个方法调用前、调用后、方法抛出异常后。一个类或一段程序代码拥有一些具有边界性质的特定点,这些点中的特定点就称为“连接点”。Spring 仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。

  2. 切点(Pointcut)
    每个程序类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。AOP 通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。

  3. 通知或增强(Advice)
    增强是织入到目标类连接点上的一段程序代码,在 Spring 中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点。

  4. 目标对象(Target)
    增强逻辑的织入目标类。如果没有 AOP,目标业务类需要自己实现所有逻辑,而在 AOP的帮助下,目标业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用 AOP 动态织入到特定的连接点上.

  5. 引介(Introduction)
    引介是一种特殊的增强,它为类添加一些属性和方法。这样 ,即使一个业务类原本没有实现某个接口,通过 AOP 的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

  6. 织入(Weaving)
    织入是将增强添加对目标类具体连接点上的过程。AOP 像一台织布机,将目标类、增强或引介通过 AOP 这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP 有三种织入的方式:

  7. 代理(Proxy)
    一个类被 AOP 织入增强后,就产出了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。

  8. 切面(Aspect)
    切面由切点和增强(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义,Spring AOP 就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

对于 Spring 中的 AOP 术语,确实不好理解,为了帮助大家理解这些术语,我们想象一下这样的场景:

        有 2 条高速公路可以通向北京,分别为 A,B(相当于 2 个目标个业务 Target(功能),每条高速公路上有 3 个服务站(相当于每个业务上的连接点(Joinpoint)有 3 个),在每条高速公路的第 2 个服务站需要测速(相当于一个切点(Pointcut),匹配了 3 个连接点),在第一条(A)进入服务站之前进行测速,在第二条(b)进入服务站之后测速(通知或增强 Advice,其实也定义了调用测速功能进行测速,以及怎么测),每条高速路第 2 个服务站加入测速功能,这件事情相当于切面 Aspect,它包含了测速功能的定义与切点的定义,将测速功能应用到服务站上,这个过程叫织入(Weaving)

3、AOP的实现原理

接下来是重点,我们来学习一下AOP的实现原理,并利用原理自己手动实现AOP。
AOP底层采用代理机制进行实现,具体的实现方法有以下两种:

1)接口+实现类:采用jdk的动态代理
2)使用实现类:Spring采用了cglib字节码增强

接下来我们就用这两个原理分别自己手动实现AOP。

4、使用jdk的动态代理实现AOP

实现思路:使用jdk中的Proxy类的newProxyInstance方法来获得一个代理对象,此后当使用某个对象的时候就使用这个代理对象而不直接去new对象,好了废话不多说,直接上代码

//Java newProxyInstance方法API
public static Object newProxyInstance(
              ClassLoader loader,  类加载器,写当前类的类加载器
              Class<?>[] interfaces,  接口,就是你要增强的业务类的接口
              InvocationHandler h){}   处理器,可以自己实现InvocationHandler接口的invkoe方法实现一个处理器

步骤
1. 写一个普通的接口以及这个接口的实现类
2. 写一个切面类(就是一个普通的java类,里面的方法写要增强的功能)
3. 上面的操作完成后,在工厂类(为了方便,当然也可以不写这个工厂类)中首先创建一个目标类对象(就是new一个业务类的对象),接着new一个切面类对象,使用动态代理把切面类中的增强功能织入到目标方法的前后,下面是示例代码:

4.1 首先写定义一个业务类接口:UserServiceBase.java
package com.xzy.sevice;

public interface UserSeviceBase {
    /**
     * 增加用户
     */
    public void addUser();

    /**
     * 删除用户
     * @param id
     */
    public void deleteUser(int id);

    /**
     * 更新用户
     * @param id
     */
    public void updateUser(int id);

}
4.2 写一个业务接口的实现类:UserSeviceImpi.java
package com.xzy.sevice;

public class UserSeviceImpi implements  UserSeviceBase {
    @Override
    public void addUser() {
        System.out.println("增加了1个用户");
    }

    @Override
    public void deleteUser(int id) {
       System.out.println("删除了id为"+id+"的用户");
    }

    @Override
    public void updateUser(int id) {
      System.out.println("id为"+id+"的用户更新了");
    }
}
4.3 基于Java原生API写一个切面类:MyAspect.java
package com.xzy.aspect;

import java.util.Date;

//切面类:就是一段增强功能的代码
public class MyAspect {


    public void before(){
        System.out.println(".......执行操作前日志......."+new Date());
    }


    public void after(){
        System.out.println(".......执行操作后日志......."+new Date());
    }
}
4.4 写一个工厂类,专门用于生产代理对象(可以没有)
package com.xzy.sevice;

import com.xzy.aspect.MyAspect;

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

public class ServiceFactory {

    /**
     * 产生一个 UserSevice
     *
     * @return
     */
    public static UserSeviceBase createUserService() {

        //1.创建目标对象
        final UserSeviceBase userSevice = new UserSeviceImpl();

        //2.声明切面类
        final MyAspect aspect = new MyAspect();

        //3.使用动态代理来把切面类中的增强方法切入目标方法前后
        UserSeviceBase proxyService = (UserSeviceBase) Proxy.newProxyInstance(
                ServiceFactory.class.getClassLoader(),
                userSevice.getClass().getInterfaces(),
                //InvocationHandler是一个接口,它里面只有一个invoke方法,这里也可以在外面单独写一个类实现InvocationHandler接口,然后在这里new这个类的对象也可以
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //目标方法开始前的增强代码
                    aspect.before();
                    //通过反射调用目标方法
                    Object obj = method.invoke(userSevice, args);
                    //目标方法执行后的增强代码
                    aspect.after();
                    return obj;
                    }
                }
        );
        return proxyService;    //返回代理对象
    }

}
4.5 测试:
package com.xzy;

import com.xzy.sevice.ServiceFactory;
import com.xzy.sevice.UserSeviceBase;
import org.junit.Test;

/**
 * Unit test for simple App.
 */
public class AppTest {

 /**
  * 使用Java的动态代理机制,手动实现AOP编程
  */
 @Test
 public void test1(){
   //直接从工厂中拿代理对象
   UserSeviceBase userSevice= ServiceFactory.createUserService();
   userSevice.deleteUser(10);
   userSevice.updateUser(3);
   userSevice.addUser();
 }
}

测试结果

5、使用Cglib实现

CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib 下面是CGLib的组成结构:

CGLIB底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类。除了CGLIB库外,脚本语言(如Groovy何BeanShell)也使用ASM生成字节码。ASM使用类似SAX的解析器来实现高性能。CGLIB相比于JDK动态代理更加强大,JDK动态代理虽然简单易用,但是其有一个致命缺陷是,只能对接口进行代理。如果要代理的类为一个普通类、没有接口,那么Java动态代理就没法使用了。接下来我们就用CGLib手动实现AOP

5.1 首先,需要导入Cglib所需的jar包,Maven依赖写法如下:
<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.1</version>
</dependency>
5.2 实现业务类

还是需要一个业务类(这次不需要接口了)以及一个切面类,前面写过,这里就跳过了。

5.3 基于cglib的API实现切面类

接着在刚才的工厂类中再写一个方法createUserService2(),步骤还是那几步,只不过这次是用cglib提供的api来写,具体如下:

 /*
     *使用cglib实现AOP
     *
     */
    public static UserSeviceBase createUserService2() {

        //1.创建目标对象
        final UserSeviceImpl userSevice = new UserSeviceImpl();

        //2.声明切面类
        final MyAspect aspect = new MyAspect();

        //3.cglib核心类Enhancer
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(userSevice.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                aspect.before();
                /*
                 *proxy代理是目标类的子类
                 */
                Object obj = methodProxy.invoke(userSevice, args);
                aspect.after();
                return obj;
            }
        });
        UserSeviceImpl proxy = (UserSeviceImpl) enhancer.create();
        return proxy;  //返回代理对象
    }

5.4 测试:

package com.xzy;

import com.xzy.sevice.ServiceFactory;
import com.xzy.sevice.UserSeviceBase;
import org.junit.Test;

/**
 * Unit test for simple App.
 */
public class AppTest 
{

    /**
     * 使用cglib手动实现AOP编程
     */
    @Test
    public void test2(){
        UserSeviceBase userSevice= ServiceFactory.createUserService2();
        userSevice.deleteUser(10);
        userSevice.updateUser(3);
        userSevice.addUser();
    }
}

测试结果

可以看到结果和刚才用JDK动态代理的结果一样,但是这里要特别注意:

  • jdk代理只能动态代理接口+实现类的形式;
  • Cglib代理的优势是可以直接代理普通的类,但同时接口也可以

留言区

还能输入500个字符