了解如何使用Spring Boot和AspectJ实现方法跟踪基础结构!最近在优锐课学习收获颇多,记录下来大家一起进步!
在我们的应用程序中,获取方法的堆栈跟踪信息可能会节省很多时间。具有输入输出参数值和方法所花费的时间可以使查找问题变得更加容易。在本文中,我们将研究如何使用Spring Boot,AspectJ和Threadlocal为方法跟踪基础结构实现起点。
在此示例中,我使用了: Spring Boot Starter Web 2.1.7
- 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,也可以在此处直接下载最终项目。
For IntelliJ:
https://www.javadevjournal.com/spring-boot/spring-boot-application-intellij/
For Eclipse:
https://dzone.com/articles/building-your-first-spring-boot-web-application-ex
简单的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
: 此类保留ThreadLocalValues
的ThreadLocal
对象。该对象在线程的整个生命周期中一直存在。
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
callsgetTitle
method with input 2 -
getTitle
returns with output "Of mice and men" in 3 ms. -
getBookInfo
callsgetPrice
with input 2 -
getPrice
returns with output 20.0 in 1 ms. -
getBookInfo
callsgetContent
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)将跟踪打印到单独的日志文件中。
感谢阅读!