深入淺出 Spring 框架,原來以前的都白學了

追逐仰望星空 發佈 2022-05-12T00:29:18.029867+00:00

為啥要用 Spring。張三是一個編程小白,他每次在 service 層寫代碼都要自己 new 一堆 Dao 接口的實現類。

推薦學習

  • 1:肝了十天半月,獻上純手繪「Spring/Cloud/Boot/MVC」全家桶腦圖
  • 2:全網首發!馬士兵內部共享—1658頁《Java面試突擊核心講》

1. 為啥要用 Spring

張三是一個編程小白,他每次在 service 層寫代碼都要自己 new 一堆 Dao 接口的實現類。

public class ProjectServiceImpl implements ProjectService {
    UserDao userDao = new UserDaoImpl();
    ProjectSectionDao projectSessionDao = new ProjectSessionDaoImpl();
    ProjectDao projectDao = new ProjectDaoImpl();
    SupplyDao supplyDao = new SupplyDaoImpl();
    .......   
}

有一天正 new 著對象,張三心想:"我這一個 service 都需要 new 好多 Dao ,那如果有一堆 service ,那我不得花費好長時間?"

"有沒有一個工具類或者什麼框架能幫我管理這些對象?我只需要配置一下,需要的時候它就能自動幫我 new 個對象出來?"

張三陷入了深深的沉思之中。

張三的室友李四也是一個編程小白。

李四呢想給自己的小項目增加一個功能:記錄方法執行的時間。結果他腦子一熱竟然給所有的方法都增加了一堆列印方法:

System.out.println("項目開始執行");
// 開始時間
long start = System.currentTimeMillis();

// 業務代碼

// 結束時間
long end = System.currentTimeMillis();
// 計算執行時間
System.out.printf("執行時間:%d 毫秒.", (end - start));

過了半個小時,李四終於給項目中所有的方法都複製粘貼上了列印語句。他長舒一口氣:"我真是個大聰明!"

張三看了一眼李四的代碼,連連鼓掌:"妙啊!咱們宿舍的技術大神!"

旁邊的王五實在忍不住了,對張三說:"妙個屁!最近的 Spring 框架課你倆是不是都沒去?光顧著打遊戲了?我都替你倆答了三次到了!"

李四問王五:"這個Spring 框架學了有用嗎?"

王五:"不僅能解決張三說的管理對象的問題,還能幫你解決記錄日誌的問題。配置完 Spring ,你只需要定義一個切面類,根本不需要在一堆類上面複製粘貼一堆代碼。"

張三摸摸後腦勺笑著說:"原來 Spring 框架那麼好用,我以後再也不逃課了。我這就去翻課本學習 Spring 框架去。"

2. Spring 簡介

Spring 是一個輕量級的 Java 開發框架。Spring 的核心是控制反轉(IOC)和面向切面編程(AOP)。

Spring 主要有如下優點:

1.解耦

2.支持面向切面編程

3.便於集成其他框架

3. 環境搭建

1.創建 Maven 項目

File -> New -> Project -> Maven

2.引入依賴

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.16.RELEASE</version>
    </dependency>
</dependencies>

3.創建接口和實現類

UserService

public interface UserService {
    void print();
}

UserServiceImpl

public class UserServiceImpl implements  UserService{
    @Override
    public void print() {
        System.out.println("hello world");
    }
}

4.創建配置文件

applicationContext.xml

<?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="userService" class="com.xxl.service.impl.UserServiceImpl"/>
</Beans>

5.測試

@Test
public void testSpring(){
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
    // 2、通過工廠類獲得對象
    UserService userService = (UserService)act.getbean("userService");
    // 3.調用方法
    userService.print();
}

測試結果:

4. IOC

4.1 IOC 簡介

IOC,全稱 Inversion of Control,意思是控制反轉。它是 Spring 框架中的一種思想。

控制反轉就是將對象的控制權從程序中的代碼轉移到了 Spring 的工廠,通過 Spring 的工廠完成對象的創建以及賦值。

也就是說之前是我們自己 new 對象、給對象中的成員變量賦值。現在是讓 Spring 來幫助我們創建對象、給成員變量賦值。

4.2 Spring 核心內容描述

1.配置文件

Spring 的配置文件可以放到項目中的任意一個地方,也可以隨意命名,但是建議使用:applicationContext.xml。

你可以將這個配置文件看成一個裝有一堆 bean 標籤的容器。

2.bean 標籤

Spring 工廠創建的對象,叫做 bean,所以一個 bean 標籤代表一個對象。

<bean id="userService" class="com.xxl.service.impl.UserServiceImpl"/>

bean 標籤中必須要有 class 屬性,它的值是一個類的全限定名(包名+類名)。

除了 class 屬性,bean 標籤還可以設置 id 、name 、scope屬性。

id:

id 必須以字母開頭,相當於這個 bean 的身份證號,是唯一的。

如果這個 bean 只使用一次,id 可以省略不寫。

如果這個 bean 需要被其他 bean 引用,或者這個 bean 要使用很多次,則必須要有 id 屬性。

如果只配置 class 屬性,Spring 框架會給每一個 bean 配置一個默認的 id:"全限定名#1"。

例如:

com.xxl.service.impl.UserServiceImpl#1

name:

name 相當於這個 bean 的別名,它可以配置多個,例如:

<bean id="user" name="aa,bb,cc" class="com.xxl.model.User"/>

