朱曄和你聊Spring系列S1E6:容易犯錯的Spring AOP

閱讀PDF版本java

標題有點標題黨了,這裏說的容易犯錯不是Spring AOP的錯,是指使用的時候容易犯錯。本文會以一些例子來展開討論AOP的使用以及使用過程當中容易出錯的點。git

幾句話說清楚AOP

有關必要術語:

  1. 切面:Aspect,有的地方也叫作方面。切面=切點+加強,表示咱們在什麼點切入蛋糕,切入蛋糕後咱們以什麼方式來加強這個點。
  2. 切點:Pointcut,相似於查詢表達式,經過在鏈接點運行查詢表達式來尋找匹配切入點,Spring AOP中默認使用AspjectJ查詢表達式。
  3. 加強:Advice,有的地方也叫作通知。定義了切入切點後加強的方式,加強方式有前、後、環繞等等。Spring AOP中把加強定義爲攔截器。
  4. 鏈接點:Join point,蛋糕全部能夠切入的點,對於Spring AOP鏈接點就是方法執行。

有關使用方式:

  1. Spring AOP API:這種方式是Spring AOP實現的基石。最老的使用方式,在Spring 1.2中的時候用這種API的方式定義AOP。
  2. 註解聲明:使用@AspectJ的@Aspect、@Pointcut等註解來定義AOP。如今基本都使用這種方式來定義,也是官方推薦的方式。
  3. 配置文件:相比註解聲明方式,配置方式有兩個缺點,一是定義和實現分離了,二是功能上會比註解聲明弱,沒法實現所有功能。好處麼就是XML在靈活方面會強一點。
  4. 編程動態配置:使用AspectJProxyFactory進行動態配置。能夠做爲註解方式靜態配置的補充。

有關織入方式:

織入說通俗點就是怎麼把加強代碼注入到鏈接點,和被加強的代碼融入到一塊兒。github

  1. 運行時:Spring AOP只支持這種方式。實現上有兩種方式,一是JDK動態代理,經過反射實現,只支持對實現接口的類進行代理,二是CGLIB動態字節碼注入方式實現代理,沒有這個限制。Spring 3.2以後的版本已經包含了CGLIB,會根據須要選擇合適的方式來使用。
  2. 編譯時:在編譯的時候把加強代碼注入進去,經過AspjectJ的ajc編譯器實現。實現上有兩種方式,一種是直接使用ajc編譯全部代碼,還有一種是javac編譯後再進行後處理。
  3. 加載時:在JVM加載類型的時候注入代碼,也叫作LTW。經過啓動程序的時候經過javaagent代理默認的類加載器實現。

使用Spring AOP實現事務的坑

新建一個模塊:web

<?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>me.josephzhu</groupId>
   <artifactId>spring101-aop</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>spring101-aop</name>
   <description></description>

   <parent>
      <groupId>me.josephzhu</groupId>
      <artifactId>spring101</artifactId>
      <version>0.0.1-SNAPSHOT</version>
   </parent>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.9.7</version>
      </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

在這裏咱們引入了jackson,之後咱們會用來作JSON序列化。引入了mybatis啓動器,之後咱們會用mybstis作數據訪問。引入了h2嵌入式數據庫,方便本地測試使用。引入了web啓動器,以後咱們還會來測試一下對web項目的Controller進行注入。
先來定義一下咱們的測試數據類:spring

package me.josephzhu.spring101aop;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyBean {
    private Long id;
    private String name;
    private Integer age;
    private BigDecimal balance;
}

而後,咱們在resources文件夾下建立schema.sql文件來初始化h2數據庫:sql

CREATE TABLE PERSON(
ID BIGINT  PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(255),
AGE SMALLINT,
BALANCE DECIMAL
);

還能夠在resources文件夾下建立data.sql來初始化數據:數據庫

INSERT INTO PERSON (NAME, AGE, BALANCE) VALUES ('zhuye', 35, 1000);

這樣程序啓動後就會有一個PERSON表,表裏有一條ID爲1的記錄。
經過啓動器使用Mybatis很是簡單,無需進行任何配置,建一個Mapper接口:apache

