SpringMVC 乾貨系列:從零搭建 SpringMVC+mybatis(四):Spring 兩大核心之 AOP 學習 | 掘金技術徵文

本來地址:SpringMVC乾貨系列:從零搭建SpringMVC+mybatis(四):Spring兩大核心之AOP學習
博客地址:tengj.top/javascript

前言

上一篇咱們介紹了Spring的核心概念DI,DI有助與應用對象之間的解耦。今天咱們就來介紹下另外一個很是核心的概念,面向切面編程AOP。php

正文

在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點(cross-cutting concern)。一般來說,這些橫切關注點從概念上是與應用的業務邏輯相分離的。好比:日誌、聲明式事物、安全和緩存。這些東西都不是咱們平時寫代碼的核心功能,但許多地方都要用到。java

把這些橫切關注點與業務相分離正是面向切面編程(AOP)索要解決的問題。web

簡單的說就是把這些許多地方都要用到,但又不是核心業務的功能,單獨剝離出來封裝,經過配置指定要切入到指定的方法中去。spring

什麼是面向切面編程


如上圖所示,這就是橫切關注點的概念,水平的是核心業務,這些切入的箭頭就是咱們的橫切關注點。
橫切關注點能夠被模塊化爲特殊的類,這些類被稱爲切面(aspect)。這樣作有兩個好處:

  • 首先,如今每一個關注點都集中於一個地方,而不是分割到多處代碼中
  • 其次,服務模塊更簡潔,由於它們只包含主要關注點(或核心功能)的代碼,而次要關注點的代碼被轉移到切面中了。

定義AOP術語

爲了理解AOP,咱們必須先了解AOP的相關術語,很簡單不難:express

通知(Advice)
在AOP中,切面的工做被稱爲通知。通知定義了切面「是什麼」以及「什麼時候」使用。除了描述切面要完成的工做,通知還解決了什麼時候執行這個工做的問題。編程

Spring切面能夠應用5種類型的通知:spring-mvc

  • 前置通知(Before):在目標方法被調用以前調用通知功能
  • 後置通知(After):在目標方法完成以後調用通知,此時不會關心方法的輸出是什麼
  • 返回通知(After-returning):在目標方法成功執行以後調用通知
  • 異常通知(After-throwing):在目標方法拋出異常後調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用以前和調用以後執行自定義的行爲

鏈接點(Join point)
鏈接點是在應用執行過程當中可以插入切面的一個點。這個點能夠是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼能夠利用這些點插入到應用的正常流程之中,並添加行爲。緩存

切點(Pointcut):
若是說通知定義了切面「是什麼」和「什麼時候」的話,那麼切點就定義了「何處」。好比我想把日誌引入到某個具體的方法中,這個方法就是所謂的切點。安全

切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的所有內容———他是什麼,在什麼時候和何處完成其功能。

引入(Introduction)
引入容許咱們向現有的類添加新的方法和屬性(Spring提供了一個方法注入的功能)。

織入(Weaving)
把切面應用到目標對象來建立新的代理對象的過程,織入通常發生在以下幾個時機:

  • 編譯時:當一個類文件被編譯時進行織入,這須要特殊的編譯器才能夠作的到,例如AspectJ的織入編譯器
  • 類加載時:使用特殊的ClassLoader在目標類被加載到程序以前加強類的字節代碼
  • 運行時:切面在運行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術

Spring對AOP的支持

建立切入點來定義切面所織入的鏈接點是AOP框架的基本功能。
Spring提供了4種類型的AOP支持:

  • 基於代理的經典Spring AOP
  • 純POJO切面
  • @AspectJ註解驅動的切面
  • 注入式AspectJ切面(使用與Spring各版本)

前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,所以,Spring對AOP的支持侷限於方法攔截。

這裏我不許備介紹經典Spring AOP,由於引入了簡單的聲明式AOP和基於直接的AOP後,Spring經典的AOP看起來就顯得很是笨重和過於複雜。

對於新手入門來講,咱們不須要知道這麼多,在這裏我也只介紹2,3兩種方式,簡單的說就是一個基於xml配置,一個基於註解。

下面就直接開始舉兩個例子分別來介紹下這兩種AOP方式,咱們就拿簡單的日誌來講明。

基於註解的方式

首先基於註解的方式須要引入這些包,對用的pom.xml以下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.8</version>
</dependency>複製代碼

咱們仍是舉前面用到的UserController來講明,下面方法很簡單,執行進入這個方法的時候會打印「進來了」信息,如今我打算給這個方法加日誌,在執行該方法前打印「進來前」,在執行完方法後執行「進來後」。