scope:

scope 屬性可以控制簡單對象的創建次數,它有兩個值:

1.singleton:每次只會創建唯一⼀個簡單對象,默認值。

2.prototype:每⼀次都會創建新的對象。

例如:

<bean id="user" class="com.xxl.model.User" scope="singleton"/>

3.ApplicationContext

ApplicationContext 是 Spring 的工廠,主要用來創建對象。

Spring 通過讀取配置文件創建工廠。

因為 Spring 的工廠會占用大量內存,所以一個程序一般只會創建一個工廠對象。

4.工廠常用方法

1.根據 id 獲取對象

UserService userService = (UserService)act.getBean("userService");

2.根據 id 和類名獲取對象

UserService userService = (UserService)act.getBean("userService",UserService.class);

3.只根據類名獲取對象

UserService userService = (UserService)act.getBean(UserService.class);

4.獲取配置文件中所有 bean 標籤的 id 值

String[] beanDefinitionNames = act.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
    System.out.println(beanDefinitionName);
}

結果:

5.判斷是否存在指定 id 或者 name 的 bean

act.containsBean("userService")

6.判斷是否存在指定 id 的 bean,只能用來判斷 id

act.containsBeanDefinition("userService")

5.創建對象

Spring 是如何創建對象的呢?

工廠和反射

首先說下反射,我們可以通過一個類的全限定名獲取 Class 對象,然後再通過 Class 實例化一個對象:

Class serviceClass = Class.forName("com.xxl.service.impl.UserServiceImpl");
UserService userService = (UserService)serviceClass.newInstance();

Spring 配置文件中 bean 標籤的 id 和類的全限定名一一對應,所以 Spring 工廠的 getBean 方法其實就是先根據 bean 的 id 獲取該類的全限定名,然後再利用反射根據類的全限定名創建對象並返回。

4.3 IOC 優點

解耦

說起解耦之前先說下耦合:耦合是指代碼之間的關聯性太強,我如果改了這一段代碼,可能會影響到一堆代碼。

那創建對象哪裡有耦合了?其實就是new關鍵字帶來的耦合。

如果你發現一個接口的實現類需要修改,你需要手動改動程序中的代碼,比如修改 new 關鍵字後面的實現類,這樣可能會影響到其他的代碼。

但是使用了 Spring 之後,我們只需要修改配置文件中 bean 標籤的 class 屬性對應的類的全限定名,不用修改程序中的代碼,這樣就做到了解耦。

解耦就是解除不同代碼之間的關聯性、依賴性。

5. DI

DI 全稱 Dependency Injection,意思是依賴注入,它是 IOC 的具體實現。

依賴就是說我需要你,比如 Service 層依賴 Dao 層,注入就是賦值

依賴注入:使用 Spring 的工廠和配置文件為一個類的成員變量賦值。

沒有使用 Spring 的依賴注入我們是這樣賦值的:

User user = new User();
user.setName("張三");

如果設置有誤,就需要手動修改代碼,代碼耦合度較高,而依賴注入的出現就是為了解耦。

Spring 的依賴注入包含兩種方式:

5.1 set 注入

set 注入:Spring 調用 Set 方法通過配置文件為成員變量賦值。

1.創建對象,為屬性添加 set/get 方法

public class User {
    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;
    }
}

2.修改配置文件

<bean id="user" class="com.xxl.model.User">
  <property name="name" value="知否君" />
  <property name="age" value="18" />
</bean>

3.測試

// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
User user = (User)act.getBean("user");
System.out.println("姓名:"+user.getName());
System.out.println("性別:"+user.getAge());

測試結果:

從上面的例子可以看出 Set 注入就是在 property 標籤中為屬性賦值。spring 可以為 JDK 內置的數據類型進行賦值,也可以為用戶自定義的數據類型進行賦值。

5.1.1 JDK 內置數據類型

1.基本類型

<property name="name" value="知否君" />
<property name="age" value="18" />

2.List 集合

<property name="phones">
        <list>
                <value>15799999918</value>
                <value>15788888819</value>
                <value>15766666620</value>
        </list>
</property>

3.Set 集合

<property name="phones">
        <set>
                <value>15799999918</value>
                <value>15788888819</value>
                <value>15766666620</value>
        </set>
</property>

4.Map 集合

<property name="mapInfo">
    <map>
        <entry>
            <key><value>name</value></key>
            <value>知否君</value>
        </entry>
        <entry>
            <key><value>age</value></key>
            <value>23</value>
        </entry>
    </map>
</property>

5.數組

<property name="phones">
    <list>
        <value>15799999918</value>
        <value>15788888819</value>
        <value>15766666620</value>
    </list>
</property>

6.Properites

<property name="prop">
    <props>
        <prop key="key1">value1</prop>
        <prop key="key2">value2</prop>
    </props>
</property>

5.1.2 用戶自定義數據類型

1.為成員變量添加 set/get 方法

public class UserServiceImpl implements UserService {
    
    private UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
    @Override
    public void print() {
        userDao.print();
    }
}

2.bean 標籤使用 ref 屬性

<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />
<bean id="userService" class="com.xxl.service.impl.UserServiceImpl">
   <property name="userDao" ref="userDao"/>
</bean>

3.測試

@Test
public void testSpring(){
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // 2、通過工廠類獲得對象
    UserService userService = (UserService)act.getBean("userService");
    // 3.調用方法
    userService.print();
}

