万字张文,从零开始,彻底了解 Spring AOP事务。

接上篇,上篇说了关于IOC的具体流程,这篇文章就来谈谈Spring AOP。关于AOP编程,也是从基础开始,然后再深入。文章稍微有点长,建议先点赞收藏一波。能看到最后的小伙伴肯定是真爱,文尾也给大家送一波资料。
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

另外提供免费的学习资料,学习技术内容包含有:Spring,Dubbo,MyBatis,RPC,源码分析,高并发、高性能、分布式,性能优化,微服务 高级架构开发等等。

需要的朋友可以点击:点这个!点这个!,暗号:csdn。

coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

AOP 编程

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

Spring的关键组件之一是AOP框架。尽管Spring IoC容器不依赖于AOP,但AOP是对Spring IoC的补充,以提供功能非常强大的中间件解决方案。

在涉及AOP之前我们先简单了解一下代理模式,因为代理模式是SpringAOP的底层实现。

代理模式

代理模式是23种设计模式之一,它分为动态代理和静态代理,代理模式可以使客户端的访问对象从真实对象变为代理对象。

为什么这么做呢?

代理模式可以屏蔽用户对真实对象的访问,这样可以避免一些安全上的问题。也能够做到不改变真实对象,对真实对象的功能进行扩展(代理对象实现附加操作进行扩展)。真实对象的功能更加纯粹,业务的分工更加明确。

那么如何实现代理模式呢?

首先需要一个抽象主题(接口或者抽象类)创建代理对象和真实对象代理对象和真实对象都实现该抽象主题客户端访问代理对象

用代码了解代理模式

静态代理

引入场景:我喜欢一双鞋,但在中国地区买不到,需要托朋友从国外代购。

这里“我”可以理解为客户端、朋友是代理对象、出售鞋的商店为真实对象、抽象主题为卖这双鞋。

// 我
public class Me {
}
// 抽象主题,卖鞋(接口)
public interface Subject {
    public void sellShoes();
}
// 商店
public class Store implements Subject{
    public void sellShoes() {
        System.out.println("鞋子售价为90刀");
    }
}
// 朋友
public class Friend implements Subject{

    // 朋友拿到商店对象,对应朋友去商店这一场景(代理对象拿到真实对象)。
    private Store store;

    public void setStore(Store store) {
        this.store = store;
    }

    // 代理对象附加操作
    public void returnHome(){
        System.out.println("朋友回到国内,来到我家");
    }
    // 代理对象附加操作
    public void giveMe(){
        System.out.println("我付给了朋友一百刀");
    }
    public void sellShoes() {
        // 朋友在商店里买下了这双鞋子(代理对象调用真实对象的方法)
        store.sellShoes();
        // 朋友回国
        returnHome();
        // 朋友把这双鞋交给我,我付给它相应的费用(含关税)
        giveMe();
    }
}

可以看到,朋友和商店都实现了Subject这个接口。原本我想买到这双鞋应该直接访问商店对象。但因为没办法访问到该对象,我只能通过访问“朋友”对象来实现我拿到这双鞋的需求。

访问“朋友”对象

// 我
public class Me {
    public static void main(String[] args) {
        // 创建真实对象
        Store store = new Store();
        // 创建代理对象
        Friend friend = new Friend();
        // 将真实对象传给代理对象
        friend.setStore(store);
        //调用代理方法
        friend.sellShoes();
    }
}

输出结果
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

到这里简单的代理操作就实现了,我们通过访问“朋友”对象,确实解决了 原本需要去访问“商店对象”才能拿到鞋的困扰。对应到代理模式中就是,我们绕过了真实对象,通过访问代理对象实现了调用真实对象功能的操作。且代理对象的两个附加操作也实现了对真实对象功能的扩展!

可能栗子举的不太恰当,大家不要太去深究,明白这一代理操作的具体实现和思想才是主要。

代理模式有没有弊端?

静态代理模式中,每有一个真实对象 就会有一个代理对象,如果真实对象十分多的话…😥

动态代理

动态代理可以根据需要,通过反射机制在程序运行时,动态的为目标对象生成代理对象。

动态代理主要分为两大类,一种是基于接口的(JDK),一种是基于类的(CGLIB)

jdk动态代理:

了解jdk动态代理之前我们需要了解两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler接口。

InvocationHandler: 该接口仅定义了一个方法

public object invoke(Object proxy,Method method,Object[] args)

