仔細想想SpringAOP也不難嘛,面試沒有必要慌


文章已托管到GitHub,大家可以去GitHub查看閱讀,歡迎老板們前來Star!

搜索關注微信公眾號 碼出Offer 領取各種學習資料!

LOGO
LOGO

SpringAOP


一、什么是AOP

AOP(Aspect Oriented Programming),即面向切面編程,利用一種稱為"橫切"的技術,剖開封裝的對象內部,並將那些影響了多個類的公共行為封裝到一個可重用模塊,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重復代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。

應用場景: 如日志記錄、審計、聲明式事務、安全性和緩存等。

二、場景分析

為了更好的理解AOP,滲透面向切面編程的思想。我這里舉一個開發中很常見的例子。打印日志

首先,我們要先理解什么是日志。

日志: 日志是一種可以追蹤某些軟件運行時所發生事件的方法,軟件開發人員可以向他們的代碼中調用日志記錄相關的方法來表明發生了某些事情。一個事件可以用一個可包含可選變量數據的消息來描述,此外,事件也有重要性的概念,這個重要性也可以被稱為嚴重性級別(level)。開發者可以通過區分嚴重性級別來分析出想要的信息。

了解了什么是日志,那就要知道怎么打印日志,在哪里打印日志。打印日志,是引入依賴,使用日志工具來實現日志嚴重性級別和日志信息的打印。至於在哪里打印日志,當然是在我們項目代碼中的關鍵位置了。

這里我們舉一個例子在某一段代碼的前后使用,有A、B、C三個方法,但是要在調用每一個方法之前,要求打印一行日志“某方法被調用了!”,在調用每個方法之后,也要求打印日志“某方法被調用完畢!”。

一般人會在每一個方法的開始和結尾部分都會添加一句日志打印吧,這樣做如果方法多了,就會有很多重復的代碼,顯得很麻煩,這時候有人會想到,為什么不把打印日志這個功能封裝一下,然后讓它能在指定的地方(比如執行方法前,或者執行方法后)自動的去調用呢?如果可以的話,業務功能代碼中就不會摻雜這一下其他的代碼,所以AOP就是做了這一類的工作的。

其工作原理為JDK動態代理和CGLIB動態代理,這里就先不展開動態代理的知識了!還是先看AOP吧!

三、AOP術語

AOP作用: Spring的AOP編程即是通過動態代理類為原始類的方法添加輔助功能。

AOP術語 描述
連接點(Joinpoint) 連接點是程序類中客觀存在的方法,可被Spring攔截並切入內容
切入點(Pointcut) 被Spring切入連接點
通知、增強(Advice) 可以為切入點添加額外功能,分為:前置通知、后置通知、異常通知、環繞通知等。
目標對象(Target) 代理的目標對象
引介(Introduction) 一種特殊的增強,可在運行期為類動態添加Field和Method
織入(Weaving) 把通知應用到具體的類,進而創建新的代理類的過程
代理(Proxy) 被AOP織入通知后,產生的結果類
切面(Aspect) 由切點和通知組成,將橫切邏輯織入切面所指定的連接點中

四、AOP術語解析

3.1 連接點

簡單來說,就是允許你使用通知、增強的地方。就比如在方法前后打印日志一樣,我們可以在一段代碼的前后做操作,可以在一段代碼前做操作,可以在一段代碼后做操作,可以在一段代碼拋異常之后做操作。所以,在這里這些可以操作的一行行代碼(方法等等)都是一個個的連接點。

3.2 切入點

把一個個方法等代碼看作連接點,那我們從哪個位置打印日志(增強操作)呢,而我們挑選出需要打印日志的位置(也就是連接點的周圍),就被稱為切入點。

3.3 增強、通知

關於增強,在上面我已經說到過了,通過在切入點做的操作叫做增強,比如我們要打印日志的話,日志就是一個增強功能操作。

3.4 目標對象

目標對象,簡單來說是要被增強的對象。

3.5 引介

