Spring基础专题——第六章(Aop编程)

前言:去年到现在一直没有很好的时间完成这个spring基础+源码的博客目标,去年一年比较懒吧,所以今年我希望我的知识可以分享给正在奋斗中的互联网开发人员,以及未来想往架构师上走的道友们我们一起进步,从一个互联网职场小白到一个沪漂湿人,一路让我知道分享是一件多么重要的事情,总之不对的地方,多多指出,我们一起徜徉代码的海洋!
 
我这里做每个章节去说的前提,不是一定很标准的套用一些官方名词,目的是为了让大家可以理解更加清楚,如果形容的不恰当,可以留言出来,万分感激

1、Aop编程概念

概念:AOP (Aspect Oriented Programing) ⾯向切⾯编程,等同于Spring动态代理开发,⾯向切⾯编程是以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建切⾯ = 切⼊点 + 额外功能

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

好处:利于原始类的维护

 

类似有其他的:

OOP (Object Oritened Programing) ⾯向对象编程 ,如Java,以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建
 

POP (Producer Oriented Programing) ⾯向过程(⽅法、函数)编程 比如C语言,以过程为基本单位的程序开发,通过过程间的彼此协同,相互调⽤,完成程序的构建

上节末尾讲到Spring组装,实际上组装成了切面!!!

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

 

2、Aop编程开发步骤

实际上aop编程就是动态代理开发的过程!

  • 原始对象
  • 额外功能 (MethodInterceptor)
  • 切⼊点
  • 组装切⾯ (额外功能+切⼊点)

 

3、切面的名词解释

为什么Spring把这个(额外功能+切⼊点) 叫做切面,不叫方便面呢?

要想理解这个命名方式,就要站在几何学的角度来说明下

我们知道面是由线构成的,而线是由点构成的,对于这些点都是具有相同性质的,比如桌子,由一个一个点构成,一个一个点都是木头做的

所以,面  = 点 + 相同的性质
 

所以你类比下切面  = 切点  +  额外功能,这个额外功能,是不是都具备相同的性质,是不是都是日志增强的相同方法,相同性质的点,一个一个在一起就构成了切面了!

这三个Service,理解为一个一个业务,而每个业务的连接点,是对应的方法,如果这些方法都做了事务增强,那么这些方法都具备相同的事务功能的性质,联系在一起,就成了一个切面!!

体会不到也没关系,当我没说,哈哈!重点关注怎么实现aop和底层原理。

 

4、AOP的底层实现原理

4.1、核心问题

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

2. Spring工厂如何加⼯创建代理对象?

通过原始对象的id值,获得的是代理对象

 

4.2、动态代理类的创建

作为Spring来说,创建动态代理有两种方式,一种是通过原生的jdk的方式来帮我们动态创建代理,一种是嫁接了第三方的框架cglib来帮我动态创建代理,我们一个一个来关注

1、JDK动态代理

既然是代理,那么他的关键作用是为原始对象增加额外功能,所以第一步我们肯定要先创建出原始对象,其次我们才能创建动态代理类,因为我们摒弃了Spring框架而使用jdk的方式,所以我们通过new的方式来创建原始对象!!!

我们先创建个原始对象,然后需要用jdk的方法Proxy.newProxyInstance(,,,)来生成代理对象,所以newProxyInstance的返回值,就是给我们创建好的动态代理对象,我们用userServiceProxy接收

来看下里面的几个参数

  • ClassLoader
  • interfaces
  • InvocationHandler

这三个参数的相关含义,我们来回忆下我们代理创建的三要素,和开发4个步骤不要搞混了(原始对象、额外功能、切点、组装切点+切面)

1、原始对象

2、额外功能

3、代理对象要和原始对象实现相同的接口(迷惑别人)

我们刚刚已经有了原始对象UserServiceImpl,那么剩下2,3两个要素,是不是要交给Proxy.newProxyInstance()的三个参数去使用,从而创建代理对象?

而InvocationHandler,就对应着我们的额外功能,讲Spring的动态代理如何写额外功能的时候,我们是不是实现了一个MethodInterceptor这个接口,并实现了他的invoke(MethodInvocation methodInvocation)方法?同理这个jdk的InvocationHandler也是一样,是个接口的方式。