測試結果:

解釋:

上面的例子中,因為 userDao 是 userService 的一個成員變量,所以在配置文件中需要使用 property 標籤,ref 指向了 userDao 這個對象,然後調用 userDao 的 set 方法為 userDao 賦值。

4.自動注入

我們還可以使用 bean 標籤的 autowire 屬性為自定義變量自動賦值。當類中引用類型的屬性名和 bean 標籤的 id 值相同時,我們可以使用 byName。例如:

<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />

<bean id="userService" autowire="byName" class="com.xxl.service.impl.UserServiceImpl" />

當類中引用類型的全限定名和 bean 標籤的 class 屬性的值相同,或者是子類、實現類,我們可以使用 byType。例如:

<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />

<bean id="userService" autowire="byType" class="com.xxl.service.impl.UserServiceImpl" />

5.2 構造注入

構造注入:Spring 調用構造方法通過配置文件為成員變量賦值。

1.為類添加構造方法

public class User {
    private String name;
    private int age;

    public User(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;
    }
}

2.修改配置文件

在 bean 標籤中使用 constructor-arg 標籤。

<bean class="com.xxl.model.User">
    <constructor-arg value="張三"/>
    <constructor-arg value="18"/>
</bean>

3.測試

@Test
public void testSpring(){
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // 2、通過工廠類獲得對象
    User user= (User)act.getBean(User.class);
    System.out.println("姓名:"+user.getName());
    System.out.println("年齡:"+user.getAge());
}

測試結果:

5.3 注入總結

注入就是通過 Spring 的配置文件為類的成員變量賦值。在實際開發中,我們一般採用 Set 方式為成員變量賦值。

6. Bean 的生命周期

Bean 生命周期指的就是由 Spring 管理的對象從創建到銷毀的過程,和人生老病死的過程一樣。

它主要分為三個階段: 創建 --> 初始化 --> 銷毀

6.1 創建階段

Spring 工廠創建對象的方式分兩類:

1. singleton 模式

當 scope 屬性為 singleton ,創建 Spring 工廠的同時創建所有單例對象。

例如:

新建 User 類:

public class User {
    String name;
    int age;

    public User() {
        System.out.println("調用User的構造方法");
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

spring 配置文件註冊 bean :

<bean id="user" class="com.xxl.model.User">
    <property name="name" value="知否君"/>
    <property name="age" value="23"/>
</bean>

測試:

  @Test
    public void testSpring(){
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    }

執行結果:

我們發現當創建 Spring 工廠的同時就會調用對象的構造方法。因為 spring 中 bean 默認的 scope 就是 singleton ,所以創建工廠的同時默認就會創建多個單例對象。

如果想修改創建單例對象的方式為獲取的時候才創建,只需要在 bean 標籤上面添加如下屬性:

lazy-init="true"

例如:

<bean id="user" class="com.xxl.model.User" lazy-init="true">
    <property name="name" value="知否君"/>
    <property name="age" value="23"/>
</bean>

2. prototype 模式

只有獲取對象的時候才會創建對象。

修改 bean 標籤的 scope 屬性:

<bean id="user" class="com.xxl.model.User" scope="prototype">
    <property name="name" value="知否君"/>
    <property name="age" value="23"/>
</bean>

測試:

  @Test
    public void testSpring(){
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Object user = act.getBean("user");
        System.out.println(user);
    }

執行結果:

通過上面的例子我們發現只有當執行 getBean() 方法的時候才會調用該類的構造方法。

6.2 初始化階段

spring 中 bean 的初始化操作指的是在創建對象的時候完成一些附加的功能。bean 的初始化操作有兩種實現方式:

1.實現 InitializingBean 接口

public class 類名 implements InitializingBean {
    public void afterPropertiesSet(){
       // 初始化方法操作
    }
}

例如:

public class User implements InitializingBean {
    String name;
    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;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    // 初始化操作
    @Override
    public void afterPropertiesSet(){
            this.name = "張無忌";
            this.age = 30;
    }
}

測試:

  @Test
    public void testSpring(){
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Object user = act.getBean("user");
        System.out.println(user);
    }

執行結果:

2.通過創建普通方法完成初始化

在 User 類中創建一個方法

// 初始化方法
  public void initMethod() {
      this.name = "張無忌";
  }

在配置文件中配置 init-method 屬性

<bean id="user" class="com.xxl.model.User" init-method="initMethod" >
    <property name="name" value="知否君"/>
    <property name="age" value="23"/>
</bean>

測試:

  @Test
    public void testSpring(){
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Object user = act.getBean("user");
        System.out.println(user);
    }

執行結果:

我們發現該初始化方法在創建對象之後修改了 user 對象的名字。

總結:

初始化方法修改了注入的值,所以初始化方法一定在注入之後執行。

6.3 銷毀階段

Spring 銷毀對象前,會調用對象的銷毀方法,完成銷毀操作。

Spring 什麼時候銷毀所創建的對象?當 Spring 工廠關閉時,Spring 工廠會調用我們自定義的銷毀方法。

銷毀方法的定義有兩種方式:

1.實現DisposableBean接口

public class 類名 implements DisposableBean {
    // 銷毀操作
    @Override
    public void destroy(){
        // 銷毀操作業務
    }
}

2.創建普通方法

在 User 類中創建一個方法

  // 銷毀方法
  public void destroyMethod() {
     // 銷毀操作業務
  }

在配置文件中配置 destroy-method 屬性

 <bean id="user" class="com.xxl.model.User" destroy-method="destroyMethod">
      <property name="name" value="知否君"/>
      <property name="age" value="23"/>
  </bean>

7. Bean 的後置處理

Spring 工廠創建完對象後如果還想對對象干點別的事情,除了初始化階段,還可以採用Bean的後置處理。

Bean 的後置處理:對 Spring 工廠創建的對象進行二次加工處理,就是創建完對象後再干點別的事。

Bean 後置處理的流程:

1.實現 BeanPostProcessor 接口

public class BeanProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("後置bean: before 方法");
        return bean;
    }
    
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("後置bean: after 方法");
        if (bean instanceof User) {
            User user = (User) bean;
            user.setName("亞里士多德");
            return user;
        }
        return bean;
    }
}

