1. 程式人生 > 實用技巧 >後端筆記07-AopLog

後端筆記07-AopLog

SpringBoot7-AopLog

使用AOP切面對請求進行日誌記錄,同時記錄UserAgent資訊

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
	<!-- aop -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.4.0</version>
    </dependency>

    <!-- 解析 UserAgent 資訊 -->
    <dependency>
        <groupId>eu.bitwalker</groupId>
        <artifactId>UserAgentUtils</artifactId>
        <version>1.21</version>
    </dependency>
</dependencies>

AopLog.java

@Aspect
@Component
@Slf4j
public class AopLog {
    private static final String START_TIME = "request-start";

    /**
     * 切入點
     */
    //Pointcut表示式 
    @Pointcut("execution(public * com.study.aoplog.controller.*Controller.*(..))")
    //Pointcut簽名
    public void log() {

    }
    /**
     * 前置操作
     *
     * @param point 切入點
     */
    //AOP通知中可以用JoinPoint獲取資料
    //Around通知比較特殊,是ProceedingJoinPoint
    @Before("log()")
    public void beforeLog(JoinPoint point) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        log.info("【請求 URL】:{}",request.getRequestURL());
        log.info("【請求 IP】:{}",request.getRemoteAddr());
        log.info("【請求類名】:{},【請求方法名】:{}",point.getSignature().getDeclaringTypeName(),point.getSignature().getName());

        Map<String,String[]> parameterMap = request.getParameterMap();
        log.info("【請求引數】:{}", JSONUtil.toJsonStr(parameterMap));
        Long start = System.currentTimeMillis();
        request.setAttribute(START_TIME, start);
    }

    /**
     * 環繞操作
     *
     * @param point 切入點
     * @return 原方法返回值
     * @throws Throwable 異常資訊
     */
    @Around("log()")
    public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
        Object result = point.proceed();
        log.info("【返回值】:{}",JSONUtil.toJsonStr(result));
        return result;
    }

    /**
     * 後置操作
     */
    @AfterReturning("log()")
    public void afterReturning() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();

        Long start = (Long) request.getAttribute(START_TIME);
        Long end = System.currentTimeMillis();
        log.info("【請求耗時】:{}毫秒",end - start);

        String header = request.getHeader("User-Agent");

        UserAgent userAgent = UserAgent.parseUserAgentString(header);
        log.info("【瀏覽器型別】:{},【作業系統】:{},【原始User-Agent】:{}", userAgent.getBrowser().toString(), userAgent.getOperatingSystem().toString(), header);

    }
}

AOP:面向切面程式設計,相對於OOP面向物件程式設計 Spring的AOP的存在目的是為了解耦。AOP可以讓一組類共享相同的行為。在OOP中只能繼承和實現介面,且類繼承只能單繼承,阻礙更多行為新增到一組類上,AOP彌補了OOP的不足。

AOP術語

1.通知(有的地方叫增強)(Advice)

需要完成的工作叫做通知,就是你寫的業務邏輯中需要比如事務、日誌等先定義好,然後需要的地方再去用。

2.連線點(Join point)

對應的是具體被攔截的物件,因為Spring只支援方法,所以被攔截的物件往往就是指特定的方法。

3.切點(Poincut)

其實就是篩選出的連線點,一個類中的所有方法都是連線點,但又不全需要,會篩選出某些作為連線點做為切點。如果說通知定義了切面的動作或者執行時機的話,切點則定義了執行的地點。

有時候,我們的切面不單單應用於單個方法,也可以是多個類的不同方法,這時,可以通過正則表示式和指示器的規則去定義。

4.切面(Aspect)

其實就是通知和切點的結合,通知和切點共同定義了切面的全部內容,它是幹什麼的,什麼時候在哪執行。

5.引入(Introduction)

在不改變一個現有類程式碼的情況下,為該類新增屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行為和狀態。其實就是把切面(也就是新方法屬性:通知定義的)用到目標類中去。

6.目標(target)

被通知的物件。也就是需要加入額外程式碼的物件,也就是真正的業務邏輯被組織織入切面。

7.織入(Weaving)