对于InvocationHandler要实现的invoke方法,我们看下几个参数

第一个proxy,第二个method吗,第三个args,后面来说参数含义,这里先混眼熟

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

对比下我们之前的MethodInterceptor的invoke方法,实际上参数的MethodInvocation,是对上面几个参数的高度封装!

public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}

而我们对于额外功能的书写,都是在方法执行之前,是不是都要调用invocation.proceed(); 然后获得原始方法的返回值Object ret = invocation.proceed();我就可以把额外功能书写在这个proceed之前,之后,等等,这个要是不清楚,看下前面的一个章节!

那么我们剩下的只需要解决一个问题,就是对于jdk代理的第三个参数InvocationHandler,是怎么让原始方法运行呢?

参数细化下

proxy:代表的是代理对象,可以忽略,我们用newProxyInstance创建的代理对象,会同时传给这个参数proxy,这个目前没有作用了,不需要关注

method:代表的额外功能增加的原始方法,比如login,register

Object args:代表的是原始方法的参数,如果额外功能加给的是login,就代表的是login(String username , String password)这两个参数,和最开始那个MethodBeforeAdvise的参数一样一样的

我已经找到原始方法,参数也找到了,能不能让这个原始方法运行呢?我们说不可以,因为什么,从反射角度来说,我们要让userService.login(String username, String password)运行,一定要三个

一个是方法执行的对象userService,其次login,然后参数,这样就可以细化到哪个具体的方法执行!!但是目前我们只有两个,一个是原始方法,一个是参数,怎么办呢?看到我们第一步创建

的UserService对象没?所以是不是恍然大明白的感觉没有??哈哈哈

所以我们要这么写

public Object invoke(Object proxy, Method method, Object[] args){
    method.invoke(userService, args);
}

有人该说了,好麻烦啊,还不如Spring的MethodInterceptor直接proceed多香,那肯定是,这Spring给我们做了高度封装,而这个是原始的jdk代理流程,对着图再回忆下

那么还有一个参数,interfaces,刚刚三要素最后一个是  代理对象和原始对象实现相同的接口,为了去对外迷惑,所以这个interfaces指原始对象实现的接口,通过这个参数interfaces,告诉Proxy.newProxyInstance你需要实现什么样的接口,这个你必须要告诉代理创建何种接口类型的代理对吧,不然这么多接口,我怎么知道你要我创建哪个接口的代理类呢?

因为第一步原始对象UserService userService = new UserServiceImpl();我们就可以通过userService.getClass().getInterfaces();来得到这个原始对象所实现的接口!作为interfaces参数,传进去,

所以这样,我在创建代理的时候,获得了原始对象提供的接口interfaces,根据这个接口实现同名的方法,加入额外功能InvocationHandler,从而完美的创建代理对象!

等等,我们是不是还有一个ClassLoader对象没有分析?差点忘记了,来分析下类加载器的作用

1、通过类加载器,把对应类的字节码加载到JVM!

2、通过类加载器创建类的class对象,进而创建这个类的对象!(这个不懂就要去看看JVM了)

比如要创建一个User对象user,首先要类加载器创建User的Class对象,进而通过new User()创建user对象

而类加载器,就是帮我们把Class文件,加载到JVM的一个过程,而类加载得到了Class文件后,帮我们生成Class对象,进而创建类对象。

那如何获得类加载器呢,虚拟机会为每一个Class文件,会自动分配,不需要程序员操心,所以你只要拿到类的Class,就可以拿到类加载器

动态代理类为啥需要类加载器呢,实际上,动态代理是不存在源文件Class的,前面我们用Spring动态代理的时候,没有看到过动态代理类的源文件,但是我们也说过动态代理类实际上是根据动态字

节码技术创建字节码的,直接把字节码写入虚拟机的过程,没有一个加载字节码的过程,甚至Class文件都不会给你生成,实际上Proxy.newProxyInstance()就是用动态字节码的技术来创建代理对象

而newProxyInstance(interfaces,InvocationHandler)就可以帮我们生成了对应的动态代理字节码了,然后把这个字节码直接写入虚拟机,没有加载的过程,也没有生成Class文件的过程,因为你连Class文件都没有,怎么会给你分配虚拟机呢。