允許我們向現有的類添加新方法屬性。這不就是把切面(也就是增強定義的新方法屬性)用到目標對象中

3.6 織入

把增強應用到具體的目標對象中,進而創建新的代理類的過程

3.7 代理

代理就像我們買房子的中介一樣,也就是被AOP織入后產生的代理對象(中介對象),通過代理對象可以實現對我們的目標對象增強

3.8 切面

切面是通知(增強)和切入點的結合。通知說明了干什么和什么時候干,而切入點說明了在哪干,這就是一個完整的切面定義。

五、SpringAOP開發步驟

5.1 pom.xml文件引入依賴

引入Spring核心依賴(spring-context)和SpringAOP依賴(spring-aspects)

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>

5.2 創建spring-context.xml文件並添加schema

我們需要在核心配置文件的頭文件中添加aop和context的Schema

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       "
>
</beans>

5.3 定義原始類

模擬創建一個原始類

public interface UserService {
    public void save();
}

public class UserServiceImpl implements UserService {
    public void save() {
        System.out.println("save method executed...");
    }
}

5.4 定義通過類

定義通知類(添加額外功能做增強)

public class MyAdvice implements MethodBeforeAdvice //實現前置通知接口
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("before advice executed...");
    }
}

5.5 定義bean

配置bean對象

<!--原始對象-->
<bean id="us" class="com.mylifes1110.service.impl.UserServiceImpl" />

<!--輔助(增強)對象-->
<bean id="myAdvice" class="com.mylifes1110.advice.MyAdvice" />

5.6 定義切入點形成切面

定義切入點(PointCut)並形成切面(Aspect)

<aop:config>
    <!--切點-->
    <aop:pointcut id="myPointCut" expression="execution(* save())" />
    <!--組裝切面-->
    <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointCut" />
</aop:config>

5.7 增強結果

使用的前置通知,結果增強的打印語句before advice executed...會在save()方法的前面打印save method executed...

六、通知

定義通知類,達到通知(增強)效果。實現不同的接口並覆蓋方法來達到不同的通知效果

通知名稱 接口 描述
前置通知 MethodBeforeAdvice接口 在目標對象的前面做增強
后置通知 AfterAdvice接口 注意:此接口內方法為空,后置默認使用第三種即可
后置通知 AfterReturningAdvice接口 在目標對象的后面做增強
異常通知 ThrowsAdvice 在目標對象發生異常后做增強
環繞通知 MethodInterceptor 在目標對象的前后做增強

七、通配切入點

根據表達式通配切入點

通配表達式順序: 返回值類型 全類名.方法名(形參)

注意: 可以用..來實現通配形參列表,可以使用*來通配方法名或返回值類型

<!-- public int com.mylifes1110.service.UserServiceImpl.queryUser(int,String,com.entity.User) -->
<!--匹配參數-->
<aop:pointcut id="myPointCut" expression="execution(* *(com.mylifes1110.bean.User))" />
<!--匹配方法名(無參)-->
<aop:pointcut id="myPointCut" expression="execution(* save())" />
<!--匹配方法名(任意參數)-->
<aop:pointcut id="myPointCut" expression="execution(* save(..))" />
<!--匹配返回值類型-->
<aop:pointcut id="myPointCut" expression="execution(com.mylifes1110.bean.User *(..))" />
<!--匹配類名-->
<aop:pointcut id="myPointCut" expression="execution(* com.mylifes1110.bean.UserServiceImpl.*(..))" />
<!--匹配包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.mylifes1110.bean.*.*(..))" />
<!--匹配包名、以及子包名-->
<aop:pointcut id="myPointCut" expression="execution(* com.mylifes1110..*.*(..))" />

八、代理模式

8.1 代理模式

將核心功能與輔助功能(事務、日志、性能監控代碼)分離,達到核心業務功能更純粹、輔助業務功能可復用。

功能分離
image-20190420002535800
image-20190420002535800

8.2 代理模式應用場景模擬

通過代理類的對象,為原始類的對象(目標類的對象)添加輔助功能,更容易更換代理實現類、利於維護。

場景模擬: 我們在租賃房子需要走如下流程:

  1. 發布租房信息
  2. 帶租戶看房
  3. 簽合同
  4. 收房租

可如果你是房東,生活中還有其他的瑣事,怎么辦呢?那你是不是可以把不重要不核心的環節交給中介(代理)去做呢?比如:發布租房信息和帶租戶看房。這兩件事情交給中介去做就好了,我們自己處理自己的事情,而且中間聯系好租戶我們走比較重要的流程就可以,比如簽合同、收房租。

8.3 創建Service接口和實現類

創建Service接口和實現類來模擬動態代理的應用場景

package com.mylifes1110.service;

public interface LandlordService {
    void rent();
}

package com.mylifes1110.service.impl;

import com.mylifes1110.service.LandlordService;

public class LandlordServiceImpl implements LandlordService {
    @Override
    public void rent() {
        System.out.println("簽合同");
        System.out.println("收款");
    }
}

8.4 靜態代理

如下是靜態代理設計模式解決代理問題

  • 靜態代理流程,創建一個代理類並實現相同接口,創建實現類對象,在代理類中添加輔助功能並調用實現類對象核心方法,使得輔助功能和核心方法一起觸發,完成代理
  • 靜態代理的問題
  • 隨着輔助功能的數量增加,代理類也會增加,導致代理類數量過多,不利於項目的管理。
  • 多個代理類的輔助功能代碼冗余,修改時,維護性差。
靜態代理
image-20190420004330551
image-20190420004330551

創建靜態代理類

package com.mylifes1110.advice1;

import com.mylifes1110.service.LandlordService;
import com.mylifes1110.service.impl.LandlordServiceImpl;

/**
 * @ClassName Proxy
 * @Description 靜態代理類
 * @Author Ziph
 * @Date 2020/7/19
 * @Since 1.8
 * @Version 1.0
 */


public class Proxy implements LandlordService {
    private LandlordService landlordService = new LandlordServiceImpl();

    @Override
    public void rent() {
        // 代理事件
        System.out.println("發布消息");
        System.out.println("看房子");
        // 核心事件
        landlordService.rent();
    }
}

靜態代理實現

package com.mylifes1110.advice1;

import org.junit.Test;

public class ProxyTest {
    /**
     * @MethodName proxyTest
     * @Param []
     * @Description 靜態代理實現
     * @Author Ziph
     * @Date 2020/7/10
     */

    @Test
    public void proxyTest() {
        new Proxy().rent();
    }
    /**
     * 結果:
     * 
     * 發布消息
     * 看房子
     * 簽合同
     * 收款
     */

}

8.5 JDK和CGLIB的選擇

spring底層,包含了jdk代理和cglib代理兩種動態代理生成機制。

基本規則是:目標業務類如果有接口則用JDK代理,沒有接口則用CGLib代理。如果配置true: ,則用CGLIB代理

class DefaultAopProxyFactory{
    // 該方法中明確定義了 JDK代理和CGLib代理的選取規則
    // 基本規則是:目標業務類如果有接口則用JDK代理,沒有接口則用CGLib代理
    public AopProxy createAopProxy(){...}
}

8.6 JDK動態代理

JDK動態代理是JDK底層基於接口實現的,也就是說我們必須通過實現JDK動態代理的接口並覆蓋方法來完成

package com.mylifes1110.advice2;

import com.mylifes1110.service.LandlordService;
import com.mylifes1110.service.impl.LandlordServiceImpl;
import org.junit.Test;

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

public class ProxyTest {
    /**
     * @MethodName proxyTest
     * @Param []
     * @Description JDK動態代理實現
     * @Author Ziph
     * @Date 2020/7/10
     */