第一个参数为调用该方法的代理实例
第二个参数为目标对象的方法
第三个参数为目标对象方法的参数

当我们使用Proxy的静态方法生成动态代理实例后,使用该实例调用接口中的任意方法,都会将调用的方法替换为 invoke方法。

Proxy:该类就是为我们生成动态代理的 类

Proxy提供了很多方法,我们最常用的是newProxyInstance方法

static Object newProxyInstanc(ClassLoader loader,Class[] interface,InvocationHandler h) :

该静态方法会返回一个Object

返回的Object就可以被当做代理类使用
三个参数(ClassLoader loader,Class[] interface,InvocationHandler h)

    loader:一个类加载器对象,我们通过反射来获取目标对象(真实)的类加载器Class[] interface: 接口对象数组,也是通过反射获取的,生成的代理对象会实现这些接口,并可以调用接口中声明的所有方法。h: InvocationHandler的对象实例,如果我们用来的生成代理类 的
    类(Friend)实现了这个接口(InvocationHandler),可以直接传入这个类本身(this)。
    我们通过newProxyInstance就可以得到真实对象所需要的代理对象

用代码进行简单的演示

    使用静态代理中的Subject公共主题和Store真实对象,不进行任何改动创建一个实现了InvocationHandler接口的类(Friend),它必须实现Invoke方法,在Invoke方法中写附加操作
// 首先实现 InvocationHandler接口
public class Friend implements InvocationHandler {
    // 被代理的接口对象
    private Object target;

    public Friend(Object target) {
        this.target = target;
    }

    // 写一个获取代理对象实例的方法
    public Object getProxy(){
        // Proxy中的newProxyInstance方法会创建一个动态的代理类
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
    // InvocationHandler接口中的invoke方法:
    // 该方法在使用getProxy方法 生成代理类并调用接口中的方法时会被自动调用
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 附加操作
        returnHome();
        // method的invoke方法,通过反射获取到目标对象中的方法。
        // 由于我们没有将目标对象写死,所有我们传入动态的target。
        Object object = method.invoke(target,args);
        // 附加操作
        giveMe();
        return object;
    }
    //附加操作
    public void returnHome(){
        System.out.println("朋友回国后来到我家");
    }
    // 同上
    public void giveMe(){

        System.out.println("我付给了朋友指定的钱");
    }
}
    在Me类中进行动态代理的调用测试
public class Me {
    public static void main(String[] args) {
        // 创建真实对象
        Store store = new Store();
        // 创建 InvocationHandler对象的实例,并传入目标对象(真实对象)
        Friend friend = new Friend(store);
        // 通过InvocationHandler的实例(friend)调用getProxy方法
        // 该方法会返回一个代理对象的实例,我们只需要将我们写好的Object类型转换为需要的接口类型即可
        Subject proxy = (Subject) friend.getProxy();
        // invoke方法会在我们调用接口中的方法时,将该方法替换为它。
        // invoke方法会通过method.invoke拿到目标对象中的方法
        // 也就是Store中的方法,从而实现代理的操作。
        proxy.sellShoes();
    }
}

运行结果:

 D:MORENANZHUANGDIZHIjdk1.8binjava.exe "-javaagent:D:MORENANZHUANGDIZHIIntelliJ IDEA 2020.1.2libidea_rt.jar=60769:D:MORENANZHUANGDIZHIIntelliJ IDEA 2020.1.2bin" -Dfile.encoding=UTF-8 -classpath........repositoryorghamcresthamcrest-core1.3hamcrest-core-1.3.jar com.molu.proxy.Me
朋友回国后来到我家
售价为90刀
我付给了朋友一百刀

进程已结束,退出代码0
    为了凸显动态代理的作用,我们再编写一个代购的栗子

引入场景: 我想要一台Mac,但是国内…所以又托朋友…

写一个公共主题(接口)写一个被代理的对象在Me类中进行动态代理对象生成的测试

// 公共主题
public interface Mac {
    public void sellMac();
}
// 被代理的对象
package com.molu.proxy;

public class MacStore implements Mac{
    public void sellMac() {
        System.out.println("Mac售价为1899刀");
    }
}

公共主题(接口)和真实对象写好后我们在Me类中动态的生成代理对象(没有对动态生成代理实例的Friend类进行任何修改)

Me类