package me.josephzhu.spring101aop;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface DbMapper {
    @Select("SELECT COUNT(0) FROM PERSON")
    int personCount();

    @Insert("INSERT INTO PERSON (NAME, AGE, BALANCE) VALUES ('zhuye', 35, 1000)")
    void personInsertWithoutId();

    @Insert("INSERT INTO PERSON (ID, NAME, AGE, BALANCE) VALUES (1,'zhuye', 35, 1000)")
    void personInsertWithId();

    @Select("SELECT * FROM PERSON")
    List<MyBean> getPersonList();

}

這裏咱們定義了4個方法:編程

  1. 查詢表中記錄數的方法
  2. 查詢表中全部數據的方法
  3. 帶ID字段插入數據的方法,因爲程序啓動的時候已經初始化了一條數據,若是這裏咱們再插入ID爲1的記錄顯然會出錯,用來以後測試事務使用
  4. 不帶ID字段插入數據的方法
    爲了咱們能夠觀察到數據庫鏈接是否被Spring歸入事務管理,咱們在application.properties配置文件中設置mybatis的Spring事務日誌級別爲DEBUG:
logging.level.org.mybatis.spring.transaction=DEBUG

如今咱們來建立服務接口:瀏覽器

package me.josephzhu.spring101aop;

import java.time.Duration;
import java.util.List;

public interface MyService {
    void insertData(boolean success);
    List<MyBean> getData(MyBean myBean, int count, Duration delay);
}

定義了插入數據和查詢數據的兩個方法,下面是實現:

package me.josephzhu.spring101aop;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Service
public class MyServiceImpl implements MyService {

    @Autowired
    private DbMapper dbMapper;

    @Transactional(rollbackFor = Exception.class)
    public void _insertData(boolean success){
        dbMapper.personInsertWithoutId();
        if(!success)
            dbMapper.personInsertWithId();
    }