    @Test
    public void proxyTest() {
        // 需要使用代理的目標
        LandlordService landlordService = new LandlordServiceImpl();
        // 匿名內部類
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 代理事件
                System.out.println("發布消息");
                System.out.println("看房子");
                return method.invoke(landlordService, args);
            }
        };

        // 動態構建代理類
        LandlordService proxy = (LandlordService) Proxy.newProxyInstance(ProxyTest.class.getClassLoader(),
                landlordService.getClass().getInterfaces(),
                handler);

        proxy.rent();

        /**
         * 結果:
         *
         * 發布消息
         * 看房子
         * 簽合同
         * 收款
         */

    }
}

8.7 CGLIB動態代理

CGLIB動態代理是Spring底層基於繼承父類實現的,也就是說我們必須通過繼承所指定的父類並覆蓋其方法來完成

package com.mylifes1110.advice3;

import com.mylifes1110.service.LandlordService;
import com.mylifes1110.service.impl.LandlordServiceImpl;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;

import java.lang.reflect.Method;

/**
 * @ClassName ProxyTest
 * @Description CGLIB動態代理實現
 * @Author Ziph
 * @Date 2020/7/19
 * @Since 1.8
 * @Version 1.0
 */


public class ProxyTest {
    public static void main(String[] args) {
        final LandlordService landlordService = new LandlordServiceImpl();
        // 創建字節碼增強對象
        Enhancer enhancer = new Enhancer();
        // 設置父類(等價於實現原始類接口)
        enhancer.setSuperclass(landlordService.getClass());
        // 設置回調函數(額外功能代碼)
        enhancer.setCallback(new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                // 代理事件
                System.out.println("發布消息");
                System.out.println("看房子");
                Object ret = method.invoke(landlordService, args);
                return ret;
            }
        });
        // 創建動態代理類
        LandlordService proxy = (LandlordService) enhancer.create();
        proxy.rent();
        /**
         * 結果:
         *
         * 發布消息
         * 看房子
         * 簽合同
         * 收款
         */

    }
}

九、后處理器

9.1 后處理器的了解

  • spring中定義了很多后處理器;
  • 每個bean在創建完成之前 ,都會有一個后處理過程,即再加工,對bean做出相關改變和調整;
  • spring-AOP中,就有一個專門的后處理器,負責通過原始業務組件(Service),再加工得到一個代理組件。
常用后處理器
系統后處理器
系統后處理器

9.2 定義后處理器

/**
 * 定義bean后處理器
 * 作用:在bean的創建之后,進行再加工
 */

public class MyBeanPostProcessor implements BeanPostProcessor{

    /**
     * 在bean的init方法之前執行
     * @param bean  原始的bean對象
     * @param beanName
     * @return
     * @throws BeansException
     */

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("后處理器 在init之前執行~~~" + bean.getClass());
        return bean;
    }
    /**
     * 在bean的init方法之后執行
     * @param bean  postProcessBeforeInitialization返回的bean
     * @param beanName
     * @return
     * @throws BeansException
     */

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("后處理器 在init之后執行~~~" + bean.getClass());
        return bean;// 此處的返回是 getBean() 最終的返回值
    }
}

9.3 配置后處理器

<!-- 配置后處理器,將對工廠中所有的bean聲明周期進行干預 -->
<bean class="com.mylifes1110.beanpostprocessor.MyBeanPostProcessor"></bean>

9.4 Bean的生命周期

創建Bean對象 -> 構造方法 -> Setter方法注入屬性、滿足依賴 -> 后處理器前置過程 -> init初始化 -> 后處理器后置過程 -> 構建完成 -> 銷毀

9.5 動態代理源碼(了解)

// AbstractAutoProxyCreator是 AspectJAwareAdvisorAutoProxyCreator的父類
// 該后處理器類中的 wrapIfNecessary方法即動態代理生成過程
AbstractAutoProxyCreator#postProcessAfterInitialization(Object bean, String beanName){
    if (!this.earlyProxyReferences.contains(cacheKey)) {
        // 開始動態定制代理
        return wrapIfNecessary(bean, beanName, cacheKey);
       }
}

記得關注我哦!拜拜,下期見!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM