使用Spring Boot和AspectJ實現方法跟蹤基礎結構

瞭解如何使用Spring Boot和AspectJ實現方法跟蹤基礎結構!最近在優銳課學習收穫頗多,記錄下來你們一塊兒進步!java

在咱們的應用程序中,獲取方法的堆棧跟蹤信息可能會節省不少時間。具備輸入輸出參數值和方法所花費的時間可使查找問題變得更加容易。在本文中,咱們將研究如何使用Spring Boot,AspectJ和Threadlocal爲方法跟蹤基礎結構實現起點。web

在此示例中,我使用了: Spring Boot Starter Web 2.1.7spring

  • Java 1.8 +
  • AspectJ 1.8
  • Maven 3.2

1. 總覽

在本教程中,咱們將準備一個簡單的REST服務,該服務將在書店中檢索有關一本書的詳細信息。而後,咱們將添加一個ThreadLocal模型,該模型將在整個線程生命週期中保持堆棧結構。最後,咱們將增長一個方面來削減調用堆棧中的方法,以獲取輸入/輸出參數值。讓咱們開始吧!數據庫

項目結構

 

 

2. Maven依賴

  • Spring Boot Starter Web —使用Spring MVC的RESTful服務
  • Spring — 具有Aspect功能
  • AspectJ編織者向Java類引入建議
  • Apache Commons Lang —用於字符串實用程序
   
 1  <parent>
 2         <groupId>org.springframework.boot</groupId>
 3         <artifactId>spring-boot-starter-parent</artifactId>
 4         <version>2.1.7.RELEASE</version>
 5     </parent>
 6 <properties>
 7         <java.version>1.8</java.version>
 8     </properties>
 9 <dependencies>
10         <dependency>
11             <groupId>org.springframework.boot</groupId>
12             <artifactId>spring-boot-starter-web</artifactId>
13             <version>2.1.7.RELEASE</version>
14         </dependency>
15         <dependency>
16             <groupId>org.springframework</groupId>
17             <artifactId>spring-aop</artifactId>
18             <version>5.0.9.RELEASE</version>
19         </dependency>
20         <dependency>
21             <groupId>org.aspectj</groupId>
22             <artifactId>aspectjweaver</artifactId>
23             <version>1.8.9</version>
24         </dependency>
25         <dependency>
26             <groupId>org.apache.commons</groupId>
27             <artifactId>commons-lang3</artifactId>
28             <version>3.8.1</version>
29         </dependency>
30     </dependencies>

 

3. 實操

建立一個Spring Boot應用程序

你可使用這些模板來爲逐步實現建立一個簡單的Spring Boot Application,也能夠在此處直接下載最終項目。apache

For IntelliJ:緩存

https://www.javadevjournal.com/spring-boot/spring-boot-application-intellij/併發

For Eclipse:app

https://dzone.com/articles/building-your-first-spring-boot-web-application-exspring-boot

簡單的Rest Service和方法

首先,咱們將建立咱們的服務。咱們將得到書籍項目號做爲輸入參數,並提供書名,價格和內容信息做爲服務輸出。學習

咱們將提供三個簡單的服務:

PriceService:

 1 package com.example.demo.service;
 2 import org.springframework.stereotype.Service;
 3 @Service
 4 public class PriceService {
 5     public double getPrice(int itemNo){
 6             switch (itemNo) {
 7                 case 1 :
 8                     return 10.d;
 9                 case 2 :
10                     return 20.d;
11                 default:
12                     return 0.d;
13             }
14     }
15 }

 

CatalogueService:

 1 package com.example.demo.service;
 2 import org.springframework.stereotype.Service;
 3 @Service
 4 public class CatalogueService {
 5     public String getContent(int itemNo){
 6         switch (itemNo) {
 7             case 1 :
 8                 return "Lorem ipsum content 1.";
 9             case 2 :
10                 return "Lorem ipsum content 2.";
11             default:
12                 return "Content not found.";
13         }
14     }
15     public String getTitle(int itemNo){
16         switch (itemNo) {
17             case 1 :
18                 return "For whom the bell tolls";
19             case 2 :
20                 return "Of mice and men";
21             default:
22                 return "Title not found.";
23         }
24     }
25 }

 

