美文网首页
AOP 面向切面编程

AOP 面向切面编程

作者: bit_拳倾天下 | 来源:发表于2021-02-08 10:36 被阅读0次

0. 相关概念

0.1 AOP 简介

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过 预编译方式运行期动态代理 实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码
经典应用:事务管理、性能监视、安全检查、缓存 、日志等

0.2 代理模式:

代理模式有静态代理动态代理两种实现方式。
参考1
参考2

0.2.1 静态代理

就是在代码运行之前,代理类就已经存在,通常情况下, 静态代理中的代理类和委托类会实现同一接口或是派生自相同的父类。

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。

从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

0.2.2 动态代理

代理类在程序运行时创建的代理方式被称为动态代理,也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指令”动态生成的。

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,像 mybatis 的 Mapper 接口,我们并没有具体实现它,但是它却能完成 sql 操作,就是使用了动态代理。
另外,其内部对代理类方法的所有实现,都是执行 invoke 方法,于是乎就完成了增强

动态代理其实是一种方便运行时候动态的处理代理方法的调用机制,通过代理可以让调用者和实现者之间解耦。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

动态代理实现

1)JDK动态代理主要是基于反射,使用反射解析目标对象的属性、方法等

根据解析的内容生成proxy.class,说白了就是把要生成的class按照字符串的形式拼接,最终通过ClassLoader加载。

newProxyInstance 内部通过 ProxyGenerator.generateProxyClass() 方法,生成一个代理类 Class(代理类的类名都是以 $Proxy 开头的),它同时实现了 InvocationHandler 和被代理接口,然后通过传参中的类加载器加载代理类,然后通过反射创建代理类对象。

缺点:
a. 被代理对象需要有适当的父接口,否则无法代理
b. 原理基于反射,效率低

2)CGLIB 介绍&原理

CGLIB原理:动态生成一个被代理类的子类,子类重写被代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

CGLIB底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

缺点:
a. 对于 final类 和 final 方法,无法进行代理。
b. 类的生成阶段所做的操作会相对耗时,且生成的类的数目较多,会占据大量的元空间的内存(永久代)。

1. AOP术语

  1. target:目标类,需要被代理(增强)的类。
  2. Joinpoint(连接点): 所谓连接点是指那些可能被拦截到的方法。
  3. PointCut 切入点:将被增强的连接点(方法)。
  4. advice 通知/增强,增强代码。例如:after、before
  5. Weaving(织入):是指把增强advice应用到目标对象target来创建新的代理对象proxy的过程
  6. proxy 代理类
  7. Aspect(切面): 是切入点pointcut和通知advice的结合—人为虚构出的概念
    一个线是一个特殊的面。
    一个切入点和一个通知,组成成一个特殊的切面
  8. 切面类:包含通知/增强方法的类

2. 手动 AOP 示例

2.1 利用JDK动态代理,使用前提是目标类必须有实现的接口。

// 接口
public interface IUserService {
    public void addUser();
    public void updateUser();
    public void deleteUser();
}
//目标类
public class UserServiceImpl implements IUserService{

    @Override
    public void addUser() {
        System.out.println("UserServiceImpl add User");
    }

    @Override
    public void updateUser() {
        System.out.println("UserServiceImpl update User");
    }

    @Override
    public void deleteUser() {
        System.out.println("UserServiceImpl delete User");
    }
}
//切面类
public class MyAspect {
    public void before(){
        System.out.println("切面类,前方法");
    }
    public void after(){
        System.out.println("切面类,后方法");
    }
}
//工厂
//生产代理对象的工厂
public class MyBeanFactory {

    public static IUserService createUserService(){
        return new UserServiceImpl();
    }
    
