0%

Spring Study

2 Spring IoC
[2.1 Spring IoC 的基本概念](#2.1 Spring IoC 的基本概念)
[2.2 Spring IoC 容器](#2.2 Spring IoC 容器)
[2.2.1 BeanFactory](#2.2.1 BeanFactory)
[2.2.2 ApplicationContext](#2.2.2 ApplicationContext)
[2.3 依赖注入的类型](#2.3 依赖注入的类型)
[2.3.1 使用构造方法注入](#2.3.1 使用构造方法注入)
[2.3.2 使用属性的 setter 方法注入](#2.3.2 使用属性的 setter 方法注入)
[3 Spring Bean](#3 Spring Bean)
[3.1 Bean 的配置](#3.1 Bean 的配置)
[3.2 Bean 的实例化](#3.2 Bean 的实例化)
[3.2.1 构造方法实例化](#3.2.1 构造方法实例化)
[3.2.2 静态工厂实例化](#3.2.2 静态工厂实例化)
[3.2.3 实例工厂实例化](#3.2.3 实例工厂实例化)
[3.3 Bean 的作用域](#3.3 Bean 的作用域)
[3.3.1 singleton作用域](#3.3.1 singleton作用域)
[3.3.2 prototype作用域](#3.3.2 prototype作用域)
[3.4 Bean 的生命周期](#3.4 Bean 的生命周期)
[3.5 Bean 的装配方式](#3.5 Bean 的装配方式)
[3.5.1 基于 XML 配置的装配](#3.5.1 基于 XML 配置的装配)
[3.5.2 基于注解的装配](#3.5.2 基于注解的装配)
[4 Spring AOP](#4 Spring AOP)
[4.1 Spring AOP 的基本概念](#4.1 Spring AOP 的基本概念)
[4.1.1 AOP 的概念](#4.1.1 AOP 的概念)
[4.1.2 AOP 的术语](#4.1.2 AOP 的术语)
[4.2 动态代理](#4.2 动态代理)
[4.2.1 JDK 动态代理](#4.2.1 JDK 动态代理)
[4.2.2 CGLIB](#4.2.2 CGLIB)
[4.3 基于代理类的 AOP 实现](#4.3 基于代理类的 AOP 实现)

文章目录

2 Spring IoC
[2.1 Spring IoC 的基本概念](#2.1 Spring IoC 的基本概念)
[2.2 Spring IoC 容器](#2.2 Spring IoC 容器)
[2.2.1 BeanFactory](#2.2.1 BeanFactory)
[2.2.2 ApplicationContext](#2.2.2 ApplicationContext)
[2.3 依赖注入的类型](#2.3 依赖注入的类型)
[2.3.1 使用构造方法注入](#2.3.1 使用构造方法注入)
[2.3.2 使用属性的 setter 方法注入](#2.3.2 使用属性的 setter 方法注入)
[3 Spring Bean](#3 Spring Bean)
[3.1 Bean 的配置](#3.1 Bean 的配置)
[3.2 Bean 的实例化](#3.2 Bean 的实例化)
[3.2.1 构造方法实例化](#3.2.1 构造方法实例化)
[3.2.2 静态工厂实例化](#3.2.2 静态工厂实例化)
[3.2.3 实例工厂实例化](#3.2.3 实例工厂实例化)
[3.3 Bean 的作用域](#3.3 Bean 的作用域)
[3.3.1 singleton作用域](#3.3.1 singleton作用域)
[3.3.2 prototype作用域](#3.3.2 prototype作用域)
[3.4 Bean 的生命周期](#3.4 Bean 的生命周期)
[3.5 Bean 的装配方式](#3.5 Bean 的装配方式)
[3.5.1 基于 XML 配置的装配](#3.5.1 基于 XML 配置的装配)
[3.5.2 基于注解的装配](#3.5.2 基于注解的装配)
[4 Spring AOP](#4 Spring AOP)
[4.1 Spring AOP 的基本概念](#4.1 Spring AOP 的基本概念)
[4.1.1 AOP 的概念](#4.1.1 AOP 的概念)
[4.1.2 AOP 的术语](#4.1.2 AOP 的术语)
[4.2 动态代理](#4.2 动态代理)
[4.2.1 JDK 动态代理](#4.2.1 JDK 动态代理)
[4.2.2 CGLIB](#4.2.2 CGLIB)
[4.3 基于代理类的 AOP 实现](#4.3 基于代理类的 AOP 实现)

2 Spring IoC

2.1 Spring IoC 的基本概念

  • 控制反转(Inversion of Control,IoC)是一个比较抽象的概念,是Spring框架的核心,用来消除计算机程序的耦合问题。依赖注入(Dependency Injection,DI)是 IoC 的另外一个说法,只是从不同的角度描述相同的概念。下面通过实际生活中的一个例子来解释 IoC 和 DI 。
  • 当人们需要一件东西时,第一反应就是找东西,例如吃面包。在没有面包店和有面包店两种情况下,您会怎么做?在没有面包店时,最直观的做法可能是您按照自己的口味制作面包,也就是一个面包需要主动制作。然而时至今日,各种网点、实体店盛行,已经没有必要自己制作面包。想吃面包了,去网店或实体店把自己的口味告诉店家,一会就可以吃到面包了。注意,您并没有制作面包,而是由店家制作,但是完全符合您的胃口。
    上面只是列举一个非常简单的例子,但包含了控制反转的思想,即把制作面包的主动权交给店家。下面通过面向对象编程思想继续探讨这两个概念。
  • 当某个Java对象(调用者,例如您)需要调用另一个Java对象(被调用者,即被依赖对象,例如面包)时,在传统编程模式下,调用者通常会采用“new被调用者”的代码方式来创建对象(例如您自己制作面包)。这种方式会增加调用者与被调用者之间的耦合性,不利于后期代码的升级与维护。
  • 当Spring框架出现后,对象的实例不再由调用者来创建,而是由Spring容器(例如面包店)来创建。Spring容器会负责控制程序之间的关系(例如面包店负责控制您与面包的关系),而不是由调用者的程序代码直接控制。这样,控制权由调用者转移到Spring 容器,控制权发生了反转,这就是Spring的控制反转。
    从Spring容器角度来看,Spring 容器负责将被依赖对象赋值给调用者的成员变量,相当于为调用者注入它所依赖的实例,这就是Spring的依赖注入。
  • 综上所述,控制反转是一种通过描述(在Spring中可以是XML或注解)并通过第三方去产生或获取特定对象的方式。在Spring 中实现控制反转的是IoC容器,其实现方法是依赖注入。

2.2 Spring IoC 容器

实现控制反转的时 Spring IoC 容器。Spring IoC 容器的设计主要基于BeanFactoryApplicationContext 两个接口。

2.2.1 BeanFactory

BeanFactory由org.springframework. beans.factory.BeanFactory接口定义,它提供了完整的IoC服务支持,是一个管理Bean的工厂,主要负责初始化各种Bean。BeanFactory 接口有多个实现类,其中比较常用的是org.springframework .beans factory.xml.XmlBeanFactory,该类会根据XML配置文件中的定义来装配Bean()。
在创建BeanFactory实例时需要提供XML文件的绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo1;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.FileSystemResource;

public class Test {
public static void main(String[] args) {
// ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
// TestDaoImpl testDaoImpl = (TestDaoImpl) classPathXmlApplicationContext.getBean("testDaoImpl");
// testDaoImpl.sayHello();
BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource("applicationContext.xml的绝对路径"));
TestDaoImpl testDaoImpl = (TestDaoImpl) beanFactory.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}


注意:使用BeanFactory实例加载配置文件在实际开发并不多见,我们只需要了解即可。

2.2.2 ApplicationContext

ApplicationContextBeanFactory 的子接口,也称应用上下文,由org.springframework.context.ApplicationContext接口定义。ApplicationContext接口包含 BeanFactory 的所有功能外,还添加了对国际化、资源访问、事件传播等内容的支持。
创建ApplicationContext接口实例通常有以下三种方法:

通过ClassPathXmlApplicationContext 创建
ClassPathXmlApplicationContext 将从类路径目录(src根目录)中寻找指定的XML配置文

1636249741950

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
TestDaoImpl testDaoImpl = (TestDaoImpl) classPathXmlApplicationContext.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}


2.通过FileSystemXmlApplicationContext创建

FileSystemXmlApplicationContext将从配置文件的绝对路径中寻找XML配置文件,找到并装载完成 ApplicationContext 的实例化工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
package demo1;
import org.springframework.context.support.FileSystemXmlApplicationContext;


public class Test {
public static void main(String[] args) {
FileSystemXmlApplicationContext fileSystemXmlApplicationContext = new FileSystemXmlApplicationContext("ApplicationContext.xml文件的绝对路径");
TestDaoImpl testDaoImpl = (TestDaoImpl) fileSystemXmlApplicationContext.getBean("testDaoImpl");
testDaoImpl.sayHello();
}
}


采用绝对路径的加载方式将导致程序的灵活性变差,一般不推荐使用。因此,通常在 Spring 的 java 应用中采取通过 ClassPathXmlApplicationContext 类来实例化 ApplicationContext 容器的实例化工作将交给Web服务器完成。

3.通过Web服务器实例化ApplicationContext 容器
在Web服务器实例化ApplicationContext 容器时,一般使用基于org.springframework.web.context.ContextLoaderListener 的实现方式(需要将spring-web-5.0.2.RELEASE.jar复制到WEB-INF/lib目录中),此方法只需要在web.xml中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<context-param>
<!-- 加载src目录下的 applicationContext.xml文件-->
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>

<!-- 指定以 ContextLoaderListener 方式启动 Spring 容器 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>

2.3 依赖注入的类型

在 Spring 中实现 IoC 容器的方法是依赖注入,依赖注入的作用是在使用 Spring 框架创建对象时动态地将其所依赖的对象(例如属性值)注入 Bean 组件中。Spring 框架的依赖注入通常有两种实现方法,一种是使用构造方法注入,另一种是使用属性的 setter 方法注入。

2.3.1 使用构造方法注入

Spring 框架可以采用java的反射机制,通过构造方法完成依赖注入。下面开始代码演示:

目的:在service 中使用构造方法依赖注入 TestEr 接口对象。

  1. 创建 dao 包
    TestEr 接口代码如下:

    1
    2
    3
    4
    5
    6
    7
    package demo2.dao;

    public interface TestEr {
    public void sayHello();

    }

  2. TestErbao 实现类的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package demo2.dao;

    public class TestErbao implements TestEr {
    @Override
    public void sayHello() {
    System.out.println("TestEr: say Hello");
    }
    }

  3. 创建 service 包
    TestErService 接口代码如下

    1
    2
    3
    4
    5
    6
    package demo2.service;

    public interface TestErService {
    public void sayHello();
    }

  4. TestErServiceImpl 实现类的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package demo2.service;

    import demo2.dao.TestEr;




    public class TestErServiceImpl implements TestErService{
    private TestEr TestErbao;

    // 构造方法,用于实现依赖注入接口TestEr
    public TestErServiceImpl(TestEr TestErbao){
    super();
    this.TestErbao = TestErbao;
    }

    @Override
    public void sayHello() {

    TestErbao.sayHello();
    System.out.println("TestErService 构造方法注入say:,hello Spring");
    }
    }

  5. 编写配置文件
    demo2包下创建applicationContext.xml文件。在配置文件中首先将TestErbao 类托管给 Spring,让Spring 创建其对象,然后service.TestErServiceImpl 类托管给 Spring,让 Spring 创建其对象,同时给构造方法传递实参。配置文件具体代码如下:

    **注意: 这是xml文件,注释不是 // **

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="TestErbao" class="demo2.dao.TestErbao"/>

    <bean id="testServiceImpl" class="demo2.service.TestErServiceImpl">
    <!-- index = 0 表示 第一个参数为 引用的 testDIDaoImpl-->
    <constructor-arg index="0" ref="TestErbao"/>
    </bean>

    </beans>

  6. 创建 Test 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package demo2;

    import demo2.service.TestErService;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class Test {
    public static void main(String[] args) {
    ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo2/applicationContext.xml");
    TestErService testServiceImpl = (TestErService) classPathXmlApplicationContext.getBean("testServiceImpl");
    testServiceImpl.sayHello();
    }
    }



    输出结果;

1636253087861

2.3.2 使用属性的 setter 方法注入

使用 setter 方法注入是 Spring 框架中最主流的注入方式,它利用 Java Bean 规范所定义的 setter 方法完成注入,灵活且可读性高。对于 setter 方法注入,Spring 框架也是使用了 Java 的反射机制实现的。下面代码讲解如何使用 setter 方法注入:

创建接口实现类 TestErServiceImpl2
TestErServiceImpl2 中使用属性的 setter 方法依赖注入 TestEr 接口对象,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo2.service;

import demo2.dao.TestEr;


public class TestErServiceImpl2 implements TestErService{
private TestEr TestErbao;

// 添加 TestEr 的setter 方法 用于实现依赖注入
public void setTestEr(TestEr TestErbao){

this.TestErbao = TestErbao;
}

@Override
public void sayHello() {

TestErbao.sayHello();
System.out.println("TestErServiceImpl2 setter注入");
}
}

将 TestErServiceImpl2 类托管给 Spring

将 TestErServiceImpl2 类托管给 Spring,让 Spring 创建其对象,同时调用 TestErServiceImpl2 类的 setter 方法完成依赖注入。ApplicationContext.xml配置文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="TestErbao" class="demo2.dao.TestErbao"/>

<bean id="testServiceImpl" class="demo2.service.TestErServiceImpl">
<!-- index = 0 表示 第一个参数为 引用的 testDIDaoImpl-->
<constructor-arg index="0" ref="TestErbao"/>
</bean>
<bean id="testServiceImpl2" class="demo2.service.TestErServiceImpl2">

<property name="TestEr" ref="TestErbao"/>
</bean>

</beans>

在Test类中测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo2;

import demo2.service.TestErService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo2/applicationContext.xml");
TestErService testServiceImpl = (TestErService) classPathXmlApplicationContext.getBean("testServiceImpl2");
testServiceImpl.sayHello();
}
}



输出结果;

1636254214152

3 Spring Bean

Spring 可以看作一个大型工厂,用于生产和管理 Spring 容器中的 Bean。如果要使用这个工厂生产和管理 Bean,需要开发者将 Bean 配置在 Spring 的配置文件中。Spring 框架支持 XML 和Properties 两种格式的配置文件,在实际开发中常用 XML 格式的配置文件。
从前面的学习得知 XML 配置文件的根元素是< beans>,
< beans>中包含多个< bean>子元素,每个< bean>元素定义一个 Bean,并描述 Bean 如何被装配到 Spring 中。< bean>元素的常用属性及其子元素如下图:

属性或子元素名称 描 述
id Bean 在 BeanFactory中唯一的标识,在代码中通过 BeanFactory 获取 Bean 实例时需要以此作为索引名称
class Bean 的具体实现类,使用类的名(例如:demo2.TestErbao)
scope 指定 Bean 实例的作用域,具体在后面讲解
constructor-arg < bean>元素的子元素,使用构造方法注入,指定构造方法的参数。该元素的 index属性指定参数的序号,ref 属性指定对 BeanFactory 中其他 Bean 的引用关系,type 属性指定参数的类型,value 属性指定参数的常量值
property < bean>元素的子元素,用于设置一个属性。该元素的name属性指定Bean 实例中相应的属性名称,value 属性指定 Bean 的属性值,ref 属性指定属性对BeanFactory 中其他 Bean 的引用关系
list property元素的子元素,用于封装 List 或数组类型的依赖注入,具体后面介绍(3.5)
map property元素的子元素,用于封装 Map 类型的依赖注入,具体后面介绍(3.5)
set property元素的子元素,用于封装 Set 类型的依赖注入,具体后面介绍(3.5)
entry map元素的子元素,用于设置一个键值对,具体后面介绍(3.5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="TestErbao" class="demo2.dao.TestErbao"/>

<bean id="testServiceImpl" class="demo2.service.TestErServiceImpl">
<!-- index = 0 表示 第一个参数为 引用的 testDIDaoImpl-->
<constructor-arg index="0" ref="TestErbao"/>
</bean>
<bean id="testServiceImpl2" class="demo2.service.TestErServiceImpl2">

<property name="TestEr" ref="TestErbao"/>
</bean>

</beans>

3.2 Bean 的实例化

在面向对象编程中,如果想使用某个对象,需要实现实例化该对象。同样,在 Spring 框架中,如果像使用 Spring 容器中的 Bean,也需要实例化 Bean。Spring 框架实例化 Bean 有3种方式,即构造方法实例化、静态工厂实例化和实例工厂实例化(其中,最常用的是构造方法实例化)。

3.2.1 构造方法实例化

在 Spring 框架中,Spring 容器可以调用 Bean 对应类中的无参构造方法来实例化 Bean,这种方法称为无参构造方法实例化。下面代码演示过程

1.创建demo3包

1636256717812

2.创建 BeanClass
在demo3包中创建 BeanClass 类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package demo3;

public class BeanClass {
public String messages;

// 无参构造
public BeanClass(){
messages = "构造方法实例化Bean";
}

// 有参构造
public BeanClass(String s){
messages = s;
}
}


3.创建配置文件
在demo3包下创建 applicationContext.xml 文件,在配置文件中定义一个 id为 constructorInstance 的 Bean,代码如下

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="constructorInstance" class="demo3.BeanClass"/>

</beans>

4、创建测试类
在demo3包下创建Test类并进行测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo3;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");
BeanClass constructorInstance = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
System.out.println(constructorInstance + constructorInstance.messages);
}
}


输出结果

1636256957757

3.2.2 静态工厂实例化

在使用静态工厂实例化 Bean 时要求开发者在工厂类中创建一个静态方法来创建 Bean 的实例。在配置 Bean 时,class 属性指定静态工厂类,同时还需要使用 factory-method 属性指定工厂类中的静态方法。下面通过代码演示:

创建工厂类 BeanStaticFactory
在 demo3包中创建 BeanStaticFactory,该类中有一个静态方法来实例化对象,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package demo3;

public class BeanStaticFactory {
private static BeanClass beanInstance = new BeanClass("调用静态工厂方法");

public static BeanClass createInstance(){
return beanInstance;
}


}


编辑demo3包下的配置文件 applicationContext.xml 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="constructorInstance" class="demo3.BeanClass"/>

<!-- 静态工厂方法实例化 Bean,createInstance 为静态工厂类 BeanStaticFactory 中的静态方法 -->
<bean id="staticFactoryInstance" class="demo3.BeanStaticFactory" factory-method="createInstance"/>

</beans>

编写Test类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package demo3;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");

BeanClass staticFactoryInstance = (BeanClass) classPathXmlApplicationContext.getBean("staticFactoryInstance");
System.out.println(staticFactoryInstance + staticFactoryInstance.messages);
}
}


1636257115687

3.2.3 实例工厂实例化

在使用实例工厂实例化 Bean 时要求开发者在工厂类中创建一个实例方法来创建 Bean 的实例。在配置 Bean 时需要使用 factory-bean 属性指定配置的实例工厂,同时还需要使用 factory-method 属性指定实例工厂中的实例方法。下面通过代码讲解:

创建工厂类 BeanInstanceFactory
在demo3包中创建工厂类 BeanInstanceFactory,该类中有一个实例方法来实例对象,代码如下:

1
2
3
4
5
6
7
8
9
package demo3;

public class BeanInstanceFactory {
public BeanClass createInstanceFactory(){
return new BeanClass("调用实例工厂方法实例化 Bean");
}
}


编辑demo3包下的配置文件 applicationContext.xml 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 配置工厂 -->
<bean id="myFactory" class="demo3.BeanInstanceFactory"/>

<!-- 使用 factory-bean 属性指定配置工厂,使用 factory-method 属性指定使用工厂中的哪一个方法实例化 Bean -->
<bean id="instanceFactoryInstance" factory-bean="myFactory" factory-method="createInstanceFactory"/>
</beans>

编写测试代码

3.3 Bean 的作用域

在 Spring 中不仅可以完成 Bean 的实例化,还可以为 Bean 指定作用域。在 Spring 5.0 中为 Bean 的实例定义了如下所示的作用域

作用域名称 描述
singleton 默认的作用域,使用 singleton 定义的 Bean 在 Spring 容器中只有一个 Bean实例
prototype Spring 容器每次获取prototype 定义的 Bean,容器都将创建一个新的 Bean 实例
request 在一个 HTTP 请求中容器将返回一个 Bean实例,不同的 HTTP 请求返回不同的 Bean实例。仅在 Web Spring 应用程序上下文中使用
session 在一个 HTTP Session中,容器将返回同一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用
application 为每个 ServletContext 对象创建一个实例,即同一个应用共享一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用
websocket 为每个 WebSocket 对象创建一个 Bean 实例。仅在 Web Spring 应用程序上下文中使用

3.3.1 singleton作用域

当将 bean 的 scope 设置为 singleton 时,Spring IoC 容器仅生成和管理一个 Bean 实例。在使用 id 或 name 获取 Bean 实例时,IoC 容器将返回共享的 Bean 实例。
由于 singleon 时 scope 的默认方式,因此:

1
2
<bean id="constructorInstance" class="demo3.BeanClass"/>

1
2
<bean id="constructorInstance" class="demo3.BeanClass" scope="singleton"/>

是等价的。

测试 singleton 的作用域,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package demo3;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo3/applicationContext.xml");

BeanClass constructorInstance = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
BeanClass constructorInstance2 = (BeanClass) classPathXmlApplicationContext.getBean("constructorInstance");
System.out.println(constructorInstance);
System.out.println(constructorInstance2);

}
}


测试结果;

1636375449367

Bean 实例时,IoC 容器仅返回一个 Bean 实例。

3.3.2 prototype作用域

当将 Bean 的 scope 设置为 prototype 时,Spring IoC 容器将为每次请求创建一个新的实例。将3.3.1中applicationContext.xml配置文件修改如下:

1
2
<bean id="constructorInstance" class="demo3.BeanClass" scope="prototype"/>

测试代码不变,输出结果如下:

1636374246248

**从上图运行结果得知,在使用id或name 两次获取 Bean 实例时, IoC 容器将返回两个不同的 Bean 实例。 **

3.4 Bean 的生命周期

一个对象的生命周期包括创建(实例化与初始化)、使用以及销毁等阶段,在 Spring 中,Bean 对象周期也遵循这一过程, 但是 Spring 提供了许多对外接口,允许开发者对3个过程(实例化、初始化、销毁)的前后做一些操作。 在 Spring Bean 中,实例化是为 Bean 对象开辟空间,初始化则是对属性的初始化。
Spring 容器可以管理 singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道 Bean 何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的 Bean , Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 实例就交给了客户端的代码管理,Spring 容器将不再跟踪其生命周期,并且不会管理那些被配置成 prototype 作用域的 Bean 。Spring 中 Bean 的生命周期的执行是一个很复杂的过程,可借鉴 Servlet 的生命周期“实例化初始化 (init)–>接收请求 (service)–>销毁 (destroy)”来理解 Bean 的生命周期。

Bean 的生命周期的整个过程如下:
  • (1)根据 Bean 的配置情况实例化一个 Bean。
  • (2)根据 Spring 上下文对实例化的 Bean 进行依赖注入,即对 Bean 的属性进行初始化。
  • (3)如果 Bean 实现了 BeanNameAware 接口,将调用它实现的setBeanName(Stringbeanld) 方法,此处参数传递的是 Spring 配置文件中 Bean 的 id。
  • (4)如果 Bean 实现了 BeanFactoryAware 接口,将调用它实现的setBeanFactory 方法,此处参数传递的是当前 Spring 工厂实例的引用。
  • (5)如果 Bean 实现了 ApplicationContextAware 接口,将调用它实现的 setApplicationContext(applicationContext)方法,此处参数传递的是 Spring 上下文实例的引用。
  • (6)如果 Bean 关联了 BeanPostProcessor 接口,将调用初始化方法postProcessBeforeInitialization(Object obj, String s)对 Bean 进行操作。
  • (7)如果 Bean实现了 InitializingBean 接口,将调用 afterPropertiesSet方法。
  • (8)如果 Bean在Spring 配置文件中配置了 init-method 属性,将自动调用其配置的初始化方法。
  • (9)如果 Bean 关联了 BeanPostProcessor 接口,将调用postProcessAfterlnitializatior(Object obj, String s) 方法,由于是在 Bean初始化结束时调用After方法,也可用于内存或缓存技术。
  • 注意:以上工作完成后就可以使用该 Bean ,由于该Bean的作用域是singleton,所以调用的是同一个 Bean 实例。
  • 10)当 Bean 不再需要时将进入销毁阶段,如果 Bean 实现了DiposableBaen 接口,则调用其实现的destroy方法将 Spring 中的 Bean销毁。
  • (11)如果配置文件中通过 destroy-method 属性指定了 Bean 的销毁方法,将调用其配置的销毁方法进行销毁。

在 Spring 中,通过实现特定的接口或通过< bean> 元素设置可以对 Bean 的生命周期过程产生影响。开发者可以随意地配置< bean> 元素的属性,但不建议过多地使用 Bean 实现接口,因为这样将使代码和 Spring 聚合比较紧密。下面通过实例演示 Bean 的生命周期。

1.创建 Bean 的实现类

在 src 下创建一个 demo4 的包,在此包下创建 BeanLife 类。在 BeanLife类中有两个方法,一个演示初始化过程,一个演示销毁过程。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package demo4;

public class BeanLife {
public void initMyself(){
System.out.println(this.getClass().getName()+" 执行自定义的初始化方法");
}

public void destroyMysqlf(){
System.out.println(this.getClass().getName()+" 执行自定义的销毁方法");

}
}

2.配置 Bean

在demo4 中创建 ApplicationContext.xml 配置文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">


<!-- 配置 bean,使用 init-method 属性指定初始化方法,使用 destroy-method 属性指定销毁方法 -->
<bean id="BeanLife" class="demo4.BeanLife" init-method="initMyself" destroy-method="destroyMyself"/>

</beans>

3.测试生命周期

在 demo4 中创建 Test 测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package demo4;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {

// 为了方便演示销毁方法的执行,这里使用 ClassPathXmlApplicationContext
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("demo4/applicationContext.xml");

System.out.println("获取对象前");

BeanLife beanLife = (BeanLife) classPathXmlApplicationContext.getBean("BeanLife");

System.out.println("获取对象后"+beanLife);

classPathXmlApplicationContext.close();
}
}


输出结果:

1636377275222

从图中我们可以看出,在加载配置文件时执行了 Bean 的初始化方法 initMyself;在获得对象后,关闭容器时,执行了 Bean 的销毁方法 destroyMyself。

3.5 Bean 的装配方式

Bean 的装配可以理解为将 Bean 依赖注入到 Spring 容器中,Bean 的装配方式即 Bean 依赖注入的方式。Spring 容器支持基于 XML 配置的装配、基于注解的装配以及自动装配等多种装配方式,其中最受亲睐的装配方式是基于注解的装配。本章主要讲解基于 XML 配置的装配和基于注解的装配。

3.5.1 基于 XML 配置的装配

基于 XML 配置的装配方式已经有很久的历史了,曾经是主要的装配方式。通过 2.3 节的学习,我们知道 Spring 提供了两种基于XML 配置的装配方式,即使用构造方法注入和使用属性的setter方法注入。
在使用构造方法注入方式装配 Bean时,Bean 的实现类需要提供带参数的构造方法,并在配置文件中使用< bean>元素的子元素< constructor-arg>来定义构造方法的参数;在使用属性的setter方法注入方式装配 Bean时,Bean 的实现类需要提供一个默认无参数的构造方法,并为需要注入的属性提供对应的 setter 方法,另外还需要使用< bean>元素的子元素< property>为每个属性注入值。

下面通过一个实例来演示基于XML配置的装配方式。

  1. 创建 Bean 的实现类

    在 src 目录下创建 assemble 包,在此包下创建 ComplexUser 类。在ComplexUser 类中分别使用构造方法和使用属性的 setter 方法注入。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package assemble;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;



public class ComplexUser {
private String name;
private List<String> hobbyList;
private Map<String,String> residenceMap;
private Set<String> aliasSet;
private String[] array;

// 无参构造,setter 方法注入时需要用到,
public ComplexUser(){
super();
}


public ComplexUser(String name, List<String> hobbyList, Map<String, String> residenceMap, Set<String> aliasSet, String[] array){
this.name = name;
this.hobbyList = hobbyList;
this.residenceMap = residenceMap;
this.aliasSet = aliasSet;
this.array = array;
}


/**
* 属性的 set 方法
*/
public void setName(String name) {
this.name = name;
}

public void setHobbyList(List<String> hobbyList) {
this.hobbyList = hobbyList;
}

public void setResidenceMap(Map<String, String> residenceMap) {
this.residenceMap = residenceMap;
}

public void setAliasSet(Set<String> aliasSet) {
this.aliasSet = aliasSet;
}


public void setArray(String[] array) {
this.array = array;
}


@Override
public String toString() {
return "ComplexUser{" +
"name='" + name + '\'' +
", hobbyList=" + hobbyList +
", residenceMap=" + residenceMap +
", aliasSet=" + aliasSet +
", array=" + Arrays.toString(array) +
'}';
}
}





  1. 配置 Bean
    在 Spring 配置文件( applicationContext.xml 代码如下 )中使用 ComplexUser 配置 Bean 的两个实例,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 使用构造方法注入 -->
<bean id="user1" class="assemble.ComplexUser">
<constructor-arg index="0" value="wdf"/>
<constructor-arg index="1">
<list>
<value>唱歌</value>
<value>爬山</value>
<value>玩游戏</value>
</list>
</constructor-arg>
<constructor-arg index="2">
<map>
<entry key="dalian" value="大连"/>
<entry key="beijing" value="北京"/>
<entry key="shanghai" value="上海"/>
</map>
</constructor-arg>
<constructor-arg index="3">
<set>
<value>wdf100</value>
<value>wdf101</value>
<value>wdf102</value>
</set>
</constructor-arg>
<constructor-arg index="4">
<array>
<value>aaa</value>
<value>bbb</value>
</array>
</constructor-arg>

</bean>

<!-- 使用 setter 方法注入 -->
<bean id="user2" class="assemble.ComplexUser">
<property name="name" value="wdf2"/>
<property name="hobbyList">
<list>
<value>看书</value>
<value>学习</value>
</list>
</property>
<property name="residenceMap">
<map>
<entry key="shenzhen" value="深圳"/>
<entry key="tianjin" value="天津"/>
</map>
</property>
<property name="aliasSet">
<set>
<value>wdf104</value>
<value>wdf105</value>
</set>
</property>
<property name="array">
<array>
<value>cccc</value>
<value>dddd</value>
</array>
</property>
</bean>
</beans>

Test类 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package assemble;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("assemble/applicationContext.xml");
// 使用构造方法装配
ComplexUser user1 = (ComplexUser) classPathXmlApplicationContext.getBean("user1");

// 使用 setter 方法装配
ComplexUser user2 = (ComplexUser) classPathXmlApplicationContext.getBean("user2");

System.out.println(user1);
System.out.println(user2);
}
}


输出结果:

1636428820726

3.5.2 基于注解的装配

在 Spring 框架中,尽管使用 XML 配置文件可以简单地装配 Bean,但如果应用中有大量的 Bean 需要装配,会导致 XML 配置文件过于庞大,不方便以后的升级与维护,因此更多的时候推荐开发者使用注解(annotation)的方式去装配 Bean。
在 Spring 框架中定义了一系列的注解,下面介绍几种常用的注解。

一 、@Component
该注解是一个泛化的概念,仅仅表示一个组件对象(Bean),可以作用在任何层次上。下面通过一个实例讲解@Component。

  • 创建 Bean的实现类
    在 src 目录下创建 annotation 包,在该包下创建 Bean 的实现类 AnnotationUser,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package annotation;

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;

    @Component()
    /**
    * 相当于把 id 为 annotationUser 的 Bean 注册到 Spring 容器中。
    */
    public class AnnotationUser {

    @Value("wdf") // 只能注入简单的值,对于复杂的值目前使用该方法还解决不了
    private String name;

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

    public String getName() {
    return name;
    }
    }


  • 配置注解
    现在有了 Bean 的实现类,但不能进行测试,因为 Spring 容器并不知道去哪里扫描 Bean 对象,需要在配置文件中配置注解,在annotation包下创建applicationContext.xml 具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- 扫描annotation包下的注解 -->
<context:component-scan base-package="annotation"/>

</beans>

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package annotation;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("annotation/applicationContext.xml");
AnnotationUser annotationUser = (AnnotationUser) classPathXmlApplicationContext.getBean("annotationUser");
System.out.println(annotationUser.getName());
}
}


输出结果:

1636451486577

注:在 Spring 4.0以上的版本,配置注解指定包中的注解进行扫描前需要实现导入 Spring AOP 的 JAR 包 spring-aop-5.0.2.RELEASE.jar

  • 二、@Repository
    该注解用于将数据访问层(DAO)的类表示为 Bean,即注解数据访问层 Bean,其功能与@Component 相同。

  • 三、@Service
    该注解用于标注一个业务逻辑组件类(Service层),其功能与@Component 相同。

  • 四、@Controller
    该注解标注一个控制器组件类(Spring MVC 的 Controller),其功能与@Component 相同。

  • 五、@Autowired
    该注解可以对类成员变量、方法及构造方法进行标注,完成自动装配的工作。通过使用@Autowired 来消除 setter 和 getter 方法。默认按照 Bean 的类型进行装配。

  • 六、@Resource
    该注解与@Autowired 的功能一样,区别在于该注解默认是按照名称来装配注入的,只有当找不到名称匹配的 Bean 时才会按照类型来装配注入;而@Autowired 默认按照 Bean 的类型进行装配,如果想按照名称来装配注入,则需要和@Qualifier 注解一起使用
    @Resource 注解有两个属性,name和type。name 属性指定 Bean 实例名称,即按照名称来装配注入;type 属性指定 Bean 的类型,即按照 Bean 的类型进行装配。

  • 七、@Qualifier
    该注解与 @Autowired 注解配合使用。当@Autowired 注解需要按照名称来装配注入时需要和该注解一起使用,Bean 的实例名称由@Qualifier 注解的参数指定。
    在上面几个注解中,虽然@Repository、@Service和@Controller 等注解的功能与@Component 注解相同,但为了使类的标注更加清晰(层次化),在实际开发中推荐使用@Repository 标注数据访问层(DAO层)、使用@Service 标注业务逻辑层(Service 层)、使用@Controller 标注控制器层(控制层)
    下面通过一个实例讲解如何使用这些注解:

  • 创建DAO层
    在 src 中创建 annotation.dao 包,在该包下创建 TestDao 接口和 TestDaoImpl实现类,并将实现类 TestDaoImpl 使用 @Repository 注解标注为数据访问层。

TestDao 的代码如下:

1
2
3
4
5
package annotation.dao;

public interface TestDao {
public void save();
}

TestDaoImpl 的代码如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package annotation.dao;

import org.springframework.stereotype.Repository;

@Repository("testDaoImpl")
// 相当于@Repository,但如果在 service 层使用@Resource(name=”testDaoImpl“), testDaoImpl 不能省略
public class TestDaoImpl implements TestDao {
@Override
public void save() {
System.out.println("testDao save");
}
}



  • 创建 Service 层
    在 src 中创建 annotation.service 包,在该包下创建 TestService 接口和 TestServiceImpl 实现类,并将实现类 TestServiceImpl 使用@Service 注解标注为业务逻辑层。
    TestService 的代码如下:
1
2
3
4
5
6
7
package annotation.service;

public interface TestService {
public void save();
}


TestServiceImpl 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package annotation.service;

import annotation.dao.TestDao;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service("testServiceImpl")
// 相当于@Service
public class TestServiceImpl implements TestService {

@Resource(name = "testDaoImpl")
private TestDao testDao;

@Override
public void save() {
testDao.save();
System.out.println("testService save");
}
}


  • 创建 Controller 层:
    在 src 中创建 annotation.controller 包,在该包下创建 TestController 类,并将 TestController 类使用@Controller 注解标注为控制层。

    TestController 的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package annotation.controller;

    import annotation.service.TestService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;

    @Controller
    public class TestController {

    @Autowired
    private TestService testService;

    public void save(){
    testService.save();
    System.out.println("testController save");
    }
    }


    配置注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <!-- 扫描annotation包下的注解 -->
    <context:component-scan base-package="annotation"/>

    </beans>

    测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package annotation.service;

    import annotation.controller.TestController;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class test {
    public static void main(String[] args) {
    ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("annotation/applicationContext.xml");
    TestController testController = (TestController) classPathXmlApplicationContext.getBean("testController");
    testController.save();
    }
    }


    1636546338439

Spring AOP

Spring AOP 是 Spring 框架体系结构中非常重要的功能模块之一,该模块提供了面向切面编程实现。面向切面编程在事物处理、日志记录、安全控制等操作中被广泛使用。本章将对 Spring AOP 的相关概念及实现进行详细讲解。

4.1 Spring AOP 的基本概念

4.1.1 AOP 的概念

AOP (Aspect-Oriented Programming)即面向切面编程,它与OOP( Object-OrientedProgramming,面向对象编程)相辅相成,提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中,以类作为程序的基本单元,而 AOP 中的基本单元是Aspect(切面)。Struts2 的拦截器设计就是基于 AOP 的思想,是个比较经典的应用。
在业务处理代码中通常有日志记录、性能统计、安全控制、事务处理、异常处理等操作。尽管使用 OOP 可以通过封装或继承的方式达到代码的重用,但仍然有同样的代码分散在各个方法中。因此,采用 OOP 处理日志记录等操作不仅增加了开发者的工作量,而且提高了升级维护的困难。为了解决此类问题,AOP 思想应运而生。AOP 采取横向抽取机制,即将分散在各个方法中的重复代码提取出来,然后在程序编译或运行阶段将这些抽取出来的代码应用到需要执行的地方。这种横向抽取机制采用传统的 OOP 是无法办到的,因为 OOP 实现的是父子关系的纵向重用。但是 AOP 不是 OOP 的替代品,而是 OOP 的补充,它们相辅相成。
在 AOP 中,横向抽取机制的类与切面的关系:

1636546628373

4.1.2 AOP 的术语

在 Spring AOP 框架中涉及以下常用术语。

  • 切面
    切面(Aspect)是指封装横切到系统功能(例如事物处理)的类。

  • 连接点
    连接点(Joinpoint)是指程序运行中的一些时间点,例如方法的调用或异常的抛出。

  • 切入点
    切入点(Pointcut)是指需要处理的连接点。在 Spring AOP 中,所有的方法执行都是连接点,而切入点是一个描述信息,他修饰的是连接点,通过切入点确定哪些连接点需要被处理。切面、连接点和切入点的关系如下:

1636546744528

  • 通知
    通知(Advice)是由切面添加到特定的连接点(满足切入点规则)的一段代码,即在定义好的切入点处所要执行的程序代码,可以将其理解为切面开启后切面的方法,因此通知是切面的具体实现。

  • 引入
    引入(Introduction)允许在现有的实现类中添加自定义的方法和属性。

  • 目标对象
    目标对象(Target Object)是指所有被通知的对象。如果 AOP 框架使用运行时代理的方式(动态的 AOP)来实现切面,那么通知对象总是一个代理对象。

  • 代理
    代理(Proxy)是通知应用到目标对象之后被动态创建的对象。

  • 织入
    织入(Weaving)是将切面代码插入到目标对象上,从而生成代理对象的过程。根据不同的实现技术,AOP 织入有 3 中方式;编译器织入,需要由特殊的 Java 编译器;类装载期织入,需要有特殊的类装载器;动态代理织入,在运行期为目标类添加通过生成子类的方式。Spring AOP 框架默认采用动态代理织入,而 AspectJ(基于 Java 语言的 AOP框架)采用编译期织入和类装载期织入

4.2 动态代理

在 Java 中有很多动态代理技术,例如 JDK、CGLIB、Javassist、ASM,其中最常用的动态代理技术是 JDK 和 CGLIB。目前,在 Spring AOP 中常用 JDK 和 CGLIB 两种动态代理技术。

4.2.1 JDK 动态代理

JDK 动态代理是 java.lang.reflect.*包提供的方式,它必须借助一个接口才能产生代理对象,因此,对于使用业务接口的类,Spring 默认使用 JDK 动态代理实现 AOP。下面通过一个实例演示如何使用 JDK 动态代理实现 Spring AOP,具体步骤如下:

注:这里我们使用maven工程,其好处是导入jar包时十分方便。

安装 maven
配置 maven

  • 使用 Idea 创建 maven工程

1636547729841

一直点击下一步

  • 在pom 文件添加相关依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>spring02</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>JDK</module>
</modules>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
</dependencies>

</project>

把 src 目录删掉,我们在其中右键创建 Module。

1636547866656

  1. 我们在 JDK module 的 src 目录下创建 dynamic.jdk 包,在该包创建接口 TestDao 和接口实现类 TestDaoImpl。该实现类作为目标类,在代理类中对其方法进行增强处理。

    TestDao的代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package dynamic.jdk;

    public interface TestDao {
    public void save();

    public void modify();

    public void delete();
    }


    TestDaoImpl 的代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package dynamic.jdk;

    public class TestDaoImpl implements TestDao {

    public void save() {
    System.out.println("保存");
    }


    public void modify() {
    System.out.println("修改");
    }


    public void delete() {
    System.out.println("删除");
    }
    }


    1. 创建切面类
      在 JDK module 的 dynamic.jdk 目录下创建切面类 MyAspect,注意在该类中可以定义多个通知(增强处理的功能方法)
      MyAspect 的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      package dynamic.jdk;

      public class MyAspect {
      public void check(){
      System.out.println("模拟权限控制");
      }

      public void except(){
      System.out.println("模拟异常处理");
      }

      public void log(){
      System.out.println("模拟日志");
      }
      }


  1. 创建代理类
    在 dynamic.jdk 包中创建代理类 JDKDynamicProxy。在 JDK 动态代理类必须实现 java.lang.reflect.InvocationHandler 接口,并编写代理方法,在代理方法中需要通过 Proxy 实现动态代理。

    JDKDynamicProxy 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package dynamic.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKDynamicProxy implements InvocationHandler {

// 声明目标类接口对象(真实对象)
private TestDao testDao;

// 创建代理的方法,建立代理对象和真实对象的代理关系,并返回代理对象
public Object createProxy(TestDao testDao){
this.testDao = testDao;

// 类加载器
ClassLoader cld = JDKDynamicProxy.class.getClassLoader();

// 被代理对象实现的所有接口
Class[] classes = testDao.getClass().getInterfaces();

// 使用代理类进行增强,返回代理后的对象
return Proxy.newProxyInstance(cld,classes,this);
}


/**
* 代理的逻辑对象
* @param proxy 被代理的对象
* @param method 将要执行的方法
* @param args 执行方法时需要的参数
* @return 返回代理结果
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 创建一个切面
MyAspect myAspect = new MyAspect();

// 前增强
myAspect.check();
myAspect.except();

// 在目标类上调用方法并传入参数,相当于调用 testDao中的方法
Object obj = method.invoke(testDao,args);

// 后增强
myAspect.log();

return obj;
}
}



  1. 创建测试类
    在test.java 包下创建Test类进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import dynamic.jdk.JDKDynamicProxy;
import dynamic.jdk.TestDao;
import dynamic.jdk.TestDaoImpl;

public class Test {
public static void main(String[] args) {
// 创建代理对象
JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy();

// 创建目标对象
TestDao testDao = new TestDaoImpl();

// 从代理对象中获取增强后的目标对象,该对象是一个被代理的对象,它会进入代理的逻辑方法 invoke中
TestDao testDaoAdvice = (TestDao) jdkDynamicProxy.createProxy(testDao);

// 执行方法
testDaoAdvice.save();
System.out.println("-----------");
testDaoAdvice.modify();
System.out.println("-----------");
testDaoAdvice.delete();
System.out.println("-----------");



}
}


运行结果如下

1636549473353

4.2.2 CGLIB

从 4.2.1 节可知,JDK 动态代理必须提供接口才能使用,对于没有提供接口的类,只能采用 CGLIB 动态代理。
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,采用非常底层的字节码技术,对指定的目标生成一个子类,并对子类进行增强。在 Spring Core 包中已经集成了 CGLIB 所需要的 jar 包。下面通过一个实例演示:

创建 CGLIB module

创建目标类
在 CGLIB module 下创建 dynamic.cglib包,在该包下创建 TestDao。
TestDao 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package dynamic.cglib;

public class TestDao {


public void save() {
System.out.println("保存");
}

public void modify() {
System.out.println("修改");
}

public void delete() {
System.out.println("删除");
}

}


  1. 创建代理类
    在 dynamic.cglib 包下创建代理类 CglibDynamicProxy,该类实现 MethodInterceptor 接口。
    CglibDynamicProxy 的代码如下:

  2. package dynamic.cglib;
    
    

import dynamic.jdk.MyAspect;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibDynamicProxy implements MethodInterceptor {
public Object createProxy(Object target){
// 创建一个动态类对象,即增强类对象
Enhancer enhancer = new Enhancer();

       // 确定需要增强的类,设置其父类
       enhancer.setSuperclass(target.getClass());

       // 确定代理逻辑对象为当前对象,要求当前对象实现 MethodInterceptor 的方法
       enhancer.setCallback(this);

       // 返回创建的代理对象
       return enhancer.create();
   }


   public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
       // 创建一个切面
       MyAspect myAspect = new MyAspect();

       // 前增强
       myAspect.check();
       myAspect.except();

       // 目标方法执行,返回代理结果
       Object obj = methodProxy.invokeSuper(o,objects);

       // 后增强
       myAspect.log();

       return obj;
   }

}

1
2
3
4
5
6

**注**:这里的切面我们还是用的 JDK 代理时用到的切面

1. 创建测试类
具体代码如下:

  package dynamic.cglib;

  public class Test {
      public static void main(String[] args) {
          // 创建代理对象
          CglibDynamicProxy cdp = new CglibDynamicProxy();

          // 创建目标对象
          TestDao testDao = new TestDao();

          // 获取增强后的目标对象
          TestDao proxy = (TestDao) cdp.createProxy(testDao);

          // 执行方法
          proxy.delete();
          System.out.println("---------");
          proxy.save();
          System.out.println("---------");
          proxy.modify();
          System.out.println("---------");
      }

  }


  ```

  输出如下

  ![1636588836248](https://py.jkraise.top/SpringStudy.assets/1636588836248.png~01style)

4.3 基于代理类的 AOP 实现

从 4.2 节可知,在 Spring 中默认使用 JDK 动态代理实现 AOP 编程。使用 org.springframework.aop.framework.ProxyFactoryBean 创建代理是 Spring AOP 实现的基本方式。

一、通知类型
在讲解 ProxyFactoryBean 之前先了解以下 Spring 的通知类型。根据 Spring 中通知在目标类方法的连接点位置,通知可以分为 6 中类型。

  • 环绕通知
    环绕通知实在目标方法执行前和执行后实施增强,可应用于日志记录、事物处理等功能。

  • 前置通知
    前置通知是在目标方法执行前实施增强,可应用于权限管理等功能。

  • 后置返回通知
    后置返回通知是在目标方法成功执行后实施增强,可应用于关闭流、删除临时文件等功能。

------ 本文结束------

欢迎关注我的其它发布渠道