BookInfoService:

 1 package com.example.demo.service;
 2 import org.springframework.beans.factory.annotation.Autowired;
 3 import org.springframework.stereotype.Service;
 4 @Service
 5 public class BookInfoService {
 6     @Autowired
 7     PriceService priceService;
 8     @Autowired
 9     CatalogueService catalogueService;
10     public String getBookInfo(int itemNo){
11         StringBuilder sb = new StringBuilder();
12         sb.append(" Title :" + catalogueService.getTitle(itemNo));
13         sb.append(" Price:" + priceService.getPrice(itemNo));
14         sb.append(" Content:" + catalogueService.getContent(itemNo));
15         return sb.toString();
16     }
17 }

 

BookController: 這是咱們的REST控制器,用於建立可檢索圖書信息的RET服務。咱們將準備一個TraceMonitor服務,以便之後打印堆棧跟蹤。

 1 package com.example.demo.controller;
 2 import com.example.demo.service.BookInfoService;
 3 import com.example.demo.trace.TraceMonitor;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.web.bind.annotation.GetMapping;
 6 import org.springframework.web.bind.annotation.PathVariable;
 7 import org.springframework.web.bind.annotation.RestController;
 8 @RestController
 9 public class BookController {
10     @Autowired
11     BookInfoService bookInfoService;
12     @Autowired
13     TraceMonitor traceMonitor;
14     @GetMapping("/getBookInfo/{itemNo}")
15     public String getBookInfo(@PathVariable int itemNo) {
16         try{
17             return bookInfoService.getBookInfo(itemNo);
18         }finally {
19             traceMonitor.printTrace();
20         }
21     }
22 }

 

 

咱們的REST控制器隨時可使用。若是咱們註釋掉還沒有實現的traceMonitor.printTrace()方法,而後使用@SpringBootApplication註釋的類運行咱們的應用程序:

1 package com.example.demo;
2 import org.springframework.boot.SpringApplication;
3 import org.springframework.boot.autoconfigure.SpringBootApplication;
4 @SpringBootApplication
5 public class DemoApplication {
6     public static void main(String[] args) {
7         SpringApplication.run(DemoApplication.class, args);
8     }
9 }

 

http://localhost:8080/getBookInfo/2

> Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2.

線程本地模型

如今,咱們將準備咱們的Method對象,該對象將保存任何方法調用的信息。稍後,咱們將準備堆棧結構和ThreadLocal對象,這些對象將在線程的整個生命週期中保持堆棧結構。

Method:這是咱們的模型對象,它將保留有關方法執行的全部詳細信息。它包含方法的輸入/輸出參數,該方法所花費的時間以及methodList對象,該對象是直接從該方法調用的方法列表。

 1 package com.example.demo.util.log.standartlogger;
 2 import java.util.List;
 3 public class Method {
 4 private String methodName;
 5 private String input;
 6 private List<Method> methodList;
 7 private String output;
 8 private Long timeInMs;
 9 public Long getTimeInMs() {
10 return timeInMs;
11 }
12 public void setTimeInMs(Long timeInMs) {
13 this.timeInMs = timeInMs;
14 }
15 public String getInput() {
16 return input;
17 }
18 public void setInput(String input) {
19 this.input = input;
20 }
21 public String getOutput() {
22 return output;
23 }
24 public void setOutput(String output) {
25 this.output = output;
26 }
27 public List<Method> getMethodList() {
28 return methodList;
29 }
30 public void setMethodList(List<Method> methodList) {
31 this.methodList = methodList;
32 }
33 public String getMethodName() {
34 return methodName;
35 }
36 public void setMethodName(String methodName) {
37 this.methodName = methodName;
38 }
39 }

 

ThreadLocalValues: 保留主要方法的跟蹤信息。方法mainMethod包含List<Method>methodList對象,該對象包含從main方法調用的子方法。

 Deque<Method>methodStack 是保留方法調用堆棧的對象。它貫穿線程的整個生命週期。調用子方法時,將Method對象推送到methodStack上,當子方法返回時,將從methodStack彈出頂部的Method對象。

 1 package com.example.demo.util.log.standartlogger;
 2 import java.util.Deque;
 3 public class ThreadLocalValues {
 4 private Deque<Method> methodStack;
 5 private Method mainMethod;
 6 public ThreadLocalValues() {
 7 super();
 8 }
 9 public Method getMainMethod() {
10 return mainMethod;
11 }
12 public void setMainMethod(Method mainMethod) {
13 this.mainMethod = mainMethod;
14 }
15 public Deque<Method> getMethodStack() {
16 return methodStack;
17 }
18 public void setMethodStack(Deque<Method> methodStack) {
19 this.methodStack = methodStack;
20 }
21 }

 

 

LoggerThreadLocal: 此類保留ThreadLocalValuesThreadLocal對象。該對象在線程的整個生命週期中一直存在。

 1 package com.example.demo.util.log.standartlogger;
 2 import java.util.ArrayDeque;
 3 import java.util.Deque;
 4 public class LoggerThreadLocal {
 5 static final ThreadLocal<ThreadLocalValues> threadLocal = new ThreadLocal<>();
 6 private LoggerThreadLocal() {
 7 super();
 8 }
 9 public static void setMethodStack(Deque<Method> methodStack) {
10 ThreadLocalValues threadLocalValues = threadLocal.get();
11 if (null == threadLocalValues) {
12 threadLocalValues = new ThreadLocalValues();
13 }
14 threadLocalValues.setMethodStack(methodStack);
15 threadLocal.set(threadLocalValues);
16 }
17 public static void setMainMethod(Method mainMethod){
18 ThreadLocalValues threadLocalValues = threadLocal.get();
19 if (null == threadLocalValues) {
20 threadLocalValues = new ThreadLocalValues();
21 }
22 threadLocalValues.setMainMethod(mainMethod);
23 threadLocal.set(threadLocalValues);
24 }
25 public static Method getMainMethod() {
26 if (threadLocal.get() == null) {
27 return null;
28 }
29 return threadLocal.get().getMainMethod();
30 }
31 public static Deque<Method> getMethodStack() {
32 if (threadLocal.get() == null) {
33 setMethodStack(new ArrayDeque<>());
34 }
35 return threadLocal.get().getMethodStack();
36 }
37 }

 

 

Aspect Implementations:

TraceMonitor: 此類是咱們方面的配置類。在此類中,咱們定義切入點,切面在切入點處切割代碼流。咱們的切入點定義了名稱以單詞「 Service」結尾的全部類中的全部方法。

 @Pointcut(value = "execution(* com.example.demo.service.*Service.*(..))") 

pushStackInBean: 這是將在切入點中執行方法以前將當前方法推入方法堆棧的方法。

popStackInBean: 此方法將在切入點返回該方法後,刪除堆棧中的top方法。

printTrace: 這是一種將以JSON格式打印threadLocal值(mainMethod)的方法。

 1 package com.example.demo.trace;
 2 import java.util.ArrayList;
 3 import com.example.demo.util.log.standartlogger.LoggerThreadLocal;
 4 import com.example.demo.util.log.standartlogger.Method;
 5 import com.fasterxml.jackson.core.JsonProcessingException;
 6 import com.fasterxml.jackson.databind.ObjectMapper;
 7 import org.apache.commons.lang3.StringUtils;
 8 import org.apache.commons.lang3.exception.ExceptionUtils;
 9 import org.aspectj.lang.JoinPoint;
10 import org.aspectj.lang.annotation.AfterReturning;
11 import org.aspectj.lang.annotation.Aspect;
12 import org.aspectj.lang.annotation.Before;
13 import org.aspectj.lang.annotation.Pointcut;
14 import org.springframework.context.annotation.Configuration;
15 import org.springframework.stereotype.Service;
16 @Aspect
17 @Service
18 @Configuration
19 public class TraceMonitor {
20     @Pointcut(value = "execution(* com.example.demo.service.*Service.*(..))")
21     private void executionInService() {
22         //do nothing, just for pointcut def
23     }
24     @Before(value = "executionInService()")
25     public void pushStackInBean(JoinPoint joinPoint) {
26         pushStack(joinPoint);
27     }
28     @AfterReturning(value = "executionInService()", returning = "returnValue")
29     public void popStackInBean(Object returnValue) {
30         popStack(returnValue);
31     }
32     ObjectMapper mapper = new ObjectMapper();
33     private void pushStack(JoinPoint joinPoint) {
34             Method m = new Method();
35             m.setMethodName(StringUtils.replace(joinPoint.getSignature().toString(), "com.example.demo.service.", ""));
36             String input = getInputParametersString(joinPoint.getArgs());
37             m.setInput(input);
38             m.setTimeInMs(Long.valueOf(System.currentTimeMillis()));
39             LoggerThreadLocal.getMethodStack().push(m);
40     }
41     private String getInputParametersString(Object[] joinPointArgs) {
42         String input;
43         try {
44             input = mapper.writeValueAsString(joinPointArgs);
45         } catch (Exception e) {
46             input = "Unable to create input parameters string. Error:" + e.getMessage();
47         }
48         return input;
49     }
50     private void popStack(Object output) {
51         Method childMethod = LoggerThreadLocal.getMethodStack().pop();
52         try {
53             childMethod.setOutput(output==null?"": mapper.writeValueAsString(output));
54         } catch (JsonProcessingException e) {
55             childMethod.setOutput(e.getMessage());
56         }
57         childMethod.setTimeInMs(Long.valueOf(System.currentTimeMillis() - childMethod.getTimeInMs().longValue()));
58         if (LoggerThreadLocal.getMethodStack().isEmpty()) {
59             LoggerThreadLocal.setMainMethod(childMethod);
60         } else {
61             Method parentMethod = LoggerThreadLocal.getMethodStack().peek();
62             addChildMethod(childMethod, parentMethod);
63         }
64     }
65     private void addChildMethod(Method childMethod, Method parentMethod) {
66         if (parentMethod != null) {
67             if (parentMethod.getMethodList() == null) {
68                 parentMethod.setMethodList(new ArrayList<>());
69             }
70             parentMethod.getMethodList().add(childMethod);
71         }
72     }
73     public void printTrace() {
74         try {
75             StringBuilder sb = new StringBuilder();
76             sb.append("\n<TRACE>\n").append(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(LoggerThreadLocal.getMainMethod()));
77             sb.append("\n</TRACE>");
78             System.out.println(sb.toString());
79         } catch (JsonProcessingException e) {
80             StringUtils.abbreviate(ExceptionUtils.getStackTrace(e), 2000);
81         }
82     }
83 }

 

3. 測試和打印堆棧

當咱們運行Spring Boot應用程序併發送get請求時:

http://localhost:8080/getBookInfo/2

回覆將是:

> Title:Of mice and men Price:20.0 Content:Lorem ipsum content 2.

注意:若是你以前對traceMonitor.printTrace()進行了註釋,請不要忘記取消註釋。

控制檯輸出將是:

 1 <TRACE>
 2 {
 3   "methodName": "String service.BookInfoService.getBookInfo(int)",
 4   "input": "[2]",
 5   "methodList": [
 6     {
 7       "methodName": "String service.ContentService.getTitle(int)",
 8       "input": "[2]",
 9       "output": "\"Of mice and men\"",
10       "timeInMs": 3
11     },
12     {
13       "methodName": "Double service.PriceService.getPrice(int)",
14       "input": "[2]",
15       "output": "20.0",
16       "timeInMs": 1
17     },
18     {
19       "methodName": "String service.ContentService.getContent(int)",
20       "input": "[2]",
21       "output": "\"Lorem ipsum content 2.\"",
22       "timeInMs": 0
23     }
24   ],
25   "output": "\" Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2.\"",
26   "timeInMs": 6
27 }
28 </TRACE>

 

 

因爲咱們能夠輕鬆跟蹤方法流程:

  •  getBookInfo method is called with input 2
  •  getBookInfo calls getTitle  method with input 2
  •  getTitle returns with output "Of mice and men" in 3 ms.
  •  getBookInfo calls getPrice  with input 2
  •  getPrice returns with output 20.0 in 1 ms.
  •  getBookInfo calls getContent  with input 2
  •  getContent returns with output "Lorem ipsum content 2." in 0 ms.
  •  getBookInfo method returns with output "Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2." in 6 ms.

咱們的跟蹤實現適用於咱們簡單的REST服務調用。

進一步的改進應該是:

  • 若是有任何方法得到異常,則使用@AfterThrowing處理異常。
  • 具備可緩存方法的打開/關閉跟蹤機制,該方法從服務或數據庫中讀取可跟蹤方法列表。
  • 使用記錄器實現(sl4j)將跟蹤打印到單獨的日誌文件中。

感謝閱讀!

相關文章
相關標籤/搜索