    @Override
    public void insertData(boolean success) {
        try {
            _insertData(success);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        System.out.println("記錄數:" + dbMapper.personCount());
    }

    @Override
    public List<MyBean> getData(MyBean myBean, int count, Duration delay) {
        try {
            Thread.sleep(delay.toMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return IntStream.rangeClosed(1,count)
                .mapToObj(i->new MyBean((long)i,myBean.getName() + i, myBean.getAge(), myBean.getBalance()))
                .collect(Collectors.toList());
    }
}

getData方法咱們就不細說了,只是實現了休眠而後根據傳入的myBean做爲模板組裝了count條測試數據返回。咱們來重點看一下insertData方法,這就是使用Spring AOP的一個坑了。看上去配置啥的都沒問題,可是_insertData是不能生效自動事務管理的。

咱們知道Spring AOP使用代理目標對象方式實現AOP,在從外部調用insertData方法的時候其實走的是代理,這個時候事務環繞能夠生效,在方法內部咱們經過this引用調用_insertData方法,雖然方法外部咱們設置了Transactional註解,可是因爲走的不是代理調用,Spring AOP天然沒法經過AOP加強爲咱們作事務管理。

咱們來建立主程序測試一下:

package me.josephzhu.spring101aop;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.math.BigDecimal;
import java.time.Duration;

@SpringBootApplication
public class Spring101AopApplication implements CommandLineRunner {

   public static void main(String[] args) {
      SpringApplication.run(Spring101AopApplication.class, args);
   }

   @Autowired
   private MyService myService;

   @Override
   public void run(String... args) throws Exception {
      myService.insertData(true);
      myService.insertData(false);
      System.out.println(myService.getData(new MyBean(0L, "zhuye",35, new BigDecimal("1000")),
            5,
            Duration.ofSeconds(1)));
   }
}

在Runner中,咱們使用true和false調用了兩次insertData方法。後面一次調用確定會失敗,由於_insert方法中會進行重複ID的數據插入。運行程序後獲得以下輸出:

2018-10-07 09:11:44.605  INFO 19380 --- [           main] m.j.s.Spring101AopApplication            : Started Spring101AopApplication in 3.072 seconds (JVM running for 3.74)
2018-10-07 09:11:44.621 DEBUG 19380 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@2126664214 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will not be managed by Spring
2018-10-07 09:11:44.626 DEBUG 19380 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@775174220 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will not be managed by Spring
記錄數:2
2018-10-07 09:11:44.638 DEBUG 19380 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@2084486251 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will not be managed by Spring
2018-10-07 09:11:44.638 DEBUG 19380 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@26418585 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will not be managed by Spring
2018-10-07 09:11:44.642  INFO 19380 --- [           main] o.s.b.f.xml.XmlBeanDefinitionReader      : Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
org.springframework.dao.DuplicateKeyException: 
### Error updating database.  Cause: org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.PERSON(ID)"; SQL statement:
INSERT INTO PERSON (ID, NAME, AGE, BALANCE) VALUES (1,'zhuye', 35, 1000) [23505-197]
2018-10-07 09:11:44.689 DEBUG 19380 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@529949842 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will not be managed by Spring
記錄數:3
[MyBean(id=1, name=zhuye1, age=35, balance=1000), MyBean(id=2, name=zhuye2, age=35, balance=1000), MyBean(id=3, name=zhuye3, age=35, balance=1000), MyBean(id=4, name=zhuye4, age=35, balance=1000), MyBean(id=5, name=zhuye5, age=35, balance=1000)]

從日誌的幾處咱們均可以獲得結論,事務管理沒有生效:

  1. 咱們能夠看到有相似Connection will not be managed by Spring的提示,說明鏈接沒有進入Spring的事務管理。
  2. 程序啓動的時候記錄數爲1,第一次調用insertData方法後記錄數爲2,第二次調用方法若是事務生效方法會回滾記錄數會維持在2,在輸出中咱們看到記錄數最後是3。

那麼,如何解決這個問題呢,有三種方式:

  1. 使用AspjectJ來實現AOP,這種方式是直接修改代碼的,不是走代理實現的,不會有這個問題,下面咱們會詳細說明一下這個過程。
  2. 在代碼中使用AopContext.currentProxy()來得到當前的代理進行_insertData方法調用。這種方式侵入太強,並且須要被代理類意識到本身是經過代理被訪問,顯然不是合適的方式。
  3. 改造代碼,使須要事務代理的方法直接調用,相似:
@Override
@Transactional(rollbackFor = Exception.class)
public void insertData(boolean success) {
    dbMapper.personInsertWithoutId();
    if(!success)
        dbMapper.personInsertWithId();
}

這裏還容易犯錯的地方是,這裏不能對異常進行捕獲,不然Spring事務代理沒法捕獲到異常也就沒法實現回滾。

使用AspectJ靜態織入進行改造

那麼原來這段代碼如何不改造實現事務呢?能夠經過AspjectJ編譯時靜態織入實現。整個配置過程以下:
首先在pom中加入下面的配置:

<build>
       <sourceDirectory>${project.build.directory}/generated-sources/delombok</sourceDirectory>
       <plugins>
      <plugin>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
           <plugin>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-maven-plugin</artifactId>
               <version>1.18.0.0</version>
               <executions>
                   <execution>
                       <phase>generate-sources</phase>
                       <goals>
                           <goal>delombok</goal>
                       </goals>
                   </execution>
               </executions>
               <configuration>
                   <addOutputDirectory>false</addOutputDirectory>
                   <sourceDirectory>src/main/java</sourceDirectory>
               </configuration>
           </plugin>
           <plugin>
               <groupId>org.codehaus.mojo</groupId>
               <artifactId>aspectj-maven-plugin</artifactId>
               <version>1.10</version>
               <configuration>
                   <complianceLevel>1.8</complianceLevel>
                   <source>1.8</source>
                   <aspectLibraries>
                       <aspectLibrary>
                           <groupId>org.springframework</groupId>
                           <artifactId>spring-aspects</artifactId>
                       </aspectLibrary>
                   </aspectLibraries>
               </configuration>
               <executions>
                   <execution>
                       <goals>
                           <goal>compile</goal>
                           <goal>test-compile</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
   </plugins>
</build>

這裏的一個坑是ajc編譯器沒法支持lambok,咱們須要先使用lombok的插件在生成源碼階段對lombok代碼進行預處理,而後咱們再經過aspjectj插件來編譯代碼。Pom文件中還須要加入以下依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

而後須要配置Spring來使用ASPECTJ的加強方式來作事務管理:
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class Spring101AopApplication implements CommandLineRunner {
從新使用maven編譯代碼後能夠看到,相關代碼已經變了樣:

@Transactional(
    rollbackFor = {Exception.class}
)
public void _insertData(boolean success) {
    AnnotationTransactionAspect var10000 = AnnotationTransactionAspect.aspectOf();
    Object[] var3 = new Object[]{this, Conversions.booleanObject(success)};
    var10000.ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(this, new MyServiceImpl$AjcClosure1(var3), ajc$tjp_0);
}

public void insertData(boolean success) {
    try {
        this._insertData(success);
    } catch (Exception var3) {
        var3.printStackTrace();
    }

    System.out.println("記錄數:" + this.dbMapper.personCount());
}

運行程序能夠看到以下日誌:

2018-10-07 09:35:12.360 DEBUG 19459 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1169317628 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will be managed by Spring

並且最後輸出的結果是2,說明第二次插入數據總體回滾了。
若是使用IDEA的話還能夠配置先由javac編譯再由ajc後處理,具體參見IDEA官網這裏不詳述。

使用AOP進行事務後處理

咱們先使用剛纔說的方法3改造一下代碼,使得Spring AOP能夠處理事務(Aspject AOP功能雖然強大可是和Spring結合的很差,因此咱們接下去的測試仍是使用Spring AOP),刪除aspjectj相關依賴,在IDEA配置回javac編譯器從新編譯項目。本節中咱們嘗試創建第一個咱們的切面:

package me.josephzhu.spring101aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Aspect
@Component
@Slf4j
class TransactionalAspect extends TransactionSynchronizationAdapter {

    @Autowired
    private DbMapper dbMapper;

    private ThreadLocal<JoinPoint> joinPoint = new ThreadLocal<>();

    @Before("@within(org.springframework.transaction.annotation.Transactional) || @annotation(org.springframework.transaction.annotation.Transactional)")
    public void registerSynchronization(JoinPoint jp) {
        joinPoint.set(jp);
        TransactionSynchronizationManager.registerSynchronization(this);
    }

    @Override
    public void afterCompletion(int status) {
        log.info(String.format("【%s】【%s】事務提交 %s,目前記錄數:%s",
                joinPoint.get().getSignature().getDeclaringType().toString(),
                joinPoint.get().getSignature().toLongString(),
                status == 0 ? "成功":"失敗",
                dbMapper.personCount()));
        joinPoint.remove();
    }
}

在這裏,咱們的切點是全部標記了@Transactional註解的類以及標記了@Transactional註解的方法,咱們的加強比較簡單,在事務同步管理器註冊一個回調方法,用於事務完成後進行額外的處理。這裏的一個坑是Spring如何實例化切面。經過查文檔或作實驗能夠得知,默認狀況下TranscationalAspect是單例的,在多線程狀況下,可能會有併發,保險起見咱們使用ThreadLocal來存放。運行代碼後能夠看到以下輸出:

2018-10-07 10:01:32.384  INFO 19599 --- [           main] m.j.spring101aop.TransactionalAspect     : 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】事務提交 成功,目前記錄數:2
2018-10-07 10:01:32.385 DEBUG 19599 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1430104337 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will be managed by Spring
2018-10-07 10:01:32.449 DEBUG 19599 --- [           main] o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@1430104337 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] will be managed by Spring
2018-10-07 10:01:32.449  INFO 19599 --- [           main] m.j.spring101aop.TransactionalAspect     : 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】事務提交 失敗,目前記錄數:2

能夠看到Spring AOP作了事務管理,咱們兩次事務提交第一次成功第二次失敗,失敗後記錄數仍是2。這個功能還能夠經過Spring的@TransactionalEventListener註解實現,這裏不詳述。

切換JDK代理和CGLIB代理

咱們如今注入的是接口,咱們知道對於這種狀況Spring AOP應該使用的是JDK代理。可是SpringBoot默認開啓了下面的屬性來全局啓用CGLIB代理:

spring.aop.proxy-target-class=true

咱們嘗試把這個屬性設置成false,而後在剛纔的TransationalAspect中的加強方法設置斷點,能夠看到這是一個ReflectiveMethodInvocation:


把配置改成true從新觀察能夠看到變爲了CglibMethodInvocation:


咱們把開關改成false,而後切換到注入實現,運行程序會獲得以下錯誤提示,意思就是我咱們走JDK代理的話不能注入實現,須要注入接口:

The bean 'myServiceImpl' could not be injected as a 'me.josephzhu.spring101aop.MyServiceImpl' because it is a JDK dynamic proxy that implements:
    me.josephzhu.spring101aop.MyService

咱們修改咱們的MyServiceImpl,去掉實現接口的代碼和@Override註解,使之成爲一個普通的類,從新運行程序能夠看到咱們的代理方式自動降級爲了CGLIB方式(雖然spring.aop.proxy-target-class參數咱們如今設置的是false)。

使用AOP無縫實現日誌+異常+打點

如今咱們來實現一個複雜點的切面的例子。咱們知道,出錯記錄異常信息,對於方法調用記錄打點信息(若是不知道什麼是打點能夠參看《朱曄的互聯網架構實踐心得S1E4:簡單好用的監控六兄弟》),甚至有的時候爲了排查問題須要記錄方法的入參和返回,這三個事情是咱們常常須要作的和業務邏輯無關的事情,咱們能夠嘗試使用AOP的方式一鍵切入這三個事情的實現,在業務代碼無感知的狀況下作好監控和打點。
首先實現咱們的註解,經過這個註解咱們能夠細化控制一些功能:

package me.josephzhu.spring101aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Metrics {
    /**
     * 是否在成功執行方法後打點
     * @return
     */
    boolean recordSuccessMetrics() default true;

    /**
     * 是否在執行方法出錯時打點
     * @return
     */
    boolean recordFailMetrics() default true;

    /**
     * 是否記錄請求參數
     * @return
     */
    boolean logParameters() default true;

    /**
     * 是否記錄返回值
     * @return
     */
    boolean logReturn() default true;

    /**
     * 是否記錄異常
     * @return
     */
    boolean logException() default true;

    /**
     * 是否屏蔽異常返回默認值
     * @return
     */
    boolean ignoreException() default false;
}

下面咱們就來實現這個切面:

package me.josephzhu.spring101aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;

@Aspect
@Component
@Slf4j
public class MetricsAspect {
    private static ObjectMapper objectMapper = new ObjectMapper();

    @Around("@annotation(me.josephzhu.spring101aop.Metrics) || @within(org.springframework.stereotype.Controller)")
    public Object metrics(ProceedingJoinPoint pjp) throws Throwable {
        //1
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Metrics metrics;
        String name;
        if (signature.getDeclaringType().isInterface()) {
            Class implClass = pjp.getTarget().getClass();
            Method method = implClass.getMethod(signature.getName(), signature.getParameterTypes());
            metrics = method.getDeclaredAnnotation(Metrics.class);
            name = String.format("【%s】【%s】", implClass.toString(), method.toString());
        } else {
            metrics = signature.getMethod().getAnnotation(Metrics.class);
            name = String.format("【%s】【%s】", signature.getDeclaringType().toString(), signature.toLongString());
        }
        //2
        if (metrics == null)
            metrics = new Metrics() {
                @Override
                public boolean logException() {
                    return true;
                }

                @Override
                public boolean logParameters() {
                    return true;
                }

                @Override
                public boolean logReturn() {
                    return true;
                }

                @Override
                public boolean recordFailMetrics() {
                    return true;
                }

                @Override
                public boolean recordSuccessMetrics() {
                    return true;
                }

                @Override
                public boolean ignoreException() {
                    return false;
                }

                @Override
                public Class<? extends Annotation> annotationType() {
                    return Metrics.class;
                }
            };
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request != null)
                name += String.format("【%s】", request.getRequestURL().toString());
        }
        //3
        if (metrics.logParameters())
            log.info(String.format("【入參日誌】調用 %s 的參數是:【%s】", name, objectMapper.writeValueAsString(pjp.getArgs())));
        //4
        Object returnValue;
        Instant start = Instant.now();
        try {
            returnValue = pjp.proceed();
            if (metrics.recordSuccessMetrics())
                log.info(String.format("【成功打點】調用 %s 成功,耗時:%s", name, Duration.between(Instant.now(), start).toString()));
        } catch (Exception ex) {
            if (metrics.recordFailMetrics())
                log.info(String.format("【失敗打點】調用 %s 失敗,耗時:%s", name, Duration.between(Instant.now(), start).toString()));

            if (metrics.logException())
                log.error(String.format("【異常日誌】調用 %s 出現異常!", name), ex);

            if (metrics.ignoreException())
                returnValue = getDefaultValue(signature.getReturnType().toString());
            else
                throw ex;
        }
        //5
        if (metrics.logReturn())
            log.info(String.format("【出參日誌】調用 %s 的返回是:【%s】", name, returnValue));
        return returnValue;
    }

    private static Object getDefaultValue(String clazz) {
        if (clazz.equals("boolean")) {
            return false;
        } else if (clazz.equals("char")) {
            return '\u0000';
        } else if (clazz.equals("byte")) {
            return 0;
        } else if (clazz.equals("short")) {
            return 0;
        } else if (clazz.equals("int")) {
            return 0;
        } else if (clazz.equals("long")) {
            return 0L;
        } else if (clazz.equals("flat")) {
            return 0.0F;
        } else if (clazz.equals("double")) {
            return 0.0D;
        } else {
            return null;
        }
    }

}

看上去代碼量不少,其實實現比較簡單:

  1. 最關鍵的切點,咱們在兩個點切入,一是標記了Metrics註解的方法,二是標記了Controller的類(咱們但願實現的目標是對於Controller全部方法默認都加上這個功能,由於這是對外的接口,比較重要)。因此在以後的代碼中,咱們還須要額外對Web程序作一些處理。
  2. 對於@Around咱們的參數是ProceedingJoinPoint不是JoinPoint,由於環繞加強容許咱們執行方法調用。
  3. 第一段代碼,咱們嘗試獲取當前方法的類名和方法名。這裏有一個坑,若是鏈接點是接口的話,@Metrics的定義須要從實現類(也就是代理的Target)上獲取。做爲框架的開發者,咱們須要考慮到各類使用方使用的狀況,若是有遺留的話就會出現BUG。
  4. 第二段代碼,是爲Web項目準備的,若是咱們但願默認爲全部的Controller方法作日誌異常打點處理的話,咱們須要初始化一個@Metrics註解出來,而後對於Web項目咱們能夠從上下文中獲取到額外的一些信息來豐富咱們的日誌。
  5. 第三段代碼,實現的是入參的日誌輸出。
  6. 第四段代碼,實現的是鏈接點方法的執行,以及成功失敗的打點,出現異常的時候還會記錄日誌。這裏咱們經過日誌方式暫時替代了打點的實現,標準的實現是須要把信息提交到相似Graphite這樣的時間序列數據庫或對接SpringBoot Actuator。另外,若是開啓忽略異常的話,咱們須要把結果替換爲返回類型的默認值,而且吃掉異常。
  7. 第五段代碼,實現了返回值的日誌輸出。
    最後,咱們修改一下MyServiceImpl的實現,在insertData和getData兩個方法上加入咱們的@Metrics註解。運行程序能夠看到以下輸出:
2018-10-07 10:47:00.813  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【入參日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 的參數是:【[true]】
2018-10-07 10:47:00.864  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【成功打點】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 成功,耗時:PT-0.048S
2018-10-07 10:47:00.864  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【出參日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 的返回是:【null】
2018-10-07 10:47:00.927  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【入參日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 的參數是:【[false]】
2018-10-07 10:47:01.084  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【失敗打點】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 失敗,耗時:PT-0.156S
2018-10-07 10:47:01.102 ERROR 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【異常日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public void me.josephzhu.spring101aop.MyServiceImpl.insertData(boolean)】 出現異常!
2018-10-07 10:47:01.231  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【入參日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public java.util.List me.josephzhu.spring101aop.MyServiceImpl.getData(me.josephzhu.spring101aop.MyBean,int,java.time.Duration)】 的參數是:【[{"id":0,"name":"zhuye","age":35,"balance":1000},5,{"seconds":1,"zero":false,"nano":0,"units":["SECONDS","NANOS"],"negative":false}]】
2018-10-07 10:47:02.237  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【成功打點】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public java.util.List me.josephzhu.spring101aop.MyServiceImpl.getData(me.josephzhu.spring101aop.MyBean,int,java.time.Duration)】 成功,耗時:PT-1.006S
2018-10-07 10:47:02.237  INFO 19737 --- [           main] me.josephzhu.spring101aop.MetricsAspect  : 【出參日誌】調用 【class me.josephzhu.spring101aop.MyServiceImpl】【public java.util.List me.josephzhu.spring101aop.MyServiceImpl.getData(me.josephzhu.spring101aop.MyBean,int,java.time.Duration)】 的返回是:【[MyBean(id=1, name=zhuye1, age=35, balance=1000), MyBean(id=2, name=zhuye2, age=35, balance=1000), MyBean(id=3, name=zhuye3, age=35, balance=1000), MyBean(id=4, name=zhuye4, age=35, balance=1000), MyBean(id=5, name=zhuye5, age=35, balance=1000)]】
[MyBean(id=1, name=zhuye1, age=35, balance=1000), MyBean(id=2, name=zhuye2, age=35, balance=1000), MyBean(id=3, name=zhuye3, age=35, balance=1000), MyBean(id=4, name=zhuye4, age=35, balance=1000), MyBean(id=5, name=zhuye5, age=35, balance=1000)]

正確實現了參很多天志、異常日誌、成功失敗打點(含耗時統計)等功能。
下面咱們建立一個Controller來測試一下是否能夠自動切入Controller:

package me.josephzhu.spring101aop;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
public class MyController {

