Spring10:AOP编程+AOP底层实现

1.AOP编程

1.1. AOP概念

1.3个概念:

	1.AOP (Aspect Oriented Programing):面向切面编程 = Spring动态代理开发。
		以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建。
		切面 = 切入点 + 额外功能。
	
	2.OOP (Object Oritened Programing):面向对象编程 Java。
		以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建。
	
	2.POP (Producer Oriented Programing):面向过程(方法、函数)编程 C 。
		以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建。

2.AOP的概念

本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。

好处:利于原始类呃维护。

注意:AOP编程不可能取代OOP,是OOP编程的有益补充。

1.2.AOP编程的开发步骤

1.原始对象
2.额外功能:实现MethodInterceptor接口
3.定义切入点:要添加额外功能之处。
4.组装切面:额外功能+切入点。

1.3.切面的名词解释

1.为什么要将切入点+额外功能的组合成为切面呢?

几何学:

  • 面是由线构成,线是由点构成。所以也可以说面是由点构成的,而且这些点必须是性质相同的点。

切面也是由相同的点构成的:

  • 即切入点,这些切入点具有相同性质,这些相同的性质就是额外功能。所以切入点+额外功能=切面。

比如有3个Service中,都有一个方法需要加上事务的功能,那么每个Servcie中的这个方法加上事务功能,就构成了一个点。三个Service中的点,就构成了一个切面,切在了整个Service层中。

在这里插入图片描述

2.AOP底层实现

即动态代理的底层实现原理。

2.1.核心问题

1.AOP如何创建动态代理类(动态字节码技术)

2.Spring工厂如何加工创建代理对象:通过原始对象的id值,获得的是代理对象。

2.2.JDK动态代理

Spring在创建动态代理对象的过程中,有两种创建方式:

JDK的动态代理
第三方Cglib框架创建动态代理。

1.类加载器的作用:在对象的创建过程中尤为重要

  • 1.通过类加载器将对象的字节码文件加载到JVM中。
  • 2.通过类加载器创建类的Class对象,进而创建这个类的对象:
    User.class --> User类的Class对象 —> new User() —> user
  • 加载字节码,创建Class字节码对象。

2.如何获得类加载器。

虚拟机会为每一个类的.class文件,自动分配与之对应的ClassLoader。

3.类加载器在类加载器中的作用。

  • 在动态代理中要先获得动态代理类,再创建代理对象。但是我们根本没有写.java源文件,所以也就没有编译后的.class文件。那么动态代理是怎么获得这个动态代理类对应的.class文件,进而创建对象的呢?

  • 动态代理是通过动态字节码技术来创建字节码的,没有.java源文件和.class文件,是动态字节码技术创建字节码的,直接将字节码写在了虚拟机中。所以不是类加载器加载.class文件到虚拟机中,没有加载.class字节码这一过程。

  • 那动态字节码技术又是怎么实现的呢?对应的API就是(Proxy类的API):生成了动态代理的字节码直接写到了虚拟机中,进而获得动态代理类的字节码(Class)对象,进而再调用构造。

    //1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性
    Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
    
    //2.得到参数是InvocationHandle的构造器
    Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
    
    //3. 根据构造器得到代理对象:有参构造
    Object proxy = constructor.newInstance(new InvocationHandler() {
          
          
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          
          
            //4.增强该类的所有方法
            System.out.println("打印日志...");
            Object result = method.invoke(target, args);
            System.out.println("结束打印...");
            return result;
        }
    });
    return proxy;
    
    //或者直接:返回指定接口的代理类的实例
    newProxyInstance(ClassLoader loader,<?>[] interfaces, InvocationHandler h) 
    
  • 注意Proxy类的API:生成了动态代理的字节码直接写到了虚拟机中;之后要获得动态代理类的字节码(Class)对象时就要类加载器的帮助了。但是这里就出现了麻烦的地方,因为以前创建对象时,加载.class文件到JVM中,是JVM自动为每个.class文件分配一个类加载器,可是次数没有加载.class字节码这一步,也就JVM没有为我们自动分配类加载器,所以类加载就要我们指定了。

  • 我们需要类加载器帮我们创建动态代理类的Class对象,可是动态代理类没有对应的.class文件,虚拟机也就不会为其分配类加载器。但是我又需要,那么借用一个ClassLoader。所以第一个参数,就是我们要借用一个ClassLoader。这个ClassLoader的目的就是完成代理类的Class对象创建。

    //或者直接:返回指定接口的代理类的实例
    newProxyInstance(ClassLoader loader,<?>[] interfaces, InvocationHandler h) 
    
  • 所以ClassLoader参数的作用:就是帮我们创建动态字节码技术生成的字节码(动态代理类)的Class对象,进而创建代理对象。

  • 借谁的,都可以,只要这个类有.class文件。一般用的都是目标类的类加载器。

  • JDK8之前,内部类访问外部变量,外部变量要定义为final。