public class Me {
    public static void main(String[] args) {
        // 创建真实对象
        Store store = new Store();
        // 创建 InvocationHandler对象的实例,并传入目标对象(真实对象)
        Friend friend = new Friend(store);
        // 通过InvocationHandler的实例调用getProxy方法
        //该方法会返回一个代理对象的实例,我们只需要指定该实例需要实现的接口即可(强转)
        Subject proxy = (Subject) friend.getProxy();
        // 通过这个动态生成代理实例来调用真实对象中的sellShoes()方法
        // proxy.sellShoes();


        // 生成第二个栗子的动态代理类,步骤同上一模一样
        MacStore macStore = new MacStore();
        Friend friendMac = new Friend(macStore);
        Mac proxyMac = (Mac) friendMac.getProxy();
        proxyMac.sellMac();
    }
}

运行结果
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

没有任何问题,又成功的生成了macStore的代理对象。这样我们就避免了反复写代理类的问题。

jdk动态代理原理剖析

我们主要分析Friend类中的具体实现

    首先来看一下我们手动写的getProxy方法,它主要使用了Proxy类中的newProxyInstance方法。

这个方法返回一个Object对象,这个Object对象有三个参数,这三个参数具体是什么,上文已经说过了。

返回的这个对象通过Proxy的静态方法生成,生成后就可以被当作一个代理对象来使用。

public Object getProxy(){ 
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this); 
} 
    InvocationHandler中的invoke方法 这个方法

有三个参数,分别是代理对象的实例(com.sun.proxy.$Proxy0),目标对象的方法,方法的参数。

我们如果通过getProxy来生成代理实例,使用该实例调用接口中的方法——就会执行invoke方法。

invoke通过反射拿到真实对象中的方法。真正执行的也就是这个通过反射拿到的方法。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object object = method.invoke(target,args);
    return object;
}
    Me类

1.首先我们创建目标对象(真实对象)的实例和InvocationHandler的实例(以下简称为 处理程序的实例 = =生成代理类的处理程序实例 = = InvocationHandler对象的实例),将目标对象传入处理程序的实例中(也就是传入了 friend== this 中)
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

打上断点后,确实看到Friend的实例中的target变成了MacStore,之后invoke方法会通过反射(method.invoke)拿到MacStore中的方法。

2.使用处理程序的实例调用getPorxy方法创建代理对象实例。该对象创建后类型默认为Object(因为我们在写getProxy方法的时候返回值写的是Object),我们将它强转为需要的接口类型即可。

3.通过生成的代理实例来调用接口中的方法时,处理程序的实例会自动调用invoke方法。

4.invoke()方法中的method.invoke(target,args)已经拿到了目标对象中的方法及参数(我们这没有写参数),所以调用invoke方法就等于是调用了目标对象中的方法。再将增强行为写在method.invoke(target,args)上下,就可以实现一次代理的操作。

MacStore macStore = new MacStore();
Friend friendMac = new Friend(macStore);
Mac proxyMac = (Mac) friendMac.getProxy();
proxyMac.sellMac();

invoke方法自动调用

我们再来聊聊为什么invoke方法会被自动调用的问题

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

invoke() 方法来自 InvocationHandler 接口

我们先来看看它的第一个参数 proxy参数,直接输出它的字节码文件名。

System.out.println(proxy.getClass().getName());
// 输出结果为:
	com.sun.proxy.$Proxy0

这个 Proxy0实际上就是我们的代理类实例,感兴趣的朋友可以去将newProxyInstance返回的object对象的字节码文件名打印出来看一下。

也会是∗∗Proxy0 实际上就是我们的代理类实例,感兴趣的朋友可以去将newProxyInstance返回的object对象的字节码文件名打印出来看一下。

也会是Proxy0实际上就是我们的代理类实例,感兴趣的朋友可以去将newProxyInstance返回的object对象的字节码文件名打印出来看一下。也会是∗∗Proxy0

要明白为什么会自动调用invoke方法,我们需要查看一下$Proxy0对象反编译文件的源码。

在main方法最前面添加该配置,运行后会生成代理类反编译的class文件

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

生成路径在idea工作空间下的comsunProxy
$Proxy0.class文件和源代码不在一个目录下
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