2.配置文件添加 bean

<bean id="beanProcessor" class="com.xxl.config.BeanProcessor"/>

3.測試

    @Test
    public void testSpring(){
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
        Object user = act.getBean("user");
        System.out.println(user);
    }

執行結果:

前面我們學習了對象的初始化方法,那麼初始化方法和 Bean 的後置處理的執行順序是什麼?

我們來修改一下 User 類,測試一下:

public class User implements InitializingBean {
    String name;
    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;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public void afterPropertiesSet(){
        System.out.println("初始化方法");
    }
}

測試執行順序:

其實在實際開發中,我們很少對 Spring 工廠創建的對象進行初始化操作,一般是採用 Bean 的後置處理的方式來加工對象。

BeanPostProcessor 接口有兩個方法,這裡簡稱 before 和 after 方法。

這兩個方法都是先獲取 Spring 創建的對象,然後再對其加工,加工完成後再交給 Spring。

因為這兩個方法的作用一樣,所以我們一般採用其中的一個方法,這裡建議採用 after 方法。

從上面的例子中我們得到了Spring 操作 bean 的順序:

8. 代理設計模式

8.1 為啥要用代理設計模式?

咱們先來看一個需求:在所有方法的執行前後輸出一段日誌。

程序小白可能會這樣寫:

接口:

public interface CalculateService {
    // 加法
    int add(int a,int b);
    // 減法
    int sub(int a,int b);
}

實現類:

public class CalculateServiceImpl implements CalculateService {
    @Override
    public int add(int a, int b) {
        System.out.println("方法執行前列印");
        int result = a + b;
        System.out.println("方法執行後列印");
        return result;
    }

    @Override
    public int sub(int a, int b) {
        System.out.println("方法執行前列印");
        int result = a - b;
        System.out.println("方法執行後列印");
        return result;
    }
}

但是這樣寫有 3 個問題:

1.代碼混亂:業務代碼和非業務代碼混在一起,看著太亂了

2.代碼重複:如果有多個方法,那就要寫一堆輸出日誌的代碼片段,吃力不討好。

3.代碼耦合:如果非業務代碼(日誌列印)要做修改,那所有相關的業務方法都要改一遍,代碼耦合度太高。

那有什麼解決辦法呢?使用代理。

生活中有關代理的例子無處不在,例如:一些大學可以面向全球招生,所以會衍生很多留學中介,這些中介可以幫學校招生。

所以中介的作用就是幫助僱主做事,有了中介,僱主變得很輕鬆。而在 java 開發中,也存在這樣的代理關係,它的專業術語是代理設計模式。

代理設計模式可以很好解決上面開發中遇到的三個問題,幫助我們簡化代碼、提高工作效率。

8.2 代理設計模式

代理設計模式:通過代理類為目標類做一些額外(非業務)的功能。

專業名詞解釋:

1.目標類(原始類):指的是完成業務的核心類,一般指的是 service 層的各種實現類。

2.目標方法(原始方法):目標類中的方法是目標方法(原始方法)。

3.額外功能(附加功能):列印日誌等非業務功能。

代理設計模式開發步驟:

(1)代理類和目標類實現相同的接口

(2)代理類中除了要調用目標類的方法實現業務功能,還要實現額外功能。

例如:

// 接口
public interface CalculateService {
  業務方法
}

// 目標類
public CalculateServiceImpl implements CalculateService {
  業務方法
}

// 代理類:要實現目標類相同的接口
public CalculateServiceProxy implements CalculateService {
 // 業務方法
 // 額外功能
}

8.3 靜態代理

靜態代理:給每一個目標類手動開發一個代理類。

例如:

public interface CalculateService {
   // 加法
  int add(int a,int b);
   // 減法
  int sub(int a,int b);
}
// 目標類
public CalculateServiceImpl implements CalculateService {
   @Override
    public int add(int a, int b) {
        int result = a + b;
        return result;
    }
    @Override
    public int sub(int a, int b) {
        int result = a - b;
        return result;
    }
}
// 代理類:要實現目標類相同的接口
public CalculateServiceProxy implements CalculateService {
  private CalculateService calculateService = new CalculateServiceImpl();
  @Override
    public int add(int a, int b) {
        System.out.println("方法執行前列印");
        int result = calculateService.add(a,b);
        System.out.println("方法執行後列印");
        return result;
    }
    @Override
    public int sub(int a, int b) {
        System.out.println("方法執行前列印");
        int result = calculateService.sub(a,b);
        System.out.println("方法執行後列印");
        return result;
    }
}