2.3.CGlib动态代理

也是Spring底层默认支持的一种动态代理方式,也是动态代理。

1.JDK的动态代理中,要求原对象和代理对象必须实现相同的接口。

2.原因:

  • 这样目标对象和代理对象都有接口中的方法,而且可以合接口形成多态,向调用者屏蔽具体的实现细节,迷惑调用者(在调用者看来还是调用目标类中的原方法)。
  • 而且我可以在代理类中提供对应方法新的实现:加入新的功能,同时调用对应原始方法。

3.如果有一个原始类,没有实现任何接口,我想为它创建代理对象该怎么做呢?

  • CGlib创建的代理类,继承原始类。那么代理类就继承了所有原始类的方法,就可以对这些方法进行代理扩展了,并且用过super.method调用原始方法。

4.CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样即可以保证2者方法一致,同时在代理类中提供新的实现。(额外功能+原始方法)

5.代码:

  • 通过CGlib创建动态代理:Spring默认引入了CGlib的jar包
  • 核心类:Enhancer.create() —> 创建代理对象
  • 核心三要素:父类,类加载,增强方法
         /*
         2 通过cglib方式创建动态代理对象
           Proxy.newProxyInstance(classloader,interface,invocationhandler)
      
           Enhancer.setClassLoader()
           Enhancer.setSuperClass()
           Enhancer.setCallback();  ---> MethodInterceptor(cglib包的)
           Enhancer.create() ---> 代理
        */
        //目标类没有实现接口
      	public class UserService {
          
          
      
      	    public void login(){
          
          
      	        System.out.println("login....");
      	    }
      	
      	    public void register(){
          
          
      	        System.out.println("register...");
      	    }
      	}
    
      public class TestCGlib {
          
          
          public static void main(String[] args) {
          
          
              UserService userService = new UserService();
              /**
               * 通过CGlib创建动态代理:Spring默认引入了CGlib的jar包
               * 核心类:Enhancer.create() ---> 创建代理对象
               */
              Enhancer enhancer = new Enhancer();
              enhancer.setSuperclass(UserService.class);
              enhancer.setClassLoader(UserService.class.getClassLoader());
              enhancer.setCallback(new MethodInterceptor() {
          
          
                  @Override
                  public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
          
          
                      System.out.println("前-----CGlib增强....");
                      Object result = method.invoke(userService, args);
                      System.out.println("后-----CGlib增强....");
                      return result;
                  }
              });
              UserService userServiceProxy = (UserService)enhancer.create();
              userServiceProxy.login();
              userServiceProxy.register();
          }
      }
    

2.4.小结

1. JDK动态代理   Proxy.newProxyInstance()  通过接口创建代理的实现类 
2. Cglib动态代理 Enhancer                  通过继承父类创建的代理类 

3.再看BeanPostProcessor

Spring创建的每一个bean对象,都会调用BeanPostProcessor实现类中重写的方法:postProcessAfterInitializationpostProcessBeforeInitialization这两个方法

3.1回顾BeanPostProcessor

注意1:无论我们有没有实现BeanPostProcessor接口,重写postProcessAfterInitializationpostProcessBeforeInitialization这两个方法。Spring工厂再创建完一个bean对象后都会调用这两个方法。如果我们没有重写这个两个方法,也不用担心会调用到空方法。因为这两个方法在接口中是default修饰,有默认的实现:即不作任何加工,返回原对象。

在这里插入图片描述

注意2:BeanPostProcessor的两个方法:postProcessAfterInitializationpostProcessBeforeInitialization的含义:

  • 1.postProcessBeforeInitialization:初始化操作之前的后置处理
  • 2.postProcessAfterInitialization:初始化操作之后的后置处理
  • 3.参数含义:
    参数1Object bean是Spring工厂创建对象;
    参数2String beanName是bean的id值。