package com.tengj.demo.controller;

@Controller
@RequestMapping(value="/test")
public class UserController {
    @Autowired
    UserService userService;

    @RequestMapping(value="/view",method = RequestMethod.GET)
    public String index(){
        userService.sayHello("tengj");
        return "index";
    }
}複製代碼

servie層代碼:

package com.tengj.demo.service
public interface UserService {
    public void sayHello(String name);
}複製代碼

servie實現類代碼:

package com.tengj.demo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello(String name) {
        System.out.println("hello,"+name);
    }
}複製代碼

上面方法index()其實就是咱們以前定義的切點,表示在哪裏切入AOP。


如圖所示,咱們使用execution()指示器選擇UserServiceImpl的sayHello方法。方法表達式以「*」號開始,代表了咱們不關心方法返回值的類型。而後,咱們指定了全限定類名和方法名。對於方法參數列表,咱們使用兩個點號(..)代表切點要選擇任意的sayHello()方法,不管該方法的入參是什麼。

接下來咱們要定義個切面,也就是所謂的日誌功能的類。

package com.tengj.demo.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component //注入依賴
@Aspect //該註解標示該類爲切面類
public class LogAspect {
    @Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
    public void logAop(){}

    @Before("logAop() && args(name)")
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    @AfterReturning("logAop()")
    public void logAfterReturning(){
        System.out.println("返回通知AfterReturning");
    }

    @After("logAop() && args(name)")
    public void logAfter(String name){
        System.out.println(name+"後置通知After");
    }

    @AfterThrowing("logAop()")
    public void logAfterThrow(){
        System.out.println("異常通知AfterThrowing");
    }
}複製代碼

上面就是切面類的代碼,很簡單,這裏用到了前面提的通知的幾種類型。
這樣就能實現切入功能了

@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}複製代碼

這裏的@Pointcut註解是爲了定義切面內重用的切點,也就是說把公共的東西抽出來,定義了任意的方法名稱logAop,這樣下面用到的各類類型通知就只要寫成

@Before("logAop() && args(name)")
@AfterReturning("logAop()")
@AfterThrowing("logAop()")複製代碼

這樣既可,不然就要寫成