    public static IUserService createProxyUserService(){
        //1.目标类
        final IUserService userService = new UserServiceImpl();
        //2.切面类
        final MyAspect myAspect = new MyAspect();
        //3.代理类:将目标类(切入点)和切面类(通知),织入切面
        /**
         * Proxy.newInstance()的参数
         *  参数1:类加载器,动态代理类,运行时创建,任何类,都需要类加载器,将其加载到内存
         *        一般情况都是:当前类:MyBeanFactory.Class.getClassLoader()  
                                                    目标类也是可以的:userService.getClass().getClassLoader()
                                              当前类和目标类的类加载器都是一样的。
         *  参数2: Class[] interfaces 代理类需要实现的所有接口
         *          方式1:目标类实例.getClass().getInterfaces()  ;注意:只能获得自己接口,不能获得父元素接口
         *          方式2:new Class[]{UserService.class}   
         *          例如:jdbc 驱动  --> DriverManager  获得接口 Connection
         *  参数3:InvocationHandler  处理类,接口,必须进行实现类,一般采用匿名内部
         *                提供 invoke 方法,代理类的每一个方法执行时,都将调用一次invoke
         *          参数31:Object proxy :代理对象
         *          参数32:Method method : 代理对象当前执行的方法的描述对象(反射)
         *                    执行方法名:method.getName()
         *                    执行方法:method.invoke(对象,实际参数)
         *          参数33:Object[] args :方法实际参数
         * 
         */
        IUserService proxyService = (IUserService)Proxy.newProxyInstance(
                MyBeanFactory.class.getClassLoader(), 
                userService.getClass().getInterfaces(), 
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method,
                            Object[] args) throws Throwable {
                        //前执行
                        myAspect.before();
                        
                        //执行目标类的方法
                        Object obj = method.invoke(userService, args);
                        
                        //后执行
                        myAspect.after();
                        
                        return obj;
                    }
                });
        return proxyService;
    }
}

//测试
    @Test
    public void demo1(){
        IUserService userService = MyBeanFactory.createUserService();
        userService.addUser();
        userService.updateUser();
        userService.deleteUser();
    }
    
    @Test
    public void demo2(){
        IUserService userService = MyBeanFactory.createProxyUserService();
        userService.addUser();
        userService.updateUser();
        userService.deleteUser();
    }

2.2 利用 cglib 代理
在目标类没有实现接口的情况下,可以用这种方式,采用字节码增强框架 cglib,在运行时 创建目标类的子类,从而对目标类进行增强。

//生产对象的工厂
public class MyBeanFactory {