把切面加入程式程式碼的過程。切面在指定的連線點被織入到目標物件中,在目標物件的生命週期裡有多個點可以進行織入:

  • 編譯期:切面在目標類編譯時被織入,這種方式需要特殊的編譯器。
  • 類載入期:切面在目標類載入到JVM時被織入,這種方式需要特殊的類載入器,它可以在目標類被引入應用之前增強該目標類的位元組碼。
  • 執行期:切面在應用執行的某個時刻被織入,一般情況下,在織入切面時,AOP容器會為目標物件動態建立一個代理物件,Spring AOP就是以這種方式織入切面的。
public class UserService{
    void save(){}
    List list(){}
    ....
}

以UserService為例,在UserService中的save()方法前需要開啟事務,在方法後關閉事務,在拋異常時回滾事務。
那麼,UserService中的所有方法都是連線點(JoinPoint),save()方法就是切點(Poincut)。需要在save()方法前後執行的方法就是通知(Advice),切點和通知合起來就是一個切面(Aspect)。save()方法就是目標(target)。把想要執行的程式碼動態的加入到save()方法前後就是織入(Weaving)。

AOP通知

1.before(前置通知): 在方法開始執行前執行

2.after(後置通知): 在方法執行後執行

3.afterReturning(返回後通知): 在方法返回後執行

4.afterThrowing(異常通知): 在丟擲異常時執行

5.around(環繞通知): 在方法執行前和執行後都會執行

執行順序 around > before > 【方法執行】>around > after > afterReturning

定義簡單切面

@Aspect // 使用@Aspect註解將一個java類定義為切面類
@Component
public class MyAspect {

  // 使用@Pointcut定義一個切入點,可以是一個規則表示式,也可以是一個註解等
  // Pointcut的定義包括兩個部分:Pointcut表示式(expression)和Pointcut簽名(signature)
  // Pointcut表示式
  // execution(* *(..)):表示匹配所有方法
  // execution(public * com.test.TestController.*(..)):表示匹配com.test.TestController類中所有的公有方法
  // execution(* com.test..*.*(..)):表示匹配com.test包中所有的方法
  @Pointcut("execution(public * com.test.TestController.testFunc(..))")
  // Pointcut簽名
  public void pointCut() {}
    
  // 使用@Before在切入點開始處切入內容
  @Before("pointCut()")
  public void before() {
      log.info("MyAspect before ...");
  }
    
  // 使用@After在切入點結尾處切入內容
  @After("pointCut()")
  public void after() {
      log.info("MyAspect after ...");
  }
    
  //使用@AfterReturning在切入點return內容之後切入內容(可以用來對處理返回值做一些加工處理)
  @AfterReturning("pointCut()")
  public void afterReturning() {
      log.info("MyAspect after returning ...");
  }
    
  //使用@AfterThrowing用來處理當切入內容部分丟擲異常之後的處理邏輯
  @AfterThrowing("pointCut()")
  public void afterThrowing() {
      log.info("MyAspect after throwing ...");
  }
    
  //使用@Around在切入點前後切入內容,並自己控制何時執行切入點自身的內容
  @Around("pointCut()")
  public void around(ProceedingJoinPoint joinPoint) throws Throwable {
      log.info("MyAspect around before ...");
      joinPoint.proceed();
      log.info("MyAspect around after ...");
  }
}

Spring AOP提供使用org.aspectj.lang.JoinPoint型別獲取連線點資料,任何通知方法的第一個引數都可以是JoinPoint(環繞通知是ProceedingJoinPoint,JoinPoint子類)。

  1. JoinPoint:提供訪問當前被通知方法的目標物件、代理物件、方法引數等資料
  2. ProceedingJoinPoint:只用於環繞通知,使用proceed()方法來執行目標方法

如引數型別是JoinPoint、ProceedingJoinPoint型別,可以從“argNames”屬性省略掉該引數名(可選,寫上也對),這些型別物件會自動傳入的,但必須作為第一個引數。

例子-切面中使用JoinPoint和ProceedingJoinPoint
@Aspect
@Component
public class MyAspect {

  @Pointcut("execution(public * com.test.TestController.testFunc(..))")
  public void pointCut() {}

  @Before("pointCut()")
  public void before(JoinPoint joinPoint) {
      String method = joinPoint.getSignature().getName();
      log.info("MyAspect before Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
      ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      HttpServletRequest request = attributes.getRequest();
      log.info("ClientIP:{}", request.getRemoteAddr());
  }

  @After("pointCut()")
  public void after(JoinPoint joinPoint) {
      String method = joinPoint.getSignature().getName();
      log.info("MyAspect after Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
  }

  @AfterReturning("pointCut()")
  public void afterReturning(JoinPoint joinPoint) {
      String method = joinPoint.getSignature().getName();
      log.info("MyAspect after returning Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
  }

  @AfterThrowing("pointCut()")
  public void afterThrowing(JoinPoint joinPoint) {
      log.info("MyAspect after throwing ...");
  }

  @Around("pointCut()")
  public void around(ProceedingJoinPoint joinPoint) throws Throwable {
      log.info("MyAspect around before ...");
      joinPoint.proceed();// 用proceed執行原方法 可用OBJ物件接收原方法返回值
      log.info("MyAspect around after ...");
  }
}

優化:AOP切面的優先順序

由於通過AOP實現,程式得到了很好的解耦,但是也會帶來一些問題,比如:我們可能會對Web層做多個切面,校驗使用者,校驗頭資訊等等,這個時候經常會碰到切面的處理順序問題。

所以,我們需要定義每個切面的優先順序,我們需要@Order(i)註解來標識切面的優先順序。i的值越小,優先順序越高。假設我們還有一個切面是CheckNameAspect用來校驗name必須為didi,我們為其設定@Order(10),假設另外有一個WebLogAspect設定為@Order(5),所以WebLogAspect有更高的優先順序,這個時候執行順序是這樣的:

  • @Before中優先執行@Order(5)的內容,再執行@Order(10)的內容
  • @After@AfterReturning中優先執行@Order(10)的內容,再執行@Order(5)的內容

所以我們可以這樣子總結:

  • 在切入點前的操作,按order的值由小到大執行
  • 在切入點後的操作,按order的值由大到小執行
JoinPoint getSignature() 返回屬性詳解
public interface Signature {
    String toString();
 
    String toShortString();
 
    String toLongString();
 
    String getName();// 請求方法名
 
    int getModifiers();
 
    Class getDeclaringType();
 
    String getDeclaringTypeName();// 請求類名
}
tips
@Around("log()")
public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
    Object result = point.proceed();
    log.info("【返回值】:{}",JSONUtil.toJsonStr(result));
    return result;
}
  @Around("pointCut()")
  public void around(ProceedingJoinPoint joinPoint) throws Throwable {
      log.info("MyAspect around before ...");
      joinPoint.proceed();
      log.info("MyAspect around after ...");
  }

在實際使用中,如果@Around下的方法是void,無返回型別,介面返回值將會被阻塞,需要使用第一種方法,才能使介面正確返回資料

參考網址:

https://juejin.im/post/6844903766035005453

https://juejin.im/post/6844903942254510093

http://blog.didispace.com/springbootaoplog/

RequestContextHolder 分析

持有上下文的Request容器 在Controller層外取用Request Response時使用 獲取方法如下

ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
HttpServletResponse response = Objects.requireNonNull(attributes).getResponse();

參考:

https://my.oschina.net/ojeta/blog/801640

UserAgentUtils

UserAgentUtils是一個處理user-agent 字元的一個工具。可以用來實時地處理http請求和分析http請求日誌檔案。這裡可以使用UserAgentUtils對訪問使用者的瀏覽器型別、作業系統、裝置種類等進行統計分析。

基本用法

UserAgent userAgent = UserAgent.parseUserAgentString(userAgentStr);
Browser browser = userAgent.getBrowser();
String browserName = browser.getName();// 瀏覽器名稱
String group = browser.getGroup().getName();// 瀏覽器大類
Version browserVersion = userAgent.getBrowserVersion();// 詳細版本
String version = browserVersion.getMajorVersion();// 瀏覽器主版本
System.out.println(browserName);
System.out.println(group);
System.out.println(browserVersion);
System.out.println(version);
System.out.println(userAgent.getOperatingSystem());// 訪問裝置系統
System.out.println(userAgent.getOperatingSystem().getDeviceType());// 訪問裝置型別
System.out.println(userAgent.getOperatingSystem().getManufacturer());// 訪問裝置製造廠商

TestController.java

@RestController
public class TestController {
    @GetMapping("/test")
    public Dict test(String who) {
        return Dict.create().set("who", StrUtil.isBlank(who) ? "me" : who);
    }
}

搬運自我的 Git:https://github.com/miles-rush/StudyNote
SpringBoot-Demo:https://github.com/xkcoding/spring-boot-demo 的學習筆記