    @Autowired
    private DbMapper dbMapper;

    @ResponseBody
    @GetMapping("/data")
    public List<MyBean> getPersonList(){
        return dbMapper.getPersonList();
    }
}

運行程序打開瀏覽器訪問http://localhost:8080/data後能看到以下輸出:

2018-10-07 10:49:53.811  INFO 19737 --- [nio-8080-exec-1] me.josephzhu.spring101aop.MetricsAspect  : 【入參日誌】調用 【class me.josephzhu.spring101aop.MyController】【public java.util.List me.josephzhu.spring101aop.MyController.getPersonList()】【http://localhost:8080/data】 的參數是:【[]】
2018-10-07 10:49:53.819  INFO 19737 --- [nio-8080-exec-1] me.josephzhu.spring101aop.MetricsAspect  : 【成功打點】調用 【class me.josephzhu.spring101aop.MyController】【public java.util.List me.josephzhu.spring101aop.MyController.getPersonList()】【http://localhost:8080/data】 成功,耗時:PT-0.008S
2018-10-07 10:49:53.819  INFO 19737 --- [nio-8080-exec-1] me.josephzhu.spring101aop.MetricsAspect  : 【出參日誌】調用 【class me.josephzhu.spring101aop.MyController】【public java.util.List me.josephzhu.spring101aop.MyController.getPersonList()】【http://localhost:8080/data】 的返回是:【[MyBean(id=1, name=zhuye, age=35, balance=1000), MyBean(id=2, name=zhuye, age=35, balance=1000)]】

最後,咱們再來踩一個坑。咱們來測一下ignoreException吞掉異常的功能(默認爲false):

@Transactional(rollbackFor = Exception.class)
@Override
@Metrics(ignoreException = true)
public void insertData(boolean success){
    dbMapper.personInsertWithoutId();
    if(!success)
        dbMapper.personInsertWithId();
}

這個功能會吞掉異常,在和Transactional事務管理結合時候會不會出問題呢?
開啓這個配置後刷新頁面能夠看到數據庫內有三條記錄了,說明第二次的insertData方法執行沒有成功回滾事務。這也是合情合理的,畢竟咱們的MetricsAspect吃掉了異常。


怎麼繞開這個問題呢?答案是咱們須要手動控制一下咱們的切面的執行優先級,咱們但願這個切面優先級比Spring事務控制切面優先級低:

@Aspect
@Component
@Slf4j
@Order(1)
public class MetricsAspect {

再次運行程序能夠看到事務正確回滾。

總結

本文咱們經過一些例子覆蓋了以下內容:

  1. Spring AOP的一些基本知識點。
  2. Mybatis和H2的簡單配置使用。
  3. 如何實現Spring事務管理。
  4. 如何切換爲AspjectJ進行AOP。
  5. 觀察JDK代理和CGLIB代理。
  6. 如何定義切面實現事務後處理和日誌異常打點這種橫切關注點。

在整個過程當中,也踩了下面的坑,印證的本文的標題:

  1. Spring AOP代理不能做用於代理類內部this方法調用的坑。
  2. Spring AOP實例化切面默認單例的坑。
  3. AJC編譯器沒法支持lambok的坑。
  4. 切面優先級順序的坑。
  5. 切面內部獲取註解方式的坑。

老樣子,本系列文章代碼見個人github:https://github.com/JosephZhu1983/Spring101。

相關文章
相關標籤/搜索