通過上面的例子我們發現靜態代理也存在很多問題:

1.如果存在很多目標類,我們就要手動創建一堆代理類,太繁瑣。

2.代理類中混雜著目標類方法和額外功能,代碼耦合度高。

那有沒有這樣一種代理模式?

  • 1.目標類和代理類互不干擾
  • 2.代碼耦合度低,便於維護

有的,動態代理閃亮登場!

8.4 動態代理

動態代理:也是通過代理類為目標類做一些額外的功能,但是不用手動寫一堆代理類,而是動態地為目標類創建代理類。

開發流程:

  1. 引入依賴
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.16.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

這裡我們主要是引入了 aspectj 這個技術,aspectj 是 spring 社區中非常流行的基於動態代理技術的框架。

  1. 創建目標類和目標方法

接口:

public interface CalculateService {
    // 加法
    int add(int a,int b);
    // 減法
    int sub(int a,int b);
}

實現類(目標類):

public class CalculateServiceImpl implements CalculateService {
     @Override
    public int add(int a, int b) {
        int result = a + b;
        System.out.println("加法操作。。。");
        return result;
    }

    @Override
    public int sub(int a, int b) {
        int result = a - b;
        System.out.println("減法操作。。。");
        return result;
    }
}

3.在 spring 配置文件中註冊 bean

 <bean id="calculateService"  class="com.xxl.service.impl.CalculateServiceImpl" />

4.實現額外功能

這裡我們需要創建一個類實現 MethodInterceptor 接口:

/**
 * @Desc: 動態代理完成非業務功能
 * @Author: 知否技術
 * @date: 下午8:49 2022/5/4
 */
public class PrintLog implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("在目標方法執行之前列印。。。。。");
        // 執行目標方法
        Object object = methodInvocation.proceed();
        System.out.println("在目標方法執行之後列印。。。。。");
        return object;
    }
}

5.註冊完成額外功能的 bean

  <bean id="printLog" class="com.xxl.aop.PrintLog" />
  1. 定義切入點
<!--切入點:給哪些方法加入額外功能-->
<aop:config>
  <aop:pointcut id="pc" expression="execution(* * (..))"/>
</aop:config>
  1. 組裝切入點和額外功能
<!--切入點:給哪些方法加入額外功能-->
<aop:config>
  <aop:pointcut id="pc" expression="execution(* * (..))"/>
  <aop:advisor advice-ref="printLog" pointcut-ref="pc"/>
</aop:config>

8.測試

@Test
  public void testSpring() {
      // 1、獲取工廠
      ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
      // 2、通過工廠類獲得對象
      CalculateService calculateService = (CalculateService) act.getBean("calculateService");
      // 3.調用方法
      int result = calculateService.add(1, 2);
      System.out.println("result:" + result);
  }

講解:

1.上面的例子中我們定義了一個 PrintLog 列印日誌的類,並實現了 MethodInterceptor 接口的 invoke 方法。invoke 方法裡面實現了在目標方法執行前後列印日誌的功能。

2.invoke 方法的返回值就是原始方法的返回值,上個例子中的原始方法就是 add 方法。

3.aop:config 這個標籤用來配置切入點和額外功能。 上面例子中額外功能就是在要執行的方法前後列印日誌,而切入點就是額外功能要作用的位置:比如某些類上或者某些方法上。

4.execution(* * (..)) 是切入點表達式,表示作用在所有類的所有方法上,這個後面會講。

5.上面的例子表示:你無論執行哪個方法,這個方法的前面和後面都會列印一段日誌。

8.5 動態代理實現原理

我們通過 spring 的工廠獲取的對象,其實是通過動態代理技術創建的代理類。那這個代理類在哪裡?

當程序運行的時候,spring 框架通過動態字節碼技術在 JVM 內存中為目標類創建代理類。當程序運行結束的時候,這個代理類就會隨之消亡。

所以使用動態代理不需要手動創建多個代理類。

9. AOP

9.1 AOP 概念

AOP: 全稱 Producer Oriented Programing,即面向切面編程。

那啥是面向切面編程?其實說白了還是 Spring 的動態代理,通過代理類為原始類增加一些 額外功能(例如列印等)。

那啥是切面?

切面 = 切入點 + 額外功能。

切入點:額外功能作用的位置,在哪些類哪些方法上。

額外功能作用在不同的類上面,我們都知道點連接起來構成面,所以不同的切入點連接起來構成了切面,這個切面就像刀切西瓜一樣切在不同的類上面,所以額外功能就對這些類中的方法起了作用。

9.2 AOP 底層實現原理

AOP 的底層還是使用 Spring 的動態代理技術創建代理類對象。

動態代理的方式分為兩種:

  • 基於接口實現動態代理: JDK 動態代理
  • 基於繼承實現動態代理:Cglib 動態代理

9.2.1 JDK 動態代理

創建代理對象的三個元素:

  • 1.原始對象
  • 2.額外功能
  • 3.原始對象實現的接口

代碼格式:

Proxy.newPorxyInstance(classloader,interfaces,invocationHandler)

講解:

(1)classloader:叫做類加載器,它可以用來創建代理對象。

創建方式:

類.class.getClassLOader()

(2)interfaces:原始對象實現的接口

創建方式

接口.getClass().getInterfaces()

(3)invocationHandler:額外功能

創建方式:

InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---- 方法執行前列印 ----");
                // 執行原始方法
                Object ret = method.invoke(caculateService, args);
                System.out.println("---- 方法執行後列印 ----");
                return ret;
            }
        };

完整代碼:

@Test
public void testJDKProxy() {
    // 1. 原始對象
    CalculateService calculateService = new CalculateServiceImpl();

    // 2. JDK 動態代理:包含額外功能
    InvocationHandler handler = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("---- 方法執行前列印 ----");
            // 執行原始方法
            Object result = method.invoke(calculateService, args);
            System.out.println("---- 方法執行後列印 ----");
            return result;
        }
    };
    // 3. 代理類
    CalculateService calService = (CalculateService) Proxy.
            newProxyInstance(CalculateService.class.getClassLoader(),
                    calculateService.getClass().getInterfaces(),
                    handler);
    // 4. 執行方法
    int result = calService.add(1, 2);
    System.out.println("result:" + result);
}

測試結果:

9.2.2 Cglib 動態代理

CGlib 創建動態代理的原理:原始類作為父類,代理類作為子類,通過繼承關係創建代理類。

代碼格式:

Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(classLoader);
enhancer.setSuperclass(calculateService);
enhancer.setCallback(interceptor);

講解:

(1)classLoader:類加載器(了解即可)

(2)Superclass:父類,就是原始類

(3)interceptor:額外功能

MethodInterceptor interceptor = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("---- 方法執行前列印 ----");
            // 執行原始方法
            Object result = method.invoke(calculateService, args);
            System.out.println("---- 方法執行後列印 ----");
            return result;
            }
        };

完整代碼:

 @Test
    public void testCglibProxy() {
        // 1. 原始對象
        CalculateService calculateService = new CalculateServiceImpl();

        // 2. Cglib 動態代理:包含額外功能
        MethodInterceptor interceptor = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("---- 方法執行前列印 ----");
                // 執行原始方法
                Object result = method.invoke(calculateService, args);
                System.out.println("---- 方法執行後列印 ----");
                return result;
            }
        };

        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(CalculateService.class.getClassLoader());
        enhancer.setSuperclass(calculateService.getClass());
        enhancer.setCallback(interceptor);

        // 3. 創建代理類
        CalculateService calService = (CalculateService)enhancer.create();
        // 4. 執行方法
        int result = calService.add(3, 4);
        System.out.println("result:" + result);
    }

執行結果:

9.2.3 Spring 如何創建代理對象?

Spring 是如何為原始對象創建目標對象的呢?是通過 BeanPostProcessor。

前面我們講過 BeanPostProcessor 可以對對象進行二次加工,所以可以用來創建代理對象。

Spring 創建代理對象的流程:

  1. 實現 BeanPostProcessor 接口
/**
 * @Desc: 後置bean創建代理對象
 * @Author: 知否技術
 * @date: 上午11:59 2022/5/5
 */
public class ProxyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        InvocationHandler handler = (proxy, method, args) -> {
            System.out.println("--- 方法執行前列印6666666---");
            Object ret = method.invoke(bean, args);
            System.out.println("--- 方法執行後列印7777777---");
            return ret;
        };
        return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
    }
}
  1. 註冊 bean
 <bean id="calculateService"  class="com.xxl.service.impl.CalculateServiceImpl" />
<bean id="proxyBeanPostProcessor" class="com.xxl.aop.ProxyBeanPostProcessor"/>
  1. 測試
 @Test
    public void testSpring() {
        // 1、獲取工廠
        ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
        // 2、通過工廠類獲得對象
        CalculateService calculateService = (CalculateService) act.getBean("calculateService");
        // 3.調用方法
        int result = calculateService.sub(7, 2);
        System.out.println("result:" + result);
    }

9.3 基於註解開發 AOP

開發流程:

  1. 開發切面類
@Aspect
public class TestAspect {

    // 前置通知:方法執行前添加額外功能
    @Before("execution(* *(..))")
    public void beforePrint(){
        System.out.println("------before: 方法執行前列印~");
    }
    
    //後置通知: 方法執行後添加額外功能
    @After("execution(* *(..))")
    public void afterPrint(){
        System.out.println("------after: 方法執行前列印~");
    }

    // 環繞通知:方法執行前後添加額外功能
    @Around("execution(* *(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("方法執行前列印~");
        Object result = joinPoint.proceed();
        System.out.println("方法執行後列印~");
        return result;
    }
}
  1. 配置切面類和掃描註解
<bean id="testMyAspect"  class="com.xxl.aop.TestAspect" />
<!-- 掃描 aop 相關註解-->
<aop:aspectj-autoproxy/>
  1. 測試
@Test
  public void testSpring() {
      // 1、獲取工廠
      ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
      // 2、通過工廠類獲得對象
      calculateService calculateService = (CalculateService) act.getBean("calculateService");
      // 3.調用方法
      int result = calculateService.add(100, 1);
      System.out.println("result:" + result);
  }

講解:

1.我們新建了一個 TestMyAspect 類,然後添加 @Aspect 註解,表示這是一個切面類,專門用來完成非業務功能的。

2.在這個類中,我們創建了三個方法,其中 @Before 註解標註的方法表示在目標方法操作前執行。@After 註解標註的方法表示在目標方法操作後執行。@Around 註解標註的方法表示在目標方法操作前後執行。

3.在實際開發中一般使用 @Around 註解標註的方法完成非業務功能。