    public static UserServiceImpl createProxyUserService(){
        //1.目标类
        final UserServiceImpl userService = new UserServiceImpl();
        //2.切面类
        final MyAspect myAspect = new MyAspect();
         //3. 代理类
          //  使用的是cglib字节码增强的jar包,这个包在spring-core-3.2.0.RELEASE.jar中
          //  底层:创建目标类的子类
        //3.1使用cglib提供的核心类
        Enhancer enhaner = new Enhancer();
        //3.2 确定代理类的父类,就是目标类
        //         增强了
        //  代理类    ======>  目标类
        //         继承了 
        //   子类     ======>   父类
        enhaner.setSuperclass(userService.getClass());
        //3.3 确定回调函数  Callback(cglib的)子类是MethodInterceptor(增强方法)等效JDK的InvocationHandler
        enhaner.setCallback(new MethodInterceptor() {
            /* 3.3 设置回调函数 , MethodInterceptor接口 等效 jdk InvocationHandler接口
             *  intercept() 等效 jdk  invoke()
             *      参数1、参数2、参数3:以invoke一样
             *      参数4:methodProxy 方法的代理
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args,
                    MethodProxy methodProxy) throws Throwable {
                //前面类中前方法
                myAspect.before();
                
                //执行目标类的方法
                Object obj = method.invoke(userService, args);
                // * 执行代理类的父类 ==>执行目标类 (目标类和代理类 父子关系)
                //methodProxy.invokeSuper(proxy, args);与上面method.invoke(userService, args);的效果一样
                
                //切面类中后方法
                myAspect.after();
                
                return obj;

            }
        }); 
        //3.4创建代理
        UserServiceImpl proxyService = (UserServiceImpl) enhaner.create();
        
        return proxyService;
    }
}

3. @AOP 注解开发

3.1 AspectJ
了解@AOP注解前,先了解了一下AspectJ。它是一个基于Java语言的AOP框架,通过JDK5注解技术,允许直接在Bean类中定义切面。
3.1.1 切入点表达式:

  1. execution() 用于描述方法
    语法:execution(修饰符 返回值 包.类.方法名(参数) throws异常)
含义 备注
修饰符 例1: public: 公共方法
例2: *: 任意
一般省略
返回值 例1: void: 返回没有值
例2: *: 任意
例3: String: 返回值字符串
...
不能省略
例1: com. aiahang.crm.*.service
例2: com. aiahang.crm.*.*
例3: com. aiahang.crm.. crm包下面的所有子包(含自己)
~
例1: UserServiceImpl:指定类
例2: *Impl:以Impl结尾
例3: User*:以User开头
例4: *:任意
~
方法名 用法和上一行的类相同 不能省略
参数 例1: (): 无参
例2: (int): 一个整型
例3: (int ,int): 两个
例4: (..):参数任意
~
throws 可省略 一般不写

综合1
execution(* com.soft.crm..service.(..))
综合2 “||”是或的关系,两类表达式都可以
execution(
com.soft.WithCommit.(..)) || execution(* com.soft.Service.(..))" id="myPointCut"

  1. within:匹配包或子包中的方法(了解)
    within(com.itheima.aop..*)
  2. this:匹配实现接口的代理对象中的方法(了解)
    this(com.itheima.aop.user.UserDAO)
  3. target:匹配实现接口的目标对象中的方法(了解)
    target(com.itheima.aop.user.UserDAO)
  4. args:匹配参数格式符合标准的方法(了解)
    args(int,int)
    6.** bean(id)**: 对指定的bean所有的方法(了解)
    bean('userServiceId')

3.1.2 AspectJ 通知类型:
before:前置通知(应用:各种校验)
  在方法执行前执行,如果通知抛出异常,阻止方法运行。

afterReturning:后置通知(应用:常规数据处理)
  方法正常返回后执行,如果方法中抛出异常,通知无法执行,必须在方法执行后才执行,所以可以获得方法的返回值,如有需要可以进一步处理这个返回值。

around:环绕通知(应用:十分强大,可以做任何事情)
  方法执行前后分别执行,可以阻止方法的执行必须手动执行目标方法。

afterThrowing:抛出异常通知(应用:包装异常信息)
  方法抛出异常后执行,如果方法没有抛出异常,无法执行。

after:最终通知(应用:清理现场)
  方法执行完毕后执行,无论方法中是否出现异常。

declareParents:引介通知,用的少

3.2 结合 SpringBoot 的注解切面
依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

示例:

@Aspect //启用切面
@Component
public class ShareDesignAspect {

    //1. 声明公共的切入点表达式,方法名随便,其它切入点可以直接反复引用其方法名,如: 2
    @Pointcut("execution(public * com.test.service.ICommonService.updateFormProcessState(..))")
    public void pointcut() {
    }

    //2. 引用公共切入点
    @AfterReturning("pointcut()")
    public void test1(){
       
    }
    //3. 直接用切入点表单时,效果同2,
    //*方法中不能直接用参数名,如果需要参数,可以用 args()
    @AfterReturning("execution(public * com.test.service.ICommonService.updateFormProcessState(..))")
    public void test1(){
       
    }
    //4. 标有 @ShareData 注解的方法,利用 JoinPoint 获取切入点参数
    @AfterReturning("@annotation(com.fawkes.cybereng.design.common.annotation.ShareData)")
    public void afterEdit(JoinPoint point){
        System.out.print("切入点的所有参数:" + point.getArgs());
    }

    //5. 标有 @ShareData 注解,并且参数名为 formCommitParam 的方法
    @AfterReturning("@annotation(com.fawkes.cybereng.design.common.annotation.ShareData) && args(formCommitParam)")
    public void afterEdit(FormCommitParam formCommitParam){
        System.out.print("切入点参数:" + formCommitParam);
    }

    @AfterThrowing("pointcut()")
    public void test(){
        log.info("更新processState失败");
    }
}

注解的使用,都是基于 AspectJ 的 切入点表达式通知
另外,还有@Order(num):AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行。

总结:

手动的方式,看着很清晰,就是动态代理的思想,在不影响原来代码的情况进行增强,有助于对动态代理和 AOP 的理解,原则上可以给任何类增强。
注解的方式,更简洁,使用方便,但依赖于框架和容器,对于没有在容器中的 bean 则需要引入织入的插件(例子

相关文章

网友评论

      本文标题:AOP 面向切面编程

      本文链接:https://www.haomeiwen.com/subject/yosztltx.html