注意3:实际开发中初始化操作很少做,一般都是靠后置Bean来完成对象的再加工。既然初始化很少做,所以postProcessBeforeInitialization一般也不做加工,直接返回源对象即可。具体的加工逻辑在postProcessAfterInitialization中实现。

注意4:无论做不做加工postProcessAfterInitializationpostProcessBeforeInitialization这两个方法都要返回原对象。那么有非常多的Bean会调用这两个方法,很可能会报强制类型转换的错误。所以每个类如果要进行后置处理的话,可以其单独建一个bean后置处理类,或者进行 bean instanceof Object判断。

3.2Spring动态代理是结合BeanPostProcessor完成动态代理创建

1.为什么通过原始对象的id值,直接获得代理对象?

就是因为BeanPostProcessor的加工,得到代理对象返回给调用者。

2.具体步骤图示:

在这里插入图片描述

3.3模拟Spring动态代理的编码演示

1.得到原始对象

public interface UserService {
    
    
    void login(User user);

    void register(String user, String password);
}

public class UserServiceImpl implements UserService {
    
    
    @Override
    public void login(User user) {
    
    
        System.out.println("UserServiceImpl----login");
    }

    @Override
    public void register(String user, String password) {
    
    
        System.out.println("UserServiceImpl----register");
    }
}

<bean id="userService" class="com.txl.factory.UserServiceImpl"/>

2.实现BeanPostProcessor进行加工

public class ProxyBeanPostProcessor implements BeanPostProcessor {
    
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    
    

        Object beanProxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {
    
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
                System.out.println("代理功能---log---Before----");
                Object result = method.invoke(bean, args);
                System.out.println("代理功能---log---After----");
                return result;
            }
        });

        return beanProxy;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    
    
        return bean;
    }
}

<bean id="proxyBeanPostProcessor" class="com.txl.factory.ProxyBeanPostProcessor"/>

3.配置文件中对BeanPostProcessor进行配置

因为pring工厂在创建完一个bean对象后都会调用这两个方法,而且接口也有默认的实现,所以不用整合,直接配一下实现类的Bean即可。

而如果是通过MethodInterceptor来实现额外功能的话,要为这个其指定切入点。即要整合。

<bean id="proxyBeanPostProcessor" class="com.txl.factory.ProxyBeanPostProcessor"/>

原始对象创建好后,就会调用postProcessAfterInitializationpostProcessBeforeInitialization这两个方法,将加工结果(代理对象)返回给我们。所以我们根据原始对象的id获取原始对象时,得到的是代理对象。

4.测试代码:通过原始对象id获得代理对象。

public class Test {
    
    
    public static void main(String[] args) {
    
    
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext2.xml");
        UserService userService = (UserService)applicationContext.getBean("userService");
        userService.login(new User());
        userService.register("txl", "123456");
    }
}

//结果:
代理功能---log---Before----
UserServiceImpl----login
代理功能---log---After----
代理功能---log---Before----
UserServiceImpl----register
代理功能---log---After----

4.基于注解的AOP编程

无论是不是基于注解:核心还是AOP编程或者说Spring的动态代理开发。

1.原始对象
2.额外功能:一般实现MethodInterceptor接口
3.切入点
4.组装切面

4.1.回顾一下之前的非注解的开发步骤

  • 1.原始对象

    public class UserServiceImpl implements UserService {
          
          
        @Override
        public void register(User user) {
          
          
            System.out.println("UserServiceImpl.register:--->业务运算+DAO调用");
        }
    
        @Override
        public boolean login(String name, String password) {
          
          
            System.out.println("UserServiceImpl.login:--->业务运算+DAO调用");
            return true;
        }
    }
    
  • 2.注册原始对象bean

    <!--目标类-->
    <bean id="userService" class="com.txl.UserServiceImpl"></bean>
    
  • 3.继承MethodInterceptor接口方法拦截器,实现额外功能的类
    其中methodInvocation作为原始方法,切入点。

    public class Around implements MethodInterceptor {
          
          
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
          
          
    
            if(methodInvocation.getMethod().getName() == "login"){
          
          
                System.out.println("before-login-proxy-log-------");
                boolean flag = (boolean)methodInvocation.proceed();
                System.out.println("after-----------");
                return flag;
            }
            Object obj = methodInvocation.proceed();
            return obj;//注意返回值的匹配:如果返回null只能匹配void类型的原始方法
        }
    }
    
  • 4.注册提供额外功能的类:MethodInterceptor

    <!--提供额外功能的类:MethodInterceptor-->
    <bean id="around" class="com.txl.dynamticproxy.Around" />
    
  • 5.整合提供额外功能类(方法拦截器)和切面(原始方法)

    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <!--将切入点和额外功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
    
  • 6.测试代码

    @org.junit.Test
    public void test2(){
          
          
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
        //Spring规定,Spring工厂通过原始对象id值获得的是代理对象。
        UserService userService = (UserService)context.getBean("userService");
        userService.register(new User());
        userService.login("txl", "123456");
    }
    

4.2.基于注解的AOP编程的开发步骤

1.原始对象
2.额外功能:一般实现MethodInterceptor接口
3.切入点
4.组装切面
  • 1.原始对象

    public class UserServiceImpl implements UserService {
          
          
        @Override
        public void register(User user) {
          
          
            System.out.println("UserServiceImpl.register:--->业务运算+DAO调用");
        }
    
        @Override
        public boolean login(String name, String password) {
          
          
            System.out.println("UserServiceImpl.login:--->业务运算+DAO调用");
            return true;
        }
    }
    
  • 2.注册原始对象bean

    <!--目标类-->
    <bean id="userService" class="com.txl.UserServiceImpl"></bean>
    
  • 3.定义切面类:额外功能+切入点
    加上@Aspect注解,表明这个类是切面类。

    @Aspect
    public class MyAspect {
          
          
    
        /**
         * 1.自己定义一个方法:作为额外功能,但是这个方法要加上@Around()注解;
         * 2.ProceedingJoinPoint joinPoint:代表原始方法;
         * 3.@Around() 注解中定义切入点:
         * @param joinPoint
         * @return
         */
        @Around("execution(* login(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
          
          
            System.out.println("额外功能--------");
            Object result = joinPoint.proceed();
            return result;
        }
    }
    
  • 4.配置文件中配置切面类:

        <!--2.配置切面类-->
        <bean id="around" class="com.txl.aspect.MyAspect"/>
    
  • 5.告诉Spring开启基于注解的AOP编程

        <!--3.告诉Spring开启基于注解的AOP编程-->
        <aop:aspectj-autoproxy/>	    
    
  • 6.核心:切面类

    1.作为切面类:@Aspect
    2.定义了额外功能:@Around
    3.定义了切入点:@Around(“execution(* login(…))”)

    @Aspect
    public class MyAspect {
          
          
    
        /**
         * 自己定义一个方法:作为额外功能,但是这个方法要加上@Around()注解;
         * ProceedingJoinPoint joinPoint:代表原始方法;
         * @Around() 注解中定义切入点:
         * @param joinPoint
         * @return
         */
        @Around("execution(* login(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
          
          
            System.out.println("额外功能--------");
            Object result = joinPoint.proceed();
            return result;
        }
    }
    
    
  • 7.结合配置

        <!--1.原始对象-->
        <bean id="userService" class="com.txl.aspect.UserServiceImpl"/>
    
        <!--2.配置切面类-->
        <bean id="around" class="com.txl.aspect.MyAspect"/>
    
        <!--3.告诉Spring开启基于注解的AOP编程-->
        <aop:aspectj-autoproxy/>
    

4.3.切入点复用

将切入点的配置@Around("execution(* login(..))"),抽取到一个独立的函数上。

1.如果要为login方法加两个额外的功能:日志和事务。那么要分别写两个额外功能函数,而且这两个额外功能都要配置同一个切点login:@Around("execution(* login(..))")

@Aspect
public class MyAspect {
    
    
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:日志--------");
        Object result = joinPoint.proceed();
        return result;
    }

    @Around("execution(* login(..))")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:事务--------");
        Object result = joinPoint.proceed();
        return result;
    }
}

2.发现切入点表达式冗余:进行抽取复用

抽取出来一个独立的方法,配合@Around(value = "myPointcut()")进行复用。

@Aspect
public class MyAspect {
    
    

    @Pointcut("execution(* login(..))")
    public void myPointcut(){
    
    }

    @Around(value = "myPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:日志--------");
        Object result = joinPoint.proceed();
        return result;
    }

    @Around(value = "myPointcut()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:事务--------");
        Object result = joinPoint.proceed();
        return result;
    }
}

4.4.基于注解的动态代理的实现切换

1.默认是JDK的动态代理。
<aop:aspectj-autoproxy proxy-target-class="false"/>

2.切换成CGlib的动态代理实现。
<aop:aspectj-autoproxy proxy-target-class="true"/>

4.5.传统的AOP编程的动态代理的实现切换

1.默认也是JDK的动态代理实现

<aop:config proxy-target-class="false">
    <!--配置切入点-->
    <aop:pointcut id="pc" expression="execution(* com.txl.UserServiceImpl.login(String,String))"/>
    <!--将切入点和额外功能进行整合-->
    <aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>

2.切换成CGlib实现
<aop:config proxy-target-class="true">
    <!--配置切入点-->
    <aop:pointcut id="pc" expression="execution(* com.txl.UserServiceImpl.login(String,String))"/>
    <!--将切入点和额外功能进行整合-->
    <aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>

5.AOP开发中的一个坑

5.1复现bug

1.可能存在这样的情况:同一个类中的方法存在相互调用,比如UserServiceImpl中的login方法调用register方法(没有意义,不推荐,只是举例):

在这里插入图片描述

2.为UserServiceImpl的所有方法都加上日志和事务的额外功能。

execution(* *..UserServiceImpl.*(..)))

@Aspect
public class MyAspect {
    
    

    @Pointcut("execution(* *..UserServiceImpl.*(..)))")
    public void myPointcut(){
    
    }
    
    @Around(value = "myPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:日志--------");
        Object result = joinPoint.proceed();
        return result;
    }

    @Around(value = "myPointcut()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("额外功能:事务--------");
        Object result = joinPoint.proceed();
        return result;
    }
}

3.测试:

预期:得到代理对象,调用login方法时,会加上日志和事务的功能;同时login方法调用register方法时,也会为register方法加上日志和事务功能。

结果:login加上了额外功能,register没有加上额外功能。

在这里插入图片描述
4.坑:在同一个业务类中,进行业务方法间的相互调用,只有最外层的方法,才是加入了额外功能(内部的方法,通过普通的方式调用,都调用的是原始方法)。如果想让内层的方法也调用代理对象的方法,就要AppicationContextAware获得工厂,进而获得代理对象。

5.原因:额外功能都是在原方法的基础上加的,无论怎么样添加额外功能,都会调用原方法。那么是谁调用原方法?原对象,不是代理对象!
在这里插入图片描述
所以在原对象调用login方法的前后,加上了额外功能。但是原对象调用login方法时,login方法内部又调用了register方法时,register方法被包含在了login方法中,额外功能保住了login,所以register就没有额外功能。

在这里插入图片描述

5.2.解决

1.怎么达到预期:

  • 最简单的想法:创建工厂获得代理对象,代替原对象this调用register。

在这里插入图片描述

  • 但是这样显然是不合理的:Spring的工厂是一个重量资源,尽量只创建一次。此时测试类中和Service都创建了工厂。

2.拿到测试类的工厂:实现ApplicationContextAware接口为当前类添加一个工厂属性。

  • 注意:工厂被创建出来后,在项目运行期间不会销毁。而ApplicationContextAware接口中的setApplicationContext方法,会将项目运行的工厂对象赋值给当前的类中的工厂属性。

  • 源码注释:Set the ApplicationContext that this object runs in.

public class UserServiceImpl implements UserService, ApplicationContextAware {
    
    

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        this.applicationContext = applicationContext;
    }
    @Override
    public void login(User user) {
    
    
        System.out.println("UserServiceImpl----login");

        UserService userService = (UserService)applicationContext.getBean("userService");
        userService.register("ttxxll", "123456");
        //this.register("ttxxll", "123456");
    }

    @Override
    public void register(String user, String password) {
    
    
        System.out.println("UserServiceImpl----register");
    }

}

3.测试结果

在这里插入图片描述

6.AOP小结

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/tttxxl/article/details/115465131
今日推荐