4.我們新建了這個切面類,但是 spring 不知道啊,所以需要在 Spring 的配置文件中註冊一下 bean。

5.現在 Spring 工廠能夠管理這個類了,但是 Spring 不知道他是切面類啊!所以需要配置一下掃描註解的標籤。

6.然後通過 Spring 獲取創建的類,我們獲取的其實是 Spring 通過後置 Bean 加工後的代理類。

切入點復用

我們可以在切面類中定義⼀個方法,方法上面標註 @Pointcut 註解。 然後就可以重複使用切入點表達式了:

@Aspect
public class TestAspect {

    @Pointcut("execution(* *(..))")
    public void myPointcut() {}
    
    // 環繞通知:方法執行前後添加額外功能
    @Around(value = "myPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("方法執行前列印~");
        Object result = joinPoint.proceed();
        System.out.println("方法執行後列印~");
        return result;
    }
}

9.4 切入點表達式

切入點:額外功能加入的位置。

<aop:pointcut id="pc" expression="execution(* * (..))"/>
複製代碼
  • execution():切入點函數
  • (* * (..)):切入點表達式
public int add(int a, int b)
   *        * (..)

第一個 * 表示方法的修飾符和返回值 第二個 * 是方法名 .. 表示方法中的參數

1.(包.類.方法)切入點:

修飾符-返回值  包.類.方法(參數)

expression="execution(* com.xxl.service.caculateServiceImpl.add(..))"

2.指定切入點為某個包下的所有類中的所有方法:

修飾符-返回值  包.類.方法(參數)

expression="execution(* com.xxl.service.*.*(..))"

3.@annotation

作用:用於匹配當前執行方法持有指定註解的方法,並為之加入額外的功能。

例如我們自定義了一個註解:NoToken

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoToken {
}

方法中添加自定義註解:

@Override
@NoToken
public int add(int a, int b) {
    int result = a + b;
    System.out.println("加法操作。。。");
    return result;
}

然後我們要為包含 NoToken 註解的方法加入額外功能:

@Aspect
public class TestAspect {
   // 環繞通知:方法執行前後添加額外功能
    @Around("@annotation(com.xxl.annotion.NoToken)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("包含 NoToken 註解--------------");
        Object result = joinPoint.proceed();
        return result;
    }
}

測試:

@Test
public void testSpring() {
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // 2、通過工廠類獲得對象
    CalculateService calculateService = (CalculateService) act.getBean("calculateService");
    // 3.調用方法
    int result1 = calculateService.add(99, 1);
    System.out.println("-----------------------");
    int result2 = calculateService.sub(99, 1);
    System.out.println("result1:" + result1);
    System.out.println("result2:" + result2);
}

10. Spring 相關註解

在講註解之前我們先看一下啥是註解。

代碼格式:@英文單詞,例如:

作用位置:常用在類上或者方法上

用處:簡化代碼、完成某些功能

所以 Spring 引入註解也是為了簡化我們的代碼,通過使用簡單的註解完成一些功能。

10.1 創建對象相關註解

我們前面在學 IOC 的時候知道如果想讓 Spring 創建對象,必須要在配置文件中寫 bean 標籤。

<bean id="calculateService"  class="com.xxl.service.impl.CalculateServiceImpl" />
<bean id="proxyBeanPostProcessor" class="com.xxl.aop.ProxyBeanPostProcessor"/>
<bean id="testMyAspect"  class="com.xxl.aop.TestAspect" />
......

可是如果想讓 Spring 管理一堆對象,我們就要寫一堆 bean 標籤。所以 Spring 為了簡化代碼,提供了一些與創建對象相關的註解。

10.1.1 @Component

作用:替換 bean 標籤,用來創建對象。就是在類上面加了這個註解,就不用在配置文件上寫 bean 標籤了。

位置:類上面

id 屬性:默認首單詞首字母小寫。

// id 屬性:默認首單詞首字母小寫。
@Component("user")
public class User{

}

10.1.2 @Component 衍生註解

我們在開發程序的時候一般會將程序分層,例如分為控制層(controller),業務層(service),持久層(dao)。

但是 @Component 註解並不能區分這些類屬於那些層,所以 Spring 提供了以下衍生註解:

1.@Controller:表示創建控制器對象

@Controller
public class UserController {
    
}

2.@Service:表示創建業務層對象

@Service
public class UserServiceImpl implements UserService {

}

3.@Repository:表示創建持久層對象

@Repository
public class UserDaoImpl implements UserDao {

}

這三個註解的作用和 @Component 的作用一樣,都是用來創建對象。

10.1.3 @Scope

我們知道 Spring 工廠創建的對象默認都是單例的,也就是 bean 標籤中 scope 屬性默認是 singleton。

@Scope 註解可以用來修改創建對象的 scope 屬性。

默認:也就是說你不寫 @Scope 註解,默認就是 singleton,所以可以省略。

@Component
// 可以省略不寫
@Scope("singleton")
public class User {
    
}

修改多例:

@Component
@Scope("prototype")
public class User {
    
}

10.1.4 生命周期相關註解

1.@PostConstruct

初始化方法註解,作用在方法上。用來替換 bean 標籤的 init-method 屬性。

例如:

@Component
public class User {
    private String name;

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
    // 初始化方法
    @PostConstruct
    public void init(){
        this.name = "王小波";
    }
}

測試:

@Test
public void testSpring() {
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // 2、通過工廠類獲得對象
    User user = (User) act.getBean("user");
    System.out.println(user);
}

報錯了!這是為什麼?

我們先看報錯內容:

No bean named 'user'

也就是說找不到這個對象。我們雖然加了 @Component 註解,但是 Spring 不知道啊,所以需要在 Spring 的配置文件中配置註解掃描:

<context:component-scan base-package="com.xxl"/>

base-package: 添加註解的類所在的包位置。

配置了註解掃描,當程序啟動的時候 Spring 會先掃描一下相關的註解,這些註解才會生效。

就比如你去快遞驛站拿快遞,你看到了自己的快遞然後對快遞小哥說:"這我的快遞我拿走了啊。"但是人家小哥無法確認是你的啊!所以他需要用掃碼槍掃一下才能出貨!

我們配置註解掃描之後再次測試:

2.@PreDestory(了解即可)

銷毀方法註解,作用在方法上。用來替換 bean 標籤的 destory-method 屬性。

10.2 注入相關註解

10.2.1 @Autowired

我們之前學 DI 的時候知道:注入就是賦值。

@Autowired 主要是為自定義的類型賦值,例如 service、dao 層的各種類。

@Controller
public class UserController {

    @Autowired
    private UserService userService;
}
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
}

@Autowired 是基於類型進行注入,所注入對象的類型必須和目標 變量類型相同或者是他的子類、實現類。

如果想基於名字注入,可以和 @Qualifier 註解連用:

@Autowired
@Qualifier("orderDAOImpl")
private OrderDAO orderDAO;

10.2.2 @Resource

@Resource 註解是 JAVAEE 規範中提供的註解,他和 @Autowired 註解的作用一樣, 但是他是基於名字進行注入:

@Resource("orderDAOImpl")
private OrderDAO orderDAO;

在實際開發中,用 @Autowired 註解比較多一點。

10.2.3 案例

Product 類

@Component
public class Product {

    private  String productName;

    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }

    @Override
    public String toString() {
        return "Product{" +
                "productName='" + productName + '\'' +
                '}';
    }

    @PostConstruct
    public void init(){
        this.productName = "西瓜";
    }
}

User 類:

@Component
public class User {
    private String name;

    @Autowired
    private Product product;

    public String getName() {
        return name;
    }

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

    public Product getProduct() {
        return product;
    }

    public void setProduct(Product product) {
        this.product = product;
    }

    @PostConstruct
    public void init(){
        this.name = "小明";
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", product=" + product +
                '}';
    }
}

測試:

@Test
public void testSpring() {
    // 1、獲取工廠
    ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
    // 2、通過工廠類獲得對象
    User user = (User) act.getBean("user");
    System.out.println(user);
}

10.3 Spring 配置文件相關註解

10.3.1 @Configuration

@Configuration 註解用於替換 xml 配置文件。

@Configuration
public class SpringConfig {
    
}

意思就是說你在一個類上面加一個 @Configuration 註解,這個類就可以看成 Spring 的配置類,你就不用再寫 xml 文件了。

我們之前是根據 xml 文件創建 Spring 的工廠,那怎樣根據配置類創建工廠呢?

有兩種方式:

方式一:根據類.class

ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

方式二:根據配置類所在的路徑

ApplicationContext ctx = new AnnotationConfigApplicationContext("com.xxl");

10.3.2 @Bean

@Bean 註解也是用來創建對象的,相當於spring 配置文件中的 bean 標籤。

@Configuration
public class SpringConfig {

    @Bean
    public Product getProduct(){
        return new Product();
    }
}

自定義 id 值:

@Configuration
public class SpringConfig {

    @Bean("product")
    public Product getProduct(){
        return new Product();
    }
}

不過在實際開發中我們一般會用 @Bean 註解創建一些複雜的對象,例如 Redis、MQ 等一些組件對象。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

10.3.3 @ComponentScan

@ComponentScan 註解相當於 xml 配置文件中的註解掃描標籤:

<context:component-scan base-package="com.xxl"/>

作用:用來掃描@Component 等相關註解

屬性:

basePackages:註解所在的包路徑

例如:

@Configuration
@ComponentScan(basePackages = "com.xxl")
public class SpringConfig {
}

11. 註解小案例

1.User 類

@Component
public class User {
    private String name;

    @Autowired
    private Product product;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Product getProduct() {
        return product;
    }
    public void setProduct(Product product) {
        this.product = product;
    }

    @PostConstruct
    public void init(){
        this.name = "渣渣輝";
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", product=" + product +
                '}';
    }
}

2.Product 類

@Component
public class Product {
    private  String productName;
    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }
    @Override
    public String toString() {
        return "Product{" +
                "productName='" + productName + '\'' +
                '}';
    }
}

3.配置類:

@Configuration
@ComponentScan(basePackages = "com.xxl")
public class SpringConfig {

    @Bean
    public Product product() {
        Product product = new Product();
        product.setProductName("草莓味的番茄");
        return product;
    }
}

4.測試

@Test
public void testSpring() {
    // 1、獲取工廠
    ApplicationContext act = new AnnotationConfigApplicationContext(SpringConfig.class);
    // 2、通過工廠類獲得對象
    User user = (User) act.getBean("user");
    System.out.println(user);
}

作者:知否技術
原文連結:https://juejin.cn/post/7095532056632885284

關鍵字: