一篇通俗易懂的长文,带你从零认识IoC和AOP

2,848 阅读26分钟

写在前面

本篇博客共一万七百字左右,从 IOC到 AOP。

算是对自己学习 Spring的一个验收,同时也分享出来供大家查漏补缺。

在我写这篇博客时自己也是第一次接触 Spring,也不敢保证写的东西绝对正确,可能存在谬误和歧义,如发现请务必指出 会及时更正。

写这篇博客前前后后花了一个礼拜多,期间有通过视频来了解 Spring,也有参考其他的博客,对自己无法表述的部分来进行完善,可以说是每一段都有经过反复修改。自己也感觉到,在一次次的修改中 自己也对这些反复敲的概念理解更进了一步。

IoC 思想

IOC(控制反转)是一种依赖倒置原则的代码设计的思路,它主要采用(DI)依赖注入的方式来实现

不使用IoC思想的传统模式

  • 在传统模式中,对象由程序员主动创建,控制权在程序员手中。
  • 程序可以做到正常工作,但仍有一个难以避免的问题。
  • 如果用户需求变更,程序员就要修改对应的代码,代码量不大还好,如果代码量巨大的话 修改一次的成本...
  • 这个问题就是耦合性过高引起的,修改一次需求,或多或少会造成代码的修改,工作量先不说,维护起来也是极其不便的啊。

  • 就如上图中这四个齿轮(对象)一样,互相啮合,如果有一方停止或更换 其他的齿轮也就没办法工作,这自然不是我们希望看到的。

为了解决对象间耦合过高的问题,软件专家Michael Mattson提出了IoC理论,用来实现对象之间的“解耦”。

那么应当如何达到理想的效果呢?

使用IoC思想后的模式

IoC的主要思想是借助一个“第三方”来拆开原本耦合的对象,并将这些对象都与“第三方”建立联系,由第三方来创建、操作 这些对象,进而达到解耦的目的。

因此IoC容器也就成了整个程序的核心,对象之间没有了联系(但都和 IoC容器有联系)。

这里引用一句知乎上看到的话

IoC的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。

什么是控制反转

这里我们引入一个场景, “如果 A对象想调用 B对象”

传统模式中该如何操作 大家都很熟悉了,在A对象中创建一个 B对象实例,就可以满足 A对象调用 B对象的需求。这是我们在 A对象中主动的去创建 B对象实例

而引入 IoC后,A对象如果想调用 B对象,IoC容器会创建一个 B对象注入到 A对象中,这样也可以满足 A对象的调用需求。但是过程由我们的主动创建,变成了 A对象被动的去接收 IoC容器注入的 B对象

A对象依赖 B对象的过程,由程序员的主动创建 B对象供其依赖,变为了被动的接收 IoC容器注入的对象。控制权从程序员手中交到了 IoC容器手中。A对象获得依赖的过程也由主动变为被动,这就是所谓的控制反转

什么是依赖注入(DI)

依赖注入是 IoC思想最主要的实现方式,也就是上文提到的 “ A对象如果想调用 B对象,IoC容器会创建一个 B对象注入到 A对象中,这样就可以满足 A对象对 B对象的依赖需求”。 这个行为就是依赖注入

DI ≠ IOC

IoC的概念更宽广一些,而 DI是 IoC的主要实现方式,但这并不意味着 DI就是 IoC,将二者混为一谈 这是不对的,很容易误导他人。

就比如你想要阅读,最主要的实现方式自然是“用眼睛长时间的去看”,但你不能把这个“眼睛长时间去看”的行为 理解为阅读。(可能例子有点不恰当)

注入方式

setter方法注入

我们需要在类中生成一个set方法和一个空构造(空构造在没有声明有参构造时会隐式声明,无需再进行多余的操作)

public class Hello {
	
    private String name;
	// 一定要生成set方法
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

在 Spring配置文件中注入 Bean(对象),在 Bean中使用 property标签为 name属性赋值

    <bean id="hello" class="com.molu.pojo.Hello">
        <!--setter方法注入使用property标签-->
        <property name="name" value="陌路"/>
    </bean>

写一个简单的测试方法,测试 property标签是否成功赋值。

构造器注入

构造器注入我们需要手动的生成一个有参构造

package com.molu.pojo;

public class Hello {

    private String name;
    // 生成有参构造
    public Hello(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

这个时候再切回 applicationContext.xml中可以看到已经报错了

因为显式的定义了有参构造后,无参构造就不存在了

我们需要将property标签改为 constructor-arg

constructor-arg标签赋值的方式更为多样化。

  • 通过下标赋值

    • <bean id="hello" class="com.molu.pojo.Hello">
          <constructor-arg index="0" value="陌路"/>
      </bean>
      
  • 通过参数类型赋值(不推荐,参数类型容易重合)

    • <bean id="hello" class="com.molu.pojo.Hello">
          <constructor-arg type="java.lang.String" value="陌路"/>
      </bean>
      
  • 通过参数名赋值(推荐)

    • <bean id="hello" class="com.molu.pojo.Hello">
          <constructor-arg name="name" value="陌路"/>
      </bean>
      

      结果都是一样的,这里就不再展示测试结果了

拓展注入

P(Property)命名空间注入
  • 在使用P命名空间之前要在引入它的约束

    • xmlns:p="http://www.springframework.org/schema/p"
      
  • P命名空间的使用必须要有一个空构造。(类似于setter方法注入)但前面也说了,没有声明有参构造时 空构造会隐式声明。

  • P命名空间注入 可以直接在 Bean标签中进行注入

首先创建一个 Me类

package com.molu.pojo;

public class Me {
    
    private String name;

    private int age;

    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }

    public void setAge(int age) { this.age = age; }

在 applicationContext.xml中用 P命名空间进行注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--在Bean标签中,使用P命名空间进行简单的属性注入-->
    <bean id="me" class="com.molu.pojo.Me" p:name="陌路" p:age="18"/>
</beans>

测试

略......

C(constructor-arg)命名标签注入
  • 使用 C命名空间之前也需要引入约束

    • xmlns:c="http://www.springframework.org/schema/c"
      
  • C命名空间不同于 P命名空间,他必须要有一个构造方法(类似构造器注入)

  • P命名空间也可以直接在 Bean标签中进行简单的注入操作

在 Me类中添加构造器

package com.molu.pojo;

public class Me {
    private String name;
    
    private int age;
    // 生成有参构造 供C命名空间调用
    public Me(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public int getAge() { return age; }

    public void setAge(int age) { this.age = age; }
}

在 applicationContext.xml中用 C命名空间进行注入

<!--C命名空间注入,通过构造器注入-->
<bean id="me" class="com.molu.pojo.Me" c:name="陌路" c:age="18" />

测试

关于其他的注入方式这里就不再一一列举了,官网上写的很详细,有能力的朋友可以移步官网,在官网上寻求答案

Spring官网

补充

我们再写一个 HelloTwo类,里面和 Hello类一样,唯一不同是显式的定义了无参构造而不是有参构造

package com.molu.pojo;

public class HelloTwo {
    private String name;
    // 显式的定义了无参构造
    public HelloTwo() {
        // 简单写一个测试输出语句
        System.out.println("HelloTwo的无参构造被调用了");
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

在 Application Context.xml中注册 Bean,不对其进行其他任何操作。

<bean id="helloTwo" class="com.molu.pojo.HelloTwo"></bean>

使用刚刚用过的 Hello类测试方法 原封不动进行测试

public class MyTest {
    @Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Hello hello = (Hello) context.getBean("hello");
        System.out.println(hello.getName());
    }
}

控制台输出了我们在 HelloTwo无参构造中写的输出语句,奇怪的是我们并没有在测试类中写任何关于 HelloTwo的代码。

由此能够得到一些信息:“注册进 applicationContext.xml中的 Bean,无论你调用与否,他都会被初始化”

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();
    }
}

输出结果

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

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

代理模式有没有弊端?

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

动态代理

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

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

jdk动态代理:

了解jdk动态代理之前我们需要了解两个类:java.lang.reflect.Proxyjava.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就可以被当做代理类使用 它的三个参数

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

我们通过 newProxyInstance就可以得到真实对象所需要的代理对象

用代码进行简单的演示

// 首先实现 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();
    }
}

运行结果:

朋友回国后来到我家
售价为90刀
我付给了朋友一百刀

进程已结束,退出代码0

为了凸显动态代理的作用,我们再编写一个代购的栗子

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

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

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

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();
    }
}

运行结果

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

jdk动态代理原理剖析

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

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

    1. 这个方法返回一个 Object对象,这个 Object对象有三个参数,这三个参数具体是什么,上文已经说过了。
    2. 返回的这个对象通过 Proxy的静态方法生成,生成后就可以被当作一个代理对象来使用。
        public Object getProxy(){
            return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
        }
    
  2. InvocationHandler中的invoke()方法

    1. 这个方法有三个参数,分别是代理对象的实例(com.sun.proxy.$Proxy0),目标对象的方法,方法的参数。
    2. 我们如果通过 getProxy来生成代理实例,使用该实例调用接口中的方法——就会执行 invoke方法。
    3. invoke通过反射拿到真实对象中的方法。真正执行的也就是这个通过反射拿到的方法。
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object object = method.invoke(target,args);
            return object;
        }
    
    1. Me类

      1. 首先我们创建目标对象(真实对象)的实例和 InvocationHandler的实例,将目标对象传入处理程序的实例中(也就是传入了 friend == this 中)

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

      1. 使用处理程序的实例调用 getPorxy方法创建代理对象实例。该对象创建后类型默认为 Object(因为我们在写getProxy方法的时候返回值写的是Object),我们将它强转为需要的接口类型即可。
      2. 通过生成的代理实例来调用接口中的方法时,处理程序的实例会自动调用 invoke()方法。
      3. 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

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

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

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

生成在 IDEA工作空间下的com\sun\Proxy$Proxy0.class文件和源代码不在一个目录下

点开代理对象反编译的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 调用源码里重写过的接口方法

重写过的接口方法

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吗。

这个this ?就是实现了InvocationHandler接口的Friend实例啊,最后通过这个实例调用了invoke方法。

"super.h.invoke(this, m3, (Object[])null);"

到这里,为什么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支持。

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();
    }
}


测试结果:
    
增强行为
Mac售价为1899刀

Process finished with exit code 0

引入AOP

在理解了AOP的底层"代理模式"后我们来正式的引入 AOP

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概念在5.1处

实现AOP

原生API接口实现

        <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();
    }
}

测试结果

可以看到,在我们调用 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测试类不进行任何改动,直接运行测试。

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

注解实现

使用注解实现之前,我们需要开启 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测试类不进行任何改动,直接进行测试

到这里 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的这篇博客也要结束了。非常感谢你能看到这里,如果有帮到你的话。


放松一下眼睛

pixiv地址

画师主页