@Before("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterReturning("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterThrowing("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")複製代碼

你們是否注意到了@Before("logAop() && args(name)")這裏多出來個&& args(name),這個是用來傳遞參數的,定義只要跟sayHello參數名稱同樣就能夠。

若是就此止步的話,LogAspect只會是Spring容器中的一個Bean,即使使用了AspectJ註解,但它並不會被視爲切面,這些註解不會解析,也不會建立將其轉換爲切面的代理。

因此須要在XML裏面配置一下,須要使用Spring aop命名空間中的<aop:aspectj-autoproxy/>元素,簡單以下:

<?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" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" default-lazy-init="true">
    <context:component-scan base-package="com.tengj.demo"/>
    <mvc:resources location="/WEB-INF/pages/" mapping="/pages/**"/>
    <!-- 默認的註解映射的支持 -->
    <mvc:annotation-driven/>
    <!--啓用AspectJ自動代理-->
    <aop:aspectj-autoproxy/>
    <!-- 視圖解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>複製代碼

接着就能夠啓動工程,訪問index這個方法,http://localhost:8080/SpringMVCMybatis/test/view
執行結果:

tengj前置通知Before
hello,tengj
tengj後置通知After
返回通知AfterReturning複製代碼

根據前面學的咱們知道,除了上面提到的通知外,還有一個更強大通知類型,就是環繞通知。能夠自定義咱們須要切入的位置,能夠替代上面提到的全部通知。看例子:

@Around("logAop()")
public void logAround(ProceedingJoinPoint jp){
    try {
        System.out.println("自定義前置通知Before");
        jp.proceed();//將控制權交給被通知的方法,也就是執行sayHello方法
        System.out.println("自定義後置通知After");
    } catch (Throwable throwable) {
        System.out.println("異常處理~");
        throwable.printStackTrace();
    }
}複製代碼

執行結果:

自定義前置通知Before
hello,tengj
自定義後置通知After複製代碼

這裏主要是經過ProceedingJoinPoint這個參數。其中裏面的proceed()方法就是將控制權交給被通知的方法。若是你忘記調用這個方法,那麼你的通知實際上會阻塞對被通知方法的調用。

有意思的是,你能夠不調用proceed()方法,從而阻塞堆被通知方法的訪問,與之相似,你也能夠在通知中對它進行屢次調用。要這樣作的一個場景就是實現重試邏輯,也就是在被通知方法失敗後,進行重複嘗試。

基於XML配置的方式

這裏介紹使用XML配置的方式來實現,在Spring的aop命名空間中,提供了多個元素用來在XML中聲明切面。

AOP配置元素 用 途
<aop:advisor> 定義AOP通知器
<aop:after> 定義AOP後置通知(無論被通知的方法是否執行成功)
<aop:after-returning> 定義AOP返回通知
<aop:after-throwing> 定義AOP異常通知
<aop:around> 定義AOP環繞通知
<aop:aspect> 定義一個切面
<aop:aspectj-autoproxy> 啓用@AspectJ註解驅動的切面
<aop:before> 定義一個AOP前置通知
<aop:config> 頂層的AOP配置元素,大多數的<aop:*>元素必須包含在<aop:config>元素內
<aop:declare-parents> 以透明的方式爲被通知的對象引入額外的接口
<aop:pointcut> 定義一個切點

咱們已經看過了<aop:aspectj-autoproxy/>元素,它可以自動代理AspectJ註解的通知類。aop命名空間的其餘元素可以讓咱們直接在Spring配置中聲明切面,而不須要使用註解。
因此,咱們從新來看看一下這個LogAspect類,此次咱們將它全部的AspectJ註解所有移除掉:

package com.tengj.demo.aspect;

public class LogAspect {
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    public void logAfterReturning(String name){
        System.out.println("返回通知AfterReturning");
    }

    public void logAfter(String name){
        System.out.println(name+"後置通知After");
    }

    public void logAfterThrow(String name){
        System.out.println("異常通知AfterThrowing");
    }
}複製代碼

而後在xml配置文件中使用Spring aop命名空間中的一些元素,詳細基本配置參考上面註解方式中的xml配置,這裏是貼出來關鍵的代碼:

<bean id="logAspect" class="com.tengj.demo.aspect.LogAspect" />
<aop:config>
        <aop:aspect id="log" ref="logAspect">
            <aop:pointcut id="logAop" expression="execution(* com.tengj.demo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
            <aop:before method="logBefore" pointcut-ref="logAop"/>
            <aop:after method="logAfter" pointcut-ref="logAop"/>
            <aop:after-returning method="logAfterReturning" pointcut-ref="logAop"/>
            <aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
            <!--<aop:around method="logAfterThrow" pointcut-ref="logAop"/>-->
        </aop:aspect>
</aop:config>複製代碼

配置也 很好理解

  • xml裏面配置aop,都是放在<aop:config>裏面
  • 而後使用<aop:aspect>一個切面,指向具體的bean類。
  • 使用<aop:pointcut>定義切點,基本跟註解的很像,其中要注意的是xml配置裏面若是要帶參數的,用的再也不是&&,要使用and關鍵字才行(由於在XML中,「&」符號會被解析爲實體的開始)
  • 而後就是使用各類通知標籤了,簡單。

執行效果以下:

tengj前置通知Before
hello,tengj
tengj後置通知After
返回通知AfterReturning複製代碼

環繞通知也很簡單,直接貼代碼:
xml配置:

<aop:around method="logAround" pointcut-ref="logAop"/>複製代碼

切面方法:

public void logAround(ProceedingJoinPoint jp,String name){
    try {
        System.out.println(name+"自定義前置通知Before");
        jp.proceed();
        System.out.println(name+"自定義後置通知After");
    } catch (Throwable throwable) {
        System.out.println("異常處理~");
        throwable.printStackTrace();
    }
}複製代碼

執行結果:

tengj自定義前置通知Before
hello,tengj
tengj自定義後置通知After複製代碼

總結

Spring AOP是Spring學習中最關鍵的,我總結的這2種寫法也是開發中最經常使用的。也不知道你們能不能理解~看得時候若是有不懂的地方能夠提出來,我好修改一下,讓更多的人理解並掌握AOP,但願對你有所幫助。


一直以爲本身寫的不是技術,而是情懷,一篇篇文章是本身這一路走來的痕跡。靠專業技能的成功是最具可複製性的,但願個人這條路能讓你少走彎路,但願我能幫你抹去知識的蒙塵,但願我能幫你理清知識的脈絡,但願將來技術之巔上有你也有我。

訂閱博主微信公衆號:嘟爺java超神學堂(javaLearn)三大好處:

  • 獲取最新博主博客更新信息,首發公衆號
  • 獲取大量視頻,電子書,精品破解軟件資源
  • 能夠跟博主聊天,歡迎程序媛妹妹來撩我

掘金技術徵文第三期:聊聊你的最佳實踐

相關文章
相關標籤/搜索