点开代理对象反编译的class文件源码可以看到
public final class $Proxy0 extends Proxy implements Subject {
    // 发现它继承了Proxy类,且实现了Subject接口(在生成该反编译文件时我将Mac相关代码都注了,所以是Subject)
	..........
        // 重写了 Subject 接口的 sellShoes方法
 public final void sellShoes() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    ..........

点进去第一行能获得两个信息

public final class $Proxy0 extends Proxy implements Subject {
    代理类的反编译文件继承了Proxy类

也就是说它的父类是Proxy,那么它就会关联一个 InvocationHandler 方法调用处理器

    实现了我们写的Subject接口

可能这就是为什么Jdk动态代理为什么必须要有接口才能使用。(单继承的局限性,但是可以通过接口来多实现)

再往下看,可以看到 $Proxy0 重写了sellShoes方法,该方法调用了super(Proxy) . h.invoke()方法。

关于 h
我们前面只在Proxy.newProxyInstance有所涉及,也就是我们传入的第三个参数,一个InvocationHandler实例(this)。而 $Proxy0 重写的 sellShoes 方法中的 h 也是从 Proxy 类中取的参数极有可能就是我们传进去的 this 。

接下来要做的就很明显了,我们要看看newProxyInstance方法的源码(大家不要看到这一堆源码就害怕,因为我水平也不高 不会全展开来一个一个说,大家放心阅读下去即可。需要看的两处地方有用注解标出)

   @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        								/* 我们通过 newProxyInstance
        								 传进来的InvocationHandler实例 h */
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        Class<?> cl = getProxyClass0(loader, intfs);
        // (o゚v゚)ノ这里
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            //  (o゚v゚)ノ还有这里
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }

不难看出我们的 $Proxy0 就是该方法创建的,c1 为 $Proxy0 的引用对象

Class<?> cl = getProxyClass0(loader, intfs);
// 需要传入一个类加载器和一个接口数组 传入的接口数组在创建$Proxy0时会被自动实现

再往下看,有这么两行代码

/* final Constructor<?> cons = cl.getConstructor(constructorParams); 这行不管 */
final InvocationHandler ih = h;

第一行我们不细细展开,篇幅有限,我觉得我也没办法在源码上讲的比较能够让人理解,所以我们将目光放到第二行。

我们通过Proxy.newProxyInstance(… , … ,h)传进来的h,被赋值给了InvocationHandler实例。InvocationHandler ih = h,这个h实际上就是Friend实例。而在代理对象的反编译文件中又看到这么几行代码

public final void sellShoes() throws  {
        try {
            // super 是继承的Proxy类
            // h 是我们传进来的InvocationHandler实例(friend),
            // invoke方法就是我们写在Friend类中的invoke方法。
            super.h.invoke(this, m3, (Object[])null);

很明了了,我们通过getProxy生成的代理对象实例 $Proxy0 ,调用sellShoes方法它最终会执行

public final void sellShoes() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { … }

方法。

继而就调用了Friend中的invoke方法。也就实现了invoke方法的自动调用。

所以我们原以为的 通过代理对象实例调用接口中的方法实际上是通过$Proxy0调用了源码中的sellShoes()方法才对

// 使用代理对象的实例调用sellShoes方法
proxy.sellShoes();
// 你以为的
 public void sellShoes();

// 实际上的
public final void sellShoes() throws {
        try { super.h.invoke(this, m3, (Object[])null);
            ..........

再捋一捋

首先我们通过newProxyInstance中的Class<?> cl = getProxyClass0(loader, intfs);方法得到Proxy0(这个Proxy0(这个Proxy0(这个Proxy0会自动的实现我们传入的接口),通过代理实例调用接口中的方法时,实际上就是通过 $Proxy0 调用源码里,重写过的接口方法
重写过的接口方法

public final void sellShoes() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    ..........

super 很容易理解,$Proxy0 继承的父类。也就是Proxy,Proxy中的h,不就是我们通过Proxy.newProxyInstance传进去的this(InvocationHandler接口实例
== friend == this)吗。
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-这个this ?就是实现了InvocationHandler接口的Friend实例啊,最后通过这个实例调用了invoke方法。“ super.h.invoke(this, m3, (Object[])null); ”到这里,为什么invoke方法会被自动调用,不就显得图样了嘛 (‾◡◝)。

讲的比较浅,有很多地方没有讲和草草带过,但并不妨碍我们get到为什么会自动调用invoke方法。

cglib动态代理

在jdk动态代理生成的代理对象实例($Proxy0)的源码中我们看到,jdk动态代理必须要有接口实现才能使用。这就造成了一定的局限性,所以在目标类没有接口实现的情况下我们就会使用cglib动态代理。

cglib动态代理采用的是继承思想,它针对类来实现代理,它会给目标类生成一个对应的子类,并覆盖其方法。

简单点说就是:代理类会继承目标类,并重写目标类中的方法(由于使用了继承,所以要避免使用final来修饰目标类)。

使用cglib动态代理

导入pom依赖

<!--导入cglib依赖-->  
<dependency>
  	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>3.3.0</version>
</dependency>

这里导入pom依赖时需要注意版本问题,可能会有无法加载依赖的错误,根本原因是ASM支持与当前的cglib版本不一致。

可以选择降低版本,来快速解决该问题,使用低版本的cglib,Mavan会自动导入版本符合的ASM支持。
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

cglib实现动态代理首选需要准备一个目标对象和一个生成动态代理的类

这里我们使用MacStore来充当目标对象,唯一的不同是没有再继承一个公共主题接口。

package com.molu.cglib;

public class MacStore {
    public void sellMac(){
        System.out.println("Mac售价为1899刀");
    }
}

编写Friend类,写一个生成代理类的方法,重写拦截器方法

// 继承cglib中的MethodInterceptor接口
public class Friend implements MethodInterceptor
{
    // 创建目标对象的实例
    private Object target;
    // 通过构造器传入目标对象
    public Friend(Object target) {
        this.target = target;
    }
	// 使用该方法来创建代理类
    public Object getProxy(){
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        // 使用Enhancer对象中的方法设置父类(将目标类设置为代理类的父类)
        enhancer.setSuperclass(target.getClass());
        // 这里需要传入一个CallBack对象,因为MethodInterceptor接口继承了CallBack
        // 而我们的Friend又实现了CallBack所以我们直接传入 this。这行代码的意思是:
        // 设置拦截器,回调对象为本身对象。
        enhancer.setCallback(this);
        // 返回Enhancer中的create()方法拿到代理对象实例给调用者
        return enhancer.create();
    }
	// 重写intercept方法
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 假装有增强行为 ˋ(°▽°)`
		System.out.println("增强行为");
        // 使用代理类对方法的代理引用,来调用invoke方法
        Object object = methodProxy.invoke(target,objects);
        return object;
    }
}

在Me类中调用getProxy方法获取动态代理实例

public class Me {
    public static void main(String[] args) {
        MacStore macStore = new MacStore();
        Friend friend = new Friend(macStore);
        MacStore proxy = (MacStore) friend.getProxy();
        proxy.sellMac();
    }
}


测试结果:
D:MORENANZHUANGDIZHIjdk1.8binjava.exe......repositorycglibcglib3.2.12cglib-3.2.12.jar;D:MORENANZHUANGDIZHImavenmaven-repositoryorgow2asmasm7.1asm-7.1.jar com.molu.cglib.Me
    
增强行为
Mac售价为1899刀

Process finished with exit code 0

cglib动态代理到这里就不再展开了,篇幅有限。

引入AOP

在理解了AOP的底层"代理模式"后我们来正式的引入AOP(一个没注意就把这个铺垫写的长了点 (⊙﹏⊙) )

因为中间隔了一千字左右的动态代理涉及,估计大家对于还没有谋面的AOP已经没什么印象了,所以我将上文写的一大段屁话再次引用过来。

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

Spring的关键组件之一是AOP框架。尽管Spring IoC容器不依赖于AOP,但AOP是对Spring IoC的补充,以提供功能非常强大的中间件解决方案。

Spring AOP默认将标准JDK动态代理用于AOP代理,在业务类没有接口的实现时,也可以使用cglib动态代理。

AspectJ

Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

我们就不像其他博客那样 放几张看完毫无头绪,好像懂又好像没懂的图片了,直接进入正题。

常见的术语和概念

在实现AOP操作之前我们先对下面的这些术语和概念有一个比较粗浅的认识

横切关注点:跨越应用程序多个模块的方法或者功能,即是 与我们业务逻辑毫无关系的部分也是我们需要关注的部分。如日志、安全、缓存、事务等等…切面(ASPECT):横切关注点 被模块化 的特殊对象。即 它是一个类。通知(Advice):切面必须要完成的工作 即 它是类中的一个方法目标(Target): 被通知的对象代理(Proxy):向目标对象应用通知之后创建的对象切入点(PointCut):切面通知执行的"地点"的定义连接点(JoinPoint):与切入点匹配的执行点

SpringAOP中, 通过 **Advice(通知) **定义横切逻辑,Spring支持五种类型的Advice

前置通知**[Before advice]**:方法(连接点)前执行的通知,它会不阻止执行流程前进到连接点(除非它引发异常)正常返回(后置)通知**[After returning advice]**:方法(连接点)正常执行完后运行的通知(没有引发异常的情况)环绕通知**[Around advice]**:环绕通知围绕在方法(连接点)执行前后运行。这是最强大的通知类型,能在方法调用前后自定义一些操作。异常返回通知**[After throwing advice]**:方法(连接点)抛出异常时运行的通知最终通知**[Final advice]**:在方法(连接点)执行完成后执行的通知,与后置通知不同的是,它会无视抛出异常的情况,即抛出异常仍然会执行该通知,用人话说就是:“无论如何都会执行的通知”。补充,后置通知可以通过配置得到返回值,而最终通知不行

更多内容可以移步Spring官网

实现AOP

原生API接口实现

使用AOP我们需要导入织入包

<dependency>
	<!--导入织入依赖-->
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

导入依赖后我们写一个简单的业务类

业务接口

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void select();
}

接口实现类

public class UserServiceImpl implements UserService{
    public void add() { System.out.println("增加了一个用户"); }
    public void delete() { System.out.println("删除了一个用户"); }
    public void update() { System.out.println("更新了用户"); }
    public void select() { System.out.println("查询用户"); }
}

写一个前置通知,这个通知类只做一件事情:“在我们调用接口实现类中的方法时 打印当前时间和调用的方法名”我们这边只写一个通知,其他的通知大多雷同,感兴趣的朋友可以自己写着玩玩,了解通知的概念和作用差不多就能简单上手实现了。

// 继承Spring原生的API接口MethodBeforeAdvice
public class Log implements MethodBeforeAdvice {
    // 在MethodBeforeAdvice接口的 before 方法写我们具体的操作
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        Date date = new Date();
        System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date)
                + " 执行了" + method.getName() + "方法");
    }
}

在spring配置文件中注册以上两个Bean

<bean id="userService" class="com.molu.service.UserServiceImpl"/>
<bean id="log" class="com.molu.service.Log"/>

之后我们在applicationContext.xml中引入AOP的命名空间

xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

引入命名空间后我们对AOP进行配置

aop:config

<aop:pointcut id=“pointcut” expression=“execution(* com.molu.service.UserServiceImpl.*(…))”/>

<aop:advisor advice-ref=“log” pointcut-ref=“pointcut”/>
</aop:config>

切入点中的execution表达式很好理解,它用来确定我们的通知会在哪些地方执行。

expression=“execution(* com.molu.service.UserServiceImpl.*(…))”

第一个 为所有的返回类型
com.molu.service.UserServiceImpl. * (…) 表示com.molu.service包下的 UserServiceImpl 类的所有方法(所有的参数)

配置完成后我们调用UserServiceImpl中的任意方法,都会在方法前执行我们的log前置通知

测试类

public class MyTest {
    public static void main(String[] args) {
        // 获取Spring上下文环境对象
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 使用上下文环境对象拿到我们的UserServiceImpl Bean的实例
        // 因为AOP默认使用标准JDK动态代理,所以我们还需要将类型强转为UserService接口
        UserService userService = (UserService) context.getBean("userService");
        // 调用add方法
        userService.add();
    }
}

测试结果
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

可以看到,在我们调用UserServiceImpl中的方法时,前置通知成功的被执行了。

自定义切面实现

切面(ASPECT):横切关注点 被模块化 的特殊对象。即 它是一个类。

我们还可以通过自定义一个类,将该类标记为一个切面,使用该类中的方法来实现通知的功能。

写一个自定义类

public class DiyAspect {
    // 前置通知
    public void Before(){
        Date date = new Date();
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(date)+ "时执行了该通知");
    }
    // 后置通知
    public void After(){
        System.out.println("方法执行完毕");
    }
}

在配置文件中注册Bean,并将该类定义为一个切面,使用该类中的方法来执行通知功能。

<!--注册Bean-->
    <bean id="diyAspect" class="com.molu.diy.DiyAspect"/>
    <!--进行AOP配置 -->
    <aop:config>
        <!--自定义切面-->
        <aop:aspect id="aspect" ref="diyAspect">
            <!--定义切入点-->
            <aop:pointcut id="pointcut" expression="execution(* com.molu.service.UserServiceImpl.*(..))"/>
            <!--前置通知设置为我们写在 diyAspect 中的 Before方法-->
            <aop:before method="Before" pointcut-ref="pointcut"/>
            <!--后置通知设置为我们写在 diyAspect 中的 After方法-->
            <aop:after method="After" pointcut-ref="pointcut"/>
        </aop:aspect>

    </aop:config>

MyTest测试类不进行任何改动,直接运行测试。
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

这种通过自定义切面的方式 相对来说会更加简单一些也更容易理解,但因为我们写的只是普通方法,功能上自然是不如实现接口的方式强大

注解实现

使用注解实现之前,我们需要开启AOP注解的支持 和自动扫描包 方便偷懒

<!--自动扫描包,使该包下的注解能够生效-->
<context:component-scan base-package="com.molu.diy"/>
<!--开启AOP注解支持-->
<aop:aspectj-autoproxy/>

写一个Annotation类,在该类中定义一个方法为前置通知,使用注解进行标记。

@Component // 使用注解注册Bean
@Aspect // 使用注解标记该类为一个切面
public class Annotation {

    @Before("execution(* com.molu.service.UserServiceImpl.*(..))")
    // 标记为前置通知,由于类中没办法引用切入点,所以切入点需要我们手动写。这也是注解来实现AOP的一个不便之处。
    public void before(){
        System.out.println("我是前置通知~~~");
    }
}

Mytest测试类不进行任何改动,直接进行测试
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

到这里AOP的三种常见的实现方式 就介绍的差不多了,三种方式各自有各自的好处。使用方面哪种简单用哪种即可。

AOP并没有我们想象中的那么难,主要的是理解这种面向切面的思想。

使用AOP后我们在业务中插入日志等功能会更加的便捷,且不会对业务类造成太多的影响,对日志等功能进行修改或删除也大多不会对业务类本身造成影响。也就做到了所谓的高内聚低耦合,能够熟练的运用AOP 对写出优质的代码多多少少也会有一些帮助

声明式事务

什么是事务

事务可以简单的理解为,将一组业务当作一个业务来处理。要么都成功要么都失败事务在开发中十分的重要,它涉及数据的完整性和一致性

事务的ACID原则

事务的ACID原则在面试中会被经常问到,分别是,原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )。这四个特性简称为 ACID 特性。

原子性

简单的说就是要么都成功要么都失败

一致性

事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。

因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。

如果数据库系统 运行中发生故障,个别事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于不一致的状态。

隔离性

多个业务可能操作同一个资源
我们需要保证这些业务操作数据时是互相隔离的,不会造成数据的损坏等问题。
确保完整性和一致性

持久性

指事务一旦提交,它对数据库中的数据的改变就应该是永久性的,不能回滚。
之后的其它操作或故障不应该对其执行结果有任何影响

开启事务

Spring支持声明式事务和编程式事务两种事务管理模式,我们一般使用声明式事务。

编程式事务管理: 通过Transaction Template手动管理事务,实际应用中很少使用使用XML配置声明式事务: 推荐使用(代码侵入性最小),实际是通过AOP实现

声明式事务会使用AOP,在指定的切入点中织入事务。

 <!--配置c3p0连接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--注入属性-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&amp;useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
        <property name="user" value="root"/>
        <property name="password" value="手动马赛克"/>
    </bean>
    <!--配置事务管理器-->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource"/>
    </bean>
    <!--配置事务通知-->
    <tx:advice id="interceptor" transaction-manager="dataSourceTransactionManager">
        <tx:attributes>
            <!-- * 表示我们每一个方法都会被织入事务-->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!--配置事务切入-->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.molu.service.*.*(..))"/>
        <aop:advisor advice-ref="interceptor" pointcut-ref="txPointCut"/>
    </aop:config>
    <!--配置完成后我们service中的所有类的所有方法,都会被织入事务-->
</beans>

以上就是如何开启声明式事务的全部操作,到这里我们从IOC到AOP的博客也结束了。非常感谢你能看到这里,希望对你有帮助!

最后

还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书+2020年最新大厂面试题。

需要的朋友可以点击:点这个!点这个!,暗号:csdn。

coding-万字张文,从零开始,彻底了解 Spring AOP事务。-
coding-万字张文,从零开始,彻底了解 Spring AOP事务。-

匿名

发表评论

匿名网友