于是有了这个动态代理字节码,就可以创建代理类对象了吗?肯定不行啊,虚拟机都没有的话,怎么帮你创建对象呢,所以你要借一个虚拟机过来,借谁的都可以,不用还哈哈,来帮你生成代理对象,比如叫做userServiceProxy!!这就是为什么入参有个ClassLoader,很好解释

参数我们说完了,开始编码

public class JDKProxy {

    public static void main(String[] args) {

        //1. 创建原始对象
        final UserService userService = new UserServiceImpl();

        //2. jdk创建动态代理
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---------------JDKProxy.invoke log--------------");
                //原始方法运行
                Object ret = method.invoke(userService, args);
                return ret;
            }
        };

        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), userService.getClass().getInterfaces(), invocationHandler);
        userServiceProxy.login("chenxin", "123455");
        userServiceProxy.register(new User());
    }
}

然后原始对象

public class UserServiceImpl implements UserService {
    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }

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

运行结果

---------------JDKProxy.invoke log--------------
UserServiceImpl.login
---------------JDKProxy.invoke log--------------
UserServiceImpl.register

我们就在需要运行的方法前进行了代理的增强----额外功能

注意几点,我们在使用内部类的过程中,如果1.8以前访问外部类的局部变量,需要把局部变量定义成final类型,而1.8及之后就不需要定义final。

//1. 创建原始对象
        final UserService userService = new UserServiceImpl();

那么jdk代理我们就告一段落!!!

 

2、Cglib的动态代理

前面我们说了JDK动态代理,那么Cglib和JDK共同特点是,都创建了动态代理的对象实现了额外功能,那么二者的区别是什么呢?

JDK动态代理首先是要有个接口——UserService

其次要有个原始对象实现了接口的方法——UserServiceImpl

再有个代理对象——userServiceProxy

这样保证我们代理类和原始类方法一致,迷惑调用者,再深入点来说为什么代理要这么做,我要做额外功能,是不希望破坏原有功能的,只希望在原始的功能上增加新的功能,做成可插拔式,所以这也是一种编程思想!!

JDK通过Proxy.newProxyInstance来创建动态代理,但是朋友们想想,在现实开发中,极有可能有个原始类,他没有实现任何的接口。

此时我想为这个没有实现任何接口的原始类进行额外功能的创建,进而来创建代理对象呢?

jdk是需要有原始接口的,而不存在原始接口,我们该怎么创建动态代理呢?

 

甭管有没有实现接口,我一定还是为了要做成,增加额外功能的形式,不破坏原有功能的基础上,是不是还是得做一个类,要和原有方法保持一模一样,这样才能出去迷惑对方!!

也就是即便只有一个UserService,他不是一个接口,而是一个业务类,里面有两个方法login和register,我要做额外功能增加,在没接口情况下,我怎么保证另外做一个类,和原始对象里面的方法保

持一模一样呢?想到了什么,对了, 继承!!!!

于是Cglib就是这么玩的,代理类实际上通过继承的方式,来动态创建代理类,进而创建对象。那么原始的方法,是不是都继承下来了!所以一句话,Cglib基于继承关系创建的动态代理!

 

CGlib创建动态代理的原理:⽗⼦继承关系创建代理对象,原始类作为⽗类,代理类作为⼦类, 这样既可以保证二者⽅法⼀致,同时在代理类中提供新的实现(额外功能+原始⽅法)

既然知道了父子继承关系,那就不存在接口了,我们直接定义一个UserService的类

public class UserService {

    public void login(String username, String password){
        System.out.println("UserService.login");
    }

    public void register(User user){
        System.out.println("UserService.register");
    }
}

这个UserService不是接口,而是一个普通的类

不管用什么方式来创建代理,第一步都要创建原始对象

public class CglibTest {

    public static void main(String[] args) {
        UserService userService = new UserService();
    }
}

Cglib给我们提供了一些api,Enhance这个类,叫做增强,最终调用create方法创建代理!

对比下Proxy.newProxyInstance(ClassLoader, interfaces, invocationhandler),需要有类加载器,代理类对应的接口,还有额外功能的书写位置invocationHandler。

那么Cglib同样是创建动态代理,应该和jdk的物质准备,是高度一致的,只不过第二样,不再需要接口了,而是你代理类对应的父类

所以Enhance要setClassLoader,要setSupperClass设置父类,更要setCallback设置额外功能,参数是个MethodInterceptor接口类型,但是这个接口是Cglib包里的,之前我们说的是aopaliance包下的

来编码

所以里面的MethodInteceptor和我们之前讲的invocationHandler高度一致!也是要让原始方法先运行,然后在运行前增加额外功能!

public class CglibTest {

    public static void main(String[] args) {
        UserService userService = new UserService();

        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(CglibTest.class.getClassLoader());
        enhancer.setSuperclass(userService.getClass());
        MethodInterceptor methodInterceptor = new MethodInterceptor() {
            //和invocationHandler高度一致,method代表原始方法,objects代表参数,
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("----------------CglibTest.intercept log-----------");
                Object ret = method.invoke(userService, objects);
                return ret;
            }
        };
        enhancer.setCallback(methodInterceptor);
        UserService userServiceProxy = (UserService) enhancer.create();
        userServiceProxy.login("chenxin", "dddd");
        userServiceProxy.register(new User());
    }
}

结果:

----------------CglibTest.intercept log-----------
UserService.login
----------------CglibTest.intercept log-----------
UserService.register

总结下创建代理的方式:

Jdk代理:通过接口创建代理

Cglib代理:通过继承父类实现的代理

 

5、再看BeanPostProcesser

我们回顾下,对于Spring在创建对象的时候,会通过bean标签的id,创建这个对象,那么Spring工厂可以提供BeanPostProcesser的方式,来对创建的对象进行加工,当时加工也很简单,只对user对象的username属性进行了改变,重新赋值的过程,然后把我

们加工好的对象返回给调用者,那么调用者就会享受到Spring工厂为我们加工的user对象!!实际上动态代理也是通过BeanPostProcesser进行加工的,结合动态代理,看看这个过程变成什么样!

再看一张图,这是加入了动态代理,对于BeanPostProcesser的过程,before环节我们不需要加工,因为对于开发来说我们很少对这些对象进行两次加工,所以我们都会在after的时候做加工,刚刚好这个Object bean是不是就是拿到了需要加工的对象userService,所以我们既然有了原始对象,那么是不是就可以调用Proxy生成jdk动态代理的过程?最终是不是要创建一个代理对象?通过Proxy.newProxyInstance()是不是就可以把我们的原始对象,加工成代理对象?这里就跟着上面,不多说了!

来编码

1、实现BeanPostProcesser,进行加工

2、配置文件中对BeanPostProcesser进行配置

接口:

public interface UserService {

    void register(User user);

    boolean login(String name, String password);
}

实现类:

public class UserServiceImpl implements UserService {

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算+dao");
    }

    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

定义一个ProxyBeanPostProcessor,这下面的代码只要看了上面,应该不用再重复了吧,记不得看看上面说的!!

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

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("--------------ProxyBeanPostProcessor.invoke log--------------");

                Object ret = method.invoke(bean, args);
                return ret;
            }
        };
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), invocationHandler);
        return userServiceProxy;
    }
}

配置文件

 <bean id="userService" class="com.chenxin.spring5.aop.UserServiceImpl">

    </bean>
    <bean id="proxyBeanPostProcessor" class="com.chenxin.spring5.postprocessor.ProxyBeanPostProcessor">
        
    </bean>

做个Test

    @Test
    public void test8(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        boolean result = userService.login("chenxin", "233");
        userService.register(new User());
    }

结果可以知道:

--------------ProxyBeanPostProcessor.invoke log--------------
UserServiceImpl.login
--------------ProxyBeanPostProcessor.invoke log--------------
UserServiceImpl.register 业务运算+dao

有发现一个问题吗,我目前切入点没有做,也就意味着所有方法都被增强了!!!注意,我们是模拟Spring创建aop的时候BeanPostProcesser加工过程,核心问题我们要了解到!

 

6、基于注解的AOP编程

不管是不是基于注解,依旧完成的是aop开发,有以下几个开发步骤

1. 原始对象

2. 额外功能

3. 切⼊点

4. 组装切⾯

第一步我们来下

做一个原始对象

public interface UserService {

    void register(User user);

    boolean login(String name, String password);
}
public class UserServiceImpl implements UserService {

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算+dao");
    }

    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

 然后2.3.4步实际上是创建切面!!!

先有额外功能,再有切点,形成切面

我们知道一切皆对象,对于切面,我们是不是可以想象成一个对象?Spring给我们提供了一个注解@Aspect,声明在你创建的类上,这个类就顺理成章成为切面类了

package com.chenxin.spring5.aspect;

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class MyAspect {
    
}

还少点什么,额外功能+切点

记得我们之前说怎么实现额外功能的?是不是要实现MethodInterceptor接口,复写里面的public Object invoke(MethodInvocation methodInvocation)方法,然后调用methodInvocation.proceed()让原始方法运行。后续只要在proceed之前,之后,加入我们额

外的功能,即可?那在这切面类中我怎么提供额外功能呢?

如果你觉得你的方法是额外方法,你只需要加个注解@Arround注解,方法名你可以随便取,但是返回值,必须是Object,方法名我定义成arround,你随便命名都可以,实际上你对比下invoke方法就知道为什么返回值是Object了,invoke方法的返回值,代表原始方法的返回值,今天的arround方法返回值,也代表着原始方法的返回值。

而原来的MethodInvitation参数,那么我arround是不是也应该有个可以让原始方法运行的参数?当然,叫做ProceedingJointPoint,翻译成执行切点,切点是不是我们说的作用在哪个具体的业务方法?

额外功能有了,是不是剩下切入点了

在@Arround中添加即可,@Arround("execution (* login(..))")等同我们配置文件的做法,只是用了新的编程方式取代老的方式!、

@Aspect
public class MyAspect {

    /**
     * 额外功能完成
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("--------------MyAspect.around log-------------");
        Object ret = proceedingJoinPoint.proceed();
        return ret;
    }
}

xml,重点,你要告知Spring基于aop注解的方式编程了,加这个标签

<aop:aspectj-autoproxy />
<bean id="userService" class="com.chenxin.spring5.aspect.UserServiceImpl">

    </bean>
    
    <bean id="arround" class="com.chenxin.spring5.aspect.MyAspect"></bean>

<aop:aspectj-autoproxy />

测试下 

    @Test
    public void test8(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        com.chenxin.spring5.aspect.UserService userService = (com.chenxin.spring5.aspect.UserService) ctx.getBean("userService");
        boolean result = userService.login("chenxin", "233");
        userService.register(new User());
    }
--------------MyAspect.around log-------------
UserServiceImpl.login
UserServiceImpl.register 业务运算+dao

注意细节:

1、切入点复用

你想想,日后我想为login增加其他的额外功能,比如事务tx

@Aspect
public class MyAspect {

    /**
     * 额外功能完成
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("--------------MyAspect.around log-------------");
        Object ret = proceedingJoinPoint.proceed();
        return ret;
    }

    @Around("execution(* login(..))")
    public Object aroundTx(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("--------------MyAspect.around Tx-------------");
        Object ret = proceedingJoinPoint.proceed();
        return ret;
    }
}

结果:

--------------MyAspect.around log-------------
--------------MyAspect.around Tx-------------
UserServiceImpl.login
UserServiceImpl.register 业务运算+dao

发现没切入点表达式,出现了冗余,其次,如果我不想在login上加这些额外功能了,想在register上加,那么@Around("execution(* login(..))")要改成register....

后续维护,不好修改

所以我们要切入点复用:切入点的配置提取到独立的函数去

这个函数有个要求,必须是public void,而且没有方法内部逻辑,方法名你随便

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

其他的地方这么改下就可以了

@Aspect
public class MyAspect {

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

    }

    /**
     * 额外功能完成
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "myPointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("--------------MyAspect.around log-------------");
        Object ret = proceedingJoinPoint.proceed();
        return ret;
    }

    @Around(value = "myPointCut()")
    public Object aroundTx(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("--------------MyAspect.around Tx-------------");
        Object ret = proceedingJoinPoint.proceed();
        return ret;
    }
}

这样是不是灵活很多,你需要多少个切点,你自己定义函数好了,然后作用在不同的额外方法上面,整体形成一个切面!!后面只需要改一个切入点就可以了

 

2、动态代理的创建⽅式

AOP底层实现 2种代理创建⽅式

  • JDK 通过实现接⼝ 做新的实现类⽅式 创建代理对象
  • Cglib通过继承⽗类 做新的⼦类 创建代理对象

默认情况 AOP编程 底层应⽤JDK动态代理创建⽅式 如果切换Cglib有两种方式

1. 对于基于注解AOP开发

<aop:aspectj-autoproxy proxy-target-class="true" /> false表示采用jdk的方式,true表示用cglib的方式创建动态代理

上面说的只有这个

<aop:aspectj-autoproxy />的时候,只开启了注解的aop开发,默认使用的是false,即JDK代理

2. 对于传统的AOP开发 加上proxy-target-class="true",和1、的效果一样,切换代理模式,false表示采用jdk的方式,true表示用cglib的方式创建动态代理

<aop:config proxy-target-class="true">
   <aop:pointcut id="pc" expression="@annotation(com.chenxin.spring5.aop.log.Log)"/>-->
   <aop:advisor advice-ref="around" pointcut-ref="pc"/>-->
</aop:config>

测试下吧

true的情况

false的情况

 

7、AOP开发中遇到的坑

上面我们实现了login方法的切面,我们修改成UserServiceImpl里的所有方法都做日志和事务的额外功能!

    @Pointcut("execution(* *..UserServiceImpl.*(..))")
    public void myPointCut(){

    }

其他不变,结果很nice!

--------------MyAspect.around log-------------
--------------MyAspect.around Tx-------------
UserServiceImpl.login
--------------MyAspect.around log-------------
--------------MyAspect.around Tx-------------
UserServiceImpl.register 业务运算+dao

于是我干了这么一个事情,我要在register里调用login方法

public class UserServiceImpl implements UserService {

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算+dao");
        this.login("chenxin","211111");
    }

    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

这么做有什么意义呢,不然,其实在实战过程中,在极其复杂的业务下,极有可能同一个Service类中,不同的业务方法相互调用的场景!!!!

如果后续我只调用register,不调用login,而login是在register内部调用的,结果会是都加上额外功能吗?

    @Test
    public void test8(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        com.chenxin.spring5.aspect.UserService userService = (com.chenxin.spring5.aspect.UserService) ctx.getBean("userService");
        userService.register(new User());
    }

结果出乎意料,只有register加上了额外功能

--------------MyAspect.around log-------------
--------------MyAspect.around Tx-------------
UserServiceImpl.register 业务运算+dao
UserServiceImpl.login

为什么会这样呢?其实很简单,我们知道this指向的是当前对象,但是Spring给我们的对象是什么,是代理对象,并不是原始对象,而你用this.login()调用的是userServiceImpl这个原始对象的login方法

但是你用aop代理生成的对象,实际上是代理对象,所以这个过程不赖Spring,因为你调错了,那么我们怎么想让login也加上呢?

你只要可以获取代理对象就可以,最简单的方式,在register内加上ApplicationContext ctx = new ......,然后你getBean,再去调用login,但是你想想,你是在Service里

之前我们说过,ApplicationContext是重量级工厂,并且一个应用只能创建一个这个工厂,所以在Service中再书写一边创建工厂的代码,实在不客观

那么Spring为了解决这个问题,给我们暴露了一个接口叫做ApplicationContextAware,称为工厂意识,可以意识到工厂,识别到工厂,强词夺理好了我也解释不来

然后你要实现一个方法叫做setApplicationContext,把Spring创建好的工厂,传递给你当前类的一个工厂对象中,代码我实现下

public class UserServiceImpl implements UserService, ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register 业务运算+dao");
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.login("chenxin","211111");
    }

    @Override
    public boolean login(String username, String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

这样是不是就不需要创建工厂,就可以实现在register中调用login,也会使得额外功能起作用了?

 

8、AOP阶段总结

贴张图,好好看,我不废话了!!

好了,本节就到这个,至此,Spring的基础部分我差不多讲了,事务的东西,以及一些关于注解开发,我们后面中级课程见,下一节,和持久层整合!!敬请期待

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风清扬逍遥子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值