文章已託管到GitHub,你們能夠去GitHub查看閱讀,歡迎老闆們前來Star! 搜索關注微信公衆號 【碼出Offer】 領取各類學習資料!java
AOP(Aspect Oriented Programming),即面向切面編程,利用一種稱爲"橫切"的技術,剖開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減小系統的重複代碼,下降模塊之間的耦合度,並有利於將來的可操做性和可維護性。git
應用場景: 如日誌記錄、審計、聲明式事務、安全性和緩存等。github
爲了更好的理解AOP,滲透面向切面編程的思想。我這裏舉一個開發中很常見的例子。打印日誌spring
首先,咱們要先理解什麼是日誌。express
日誌: 日誌是一種能夠追蹤某些軟件運行時所發生事件的方法,軟件開發人員能夠向他們的代碼中調用日誌記錄相關的方法來代表發生了某些事情。一個事件能夠用一個可包含可選變量數據的消息來描述,此外,事件也有重要性的概念,這個重要性也能夠被稱爲嚴重性級別(level)。開發者能夠經過區分嚴重性級別來分析出想要的信息。編程
瞭解了什麼是日誌,那就要知道怎麼打印日誌,在哪裏打印日誌。打印日誌,是引入依賴,使用日誌工具來實現日誌嚴重性級別和日誌信息的打印。至於在哪裏打印日誌,固然是在咱們項目代碼中的關鍵位置了。設計模式
這裏咱們舉一個例子在某一段代碼的先後使用,有A、B、C三個方法,可是要在調用每個方法以前,要求打印一行日誌「某方法被調用了!」,在調用每一個方法以後,也要求打印日誌「某方法被調用完畢!」。緩存
通常人會在每個方法的開始和結尾部分都會添加一句日誌打印吧,這樣作若是方法多了,就會有不少重複的代碼,顯得很麻煩,這時候有人會想到,爲何不把打印日誌這個功能封裝一下,而後讓它能在指定的地方(好比執行方法前,或者執行方法後)自動的去調用呢?若是能夠的話,業務功能代碼中就不會摻雜這一下其餘的代碼,因此AOP就是作了這一類的工做的。安全
其工做原理爲JDK動態代理和CGLIB動態代理,這裏就先不展開動態代理的知識了!仍是先看AOP吧!微信
AOP做用: Spring的AOP編程便是經過動態代理類爲原始類的方法添加輔助功能。
AOP術語 | 描述 |
---|---|
鏈接點(Joinpoint) | 鏈接點是程序類中客觀存在的方法,可被Spring攔截並切入內容 |
切入點(Pointcut) | 被Spring切入鏈接點 |
通知、加強(Advice) | 能夠爲切入點添加額外功能,分爲:前置通知、後置通知、異常通知、環繞通知等。 |
目標對象(Target) | 代理的目標對象 |
引介(Introduction) | 一種特殊的加強,可在運行期爲類動態添加Field和Method |
織入(Weaving) | 把通知應用到具體的類,進而建立新的代理類的過程 |
代理(Proxy) | 被AOP織入通知後,產生的結果類 |
切面(Aspect) | 由切點和通知組成,將橫切邏輯織入切面所指定的鏈接點中 |
簡單來講,就是容許你使用通知、加強的地方。就好比在方法先後打印日誌同樣,咱們能夠在一段代碼的先後作操做,能夠在一段代碼前作操做,能夠在一段代碼後作操做,能夠在一段代碼拋異常以後作操做。因此,在這裏這些能夠操做的一行行代碼(方法等等)都是一個個的鏈接點。
把一個個方法等代碼看做鏈接點,那咱們從哪一個位置打印日誌(加強操做)呢,而咱們挑選出須要打印日誌的位置(也就是鏈接點的周圍),就被稱爲切入點。
關於加強,在上面我已經說到過了,經過在切入點作的操做叫作加強,好比咱們要打印日誌的話,日誌就是一個加強功能操做。
目標對象,簡單來講是要被加強的對象。
容許咱們向現有的類添加新方法屬性。這不就是把切面(也就是加強定義的新方法屬性)用到目標對象中
把加強應用到具體的目標對象中,進而建立新的代理類的過程
代理就像咱們買房子的中介同樣,也就是被AOP織入後產生的代理對象(中介對象),經過代理對象能夠實現對咱們的目標對象加強
切面是通知(加強)和切入點的結合。通知說明了幹什麼和何時幹,而切入點說明了在哪幹,這就是一個完整的切面定義。
引入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>
複製代碼
咱們須要在覈心配置文件的頭文件中添加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>
複製代碼
模擬建立一個原始類
public interface UserService {
public void save();
}
public class UserServiceImpl implements UserService {
public void save() {
System.out.println("save method executed...");
}
}
複製代碼
定義通知類(添加額外功能作加強)
public class MyAdvice implements MethodBeforeAdvice { //實現前置通知接口
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("before advice executed...");
}
}
複製代碼
配置bean對象
<!--原始對象-->
<bean id="us" class="com.mylifes1110.service.impl.UserServiceImpl" />
<!--輔助(加強)對象-->
<bean id="myAdvice" class="com.mylifes1110.advice.MyAdvice" />
複製代碼
定義切入點(PointCut)並造成切面(Aspect)
<aop:config>
<!--切點-->
<aop:pointcut id="myPointCut" expression="execution(* save())" />
<!--組裝切面-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="myPointCut" />
</aop:config>
複製代碼
使用的前置通知,結果加強的打印語句
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..*.*(..))" />
複製代碼
將核心功能與輔助功能(事務、日誌、性能監控代碼)分離,達到核心業務功能更純粹、輔助業務功能可複用。
功能分離 |
---|
![]() |
經過代理類的對象,爲原始類的對象(目標類的對象)添加輔助功能,更容易更換代理實現類、利於維護。
場景模擬: 咱們在租賃房子須要走以下流程:
可若是你是房東,生活中還有其餘的雜事,怎麼辦呢?那你是否是能夠把不重要不核心的環節交給中介(代理)去作呢?好比:發佈租房信息和帶租戶看房。這兩件事情交給中介去作就行了,咱們本身處理本身的事情,並且中間聯繫好租戶咱們走比較重要的流程就能夠,好比籤合同、收房租。
建立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("收款");
}
}
複製代碼
以下是靜態代理設計模式解決代理問題
- 靜態代理流程,建立一個代理類並實現相同接口,建立實現類對象,在代理類中添加輔助功能並調用實現類對象核心方法,使得輔助功能和核心方法一塊兒觸發,完成代理
- 靜態代理的問題
- 隨着輔助功能的數量增長,代理類也會增長,致使代理類數量過多,不利於項目的管理。
- 多個代理類的輔助功能代碼冗餘,修改時,維護性差。
靜態代理 |
---|
![]() |
建立靜態代理類
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();
}
/** * 結果: * * 發佈消息 * 看房子 * 籤合同 * 收款 */
}
複製代碼
spring底層,包含了jdk代理和cglib代理兩種動態代理生成機制。
基本規則是:目標業務類若是有接口則用JDK代理,沒有接口則用CGLib代理。若是配置true:<aop:config proxy-target-class="true">,則用CGLIB代理
class DefaultAopProxyFactory{
// 該方法中明肯定義了 JDK代理和CGLib代理的選取規則
// 基本規則是:目標業務類若是有接口則用JDK代理,沒有接口則用CGLib代理
public AopProxy createAopProxy(){...}
}
複製代碼
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();
/** * 結果: * * 發佈消息 * 看房子 * 籤合同 * 收款 */
}
}
複製代碼
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();
/** * 結果: * * 發佈消息 * 看房子 * 籤合同 * 收款 */
}
}
複製代碼
- spring中定義了不少後處理器;
- 每一個bean在建立完成以前 ,都會有一個後處理過程,即再加工,對bean作出相關改變和調整;
- spring-AOP中,就有一個專門的後處理器,負責經過原始業務組件(Service),再加工獲得一個代理組件。
經常使用後處理器 |
---|
![]() |
/** * 定義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() 最終的返回值
}
}
複製代碼
<!-- 配置後處理器,將對工廠中全部的bean聲明週期進行干預 -->
<bean class="com.mylifes1110.beanpostprocessor.MyBeanPostProcessor"></bean>
複製代碼
建立Bean對象 -> 構造方法 -> Setter方法注入屬性、知足依賴 -> 後處理器前置過程 -> init初始化 -> 後處理器後置過程 -> 構建完成 -> 銷燬
// AbstractAutoProxyCreator是 AspectJAwareAdvisorAutoProxyCreator的父類
// 該後處理器類中的 wrapIfNecessary方法即動態代理生成過程
AbstractAutoProxyCreator#postProcessAfterInitialization(Object bean, String beanName){
if (!this.earlyProxyReferences.contains(cacheKey)) {
// 開始動態定製代理
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
複製代碼