使用Spring Boot和AspectJ实现方法跟踪基础结构
了解如何使用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 —用于字符串实用程序
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</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:
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class PriceService {
public double getPrice(int itemNo){
switch (itemNo) {
case 1 :
return 10.d;
case 2 :
return 20.d;
default:
return 0.d;
}
}
}
CatalogueService:
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class CatalogueService {
public String getContent(int itemNo){
switch (itemNo) {
case 1 :
return "Lorem ipsum content 1.";
case 2 :
return "Lorem ipsum content 2.";
default:
return "Content not found.";
}
}
public String getTitle(int itemNo){
switch (itemNo) {
case 1 :
return "For whom the bell tolls";
case 2 :
return "Of mice and men";
default:
return "Title not found.";
}
}
}
BookInfoService:
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookInfoService {
@Autowired
PriceService priceService;
@Autowired
CatalogueService catalogueService;
public String getBookInfo(int itemNo){
StringBuilder sb = new StringBuilder();
sb.append(" Title :" + catalogueService.getTitle(itemNo));
sb.append(" Price:" + priceService.getPrice(itemNo));
sb.append(" Content:" + catalogueService.getContent(itemNo));
return sb.toString();
}
}
BookController: 这是我们的REST控制器,用于创建可检索图书信息的RET服务。我们将准备一个TraceMonitor服务,以便以后打印堆栈跟踪。
package com.example.demo.controller;
import com.example.demo.service.BookInfoService;
import com.example.demo.trace.TraceMonitor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookController {
@Autowired
BookInfoService bookInfoService;
@Autowired
TraceMonitor traceMonitor;
@GetMapping("/getBookInfo/{itemNo}")
public String getBookInfo(@PathVariable int itemNo) {
try{
return bookInfoService.getBookInfo(itemNo);
}finally {
traceMonitor.printTrace();
}
}
}
我们的REST控制器随时可以使用。如果我们注释掉尚未实现的traceMonitor.printTrace()方法,然后使用@SpringBootApplication注释的类运行我们的应用程序:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
http://localhost:8080/getBookInfo/2
> Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2.
线程本地模型
现在,我们将准备我们的Method对象,该对象将保存任何方法调用的信息。稍后,我们将准备堆栈结构和ThreadLocal对象,这些对象将在线程的整个生命周期中保持堆栈结构。
Method:这是我们的模型对象,它将保留有关方法执行的所有详细信息。它包含方法的输入/输出参数,该方法所花费的时间以及methodList对象,该对象是直接从该方法调用的方法列表。
package com.example.demo.util.log.standartlogger;
import java.util.List;
public class Method {
private String methodName;
private String input;
private List<Method> methodList;
private String output;
private Long timeInMs;
public Long getTimeInMs() {
return timeInMs;
}
public void setTimeInMs(Long timeInMs) {
this.timeInMs = timeInMs;
}
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public String getOutput() {
return output;
}
public void setOutput(String output) {
this.output = output;
}
public List<Method> getMethodList() {
return methodList;
}
public void setMethodList(List<Method> methodList) {
this.methodList = methodList;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
}
ThreadLocalValues: 保留主要方法的跟踪信息。方法mainMethod包含List<Method>methodList对象,该对象包含从main方法调用的子方法。
Deque<Method>methodStack 是保留方法调用堆栈的对象。它贯穿线程的整个生命周期。调用子方法时,将Method对象推送到methodStack上,当子方法返回时,将从methodStack弹出顶部的Method对象。
package com.example.demo.util.log.standartlogger;
import java.util.Deque;
public class ThreadLocalValues {
private Deque<Method> methodStack;
private Method mainMethod;
public ThreadLocalValues() {
super();
}
public Method getMainMethod() {
return mainMethod;
}
public void setMainMethod(Method mainMethod) {
this.mainMethod = mainMethod;
}
public Deque<Method> getMethodStack() {
return methodStack;
}
public void setMethodStack(Deque<Method> methodStack) {
this.methodStack = methodStack;
}
}
LoggerThreadLocal: 此类保留ThreadLocalValues的ThreadLocal对象。该对象在线程的整个生命周期中一直存在。
package com.example.demo.util.log.standartlogger;
import java.util.ArrayDeque;
import java.util.Deque;
public class LoggerThreadLocal {
static final ThreadLocal<ThreadLocalValues> threadLocal = new ThreadLocal<>();
private LoggerThreadLocal() {
super();
}
public static void setMethodStack(Deque<Method> methodStack) {
ThreadLocalValues threadLocalValues = threadLocal.get();
if (null == threadLocalValues) {
threadLocalValues = new ThreadLocalValues();
}
threadLocalValues.setMethodStack(methodStack);
threadLocal.set(threadLocalValues);
}
public static void setMainMethod(Method mainMethod){
ThreadLocalValues threadLocalValues = threadLocal.get();
if (null == threadLocalValues) {
threadLocalValues = new ThreadLocalValues();
}
threadLocalValues.setMainMethod(mainMethod);
threadLocal.set(threadLocalValues);
}
public static Method getMainMethod() {
if (threadLocal.get() == null) {
return null;
}
return threadLocal.get().getMainMethod();
}
public static Deque<Method> getMethodStack() {
if (threadLocal.get() == null) {
setMethodStack(new ArrayDeque<>());
}
return threadLocal.get().getMethodStack();
}
}
Aspect Implementations:
TraceMonitor: 此类是我们方面的配置类。在此类中,我们定义切入点,切面在切入点处切割代码流。我们的切入点定义了名称以单词“ Service”结尾的所有类中的所有方法。
@Pointcut(value = "execution(* com.example.demo.service.*Service.*(..))")
pushStackInBean: 这是将在切入点中执行方法之前将当前方法推入方法堆栈的方法。
popStackInBean: 此方法将在切入点返回该方法后,删除堆栈中的top方法。
printTrace: 这是一种将以JSON格式打印threadLocal值(mainMethod)的方法。
package com.example.demo.trace;
import java.util.ArrayList;
import com.example.demo.util.log.standartlogger.LoggerThreadLocal;
import com.example.demo.util.log.standartlogger.Method;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
@Aspect
@Service
@Configuration
public class TraceMonitor {
@Pointcut(value = "execution(* com.example.demo.service.*Service.*(..))")
private void executionInService() {
//do nothing, just for pointcut def
}
@Before(value = "executionInService()")
public void pushStackInBean(JoinPoint joinPoint) {
pushStack(joinPoint);
}
@AfterReturning(value = "executionInService()", returning = "returnValue")
public void popStackInBean(Object returnValue) {
popStack(returnValue);
}
ObjectMapper mapper = new ObjectMapper();
private void pushStack(JoinPoint joinPoint) {
Method m = new Method();
m.setMethodName(StringUtils.replace(joinPoint.getSignature().toString(), "com.example.demo.service.", ""));
String input = getInputParametersString(joinPoint.getArgs());
m.setInput(input);
m.setTimeInMs(Long.valueOf(System.currentTimeMillis()));
LoggerThreadLocal.getMethodStack().push(m);
}
private String getInputParametersString(Object[] joinPointArgs) {
String input;
try {
input = mapper.writeValueAsString(joinPointArgs);
} catch (Exception e) {
input = "Unable to create input parameters string. Error:" + e.getMessage();
}
return input;
}
private void popStack(Object output) {
Method childMethod = LoggerThreadLocal.getMethodStack().pop();
try {
childMethod.setOutput(output==null?"": mapper.writeValueAsString(output));
} catch (JsonProcessingException e) {
childMethod.setOutput(e.getMessage());
}
childMethod.setTimeInMs(Long.valueOf(System.currentTimeMillis() - childMethod.getTimeInMs().longValue()));
if (LoggerThreadLocal.getMethodStack().isEmpty()) {
LoggerThreadLocal.setMainMethod(childMethod);
} else {
Method parentMethod = LoggerThreadLocal.getMethodStack().peek();
addChildMethod(childMethod, parentMethod);
}
}
private void addChildMethod(Method childMethod, Method parentMethod) {
if (parentMethod != null) {
if (parentMethod.getMethodList() == null) {
parentMethod.setMethodList(new ArrayList<>());
}
parentMethod.getMethodList().add(childMethod);
}
}
public void printTrace() {
try {
StringBuilder sb = new StringBuilder();
sb.append("\n<TRACE>\n").append(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(LoggerThreadLocal.getMainMethod()));
sb.append("\n</TRACE>");
System.out.println(sb.toString());
} catch (JsonProcessingException e) {
StringUtils.abbreviate(ExceptionUtils.getStackTrace(e), 2000);
}
}
}
3. 测试和打印堆栈
当我们运行Spring Boot应用程序并发送get请求时:
http://localhost:8080/getBookInfo/2
回复将是:
> Title:Of mice and men Price:20.0 Content:Lorem ipsum content 2.
注意:如果你之前对traceMonitor.printTrace()进行了注释,请不要忘记取消注释。
控制台输出将是:
<TRACE>
{
"methodName": "String service.BookInfoService.getBookInfo(int)",
"input": "[2]",
"methodList": [
{
"methodName": "String service.ContentService.getTitle(int)",
"input": "[2]",
"output": "\"Of mice and men\"",
"timeInMs": 3
},
{
"methodName": "Double service.PriceService.getPrice(int)",
"input": "[2]",
"output": "20.0",
"timeInMs": 1
},
{
"methodName": "String service.ContentService.getContent(int)",
"input": "[2]",
"output": "\"Lorem ipsum content 2.\"",
"timeInMs": 0
}
],
"output": "\" Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2.\"",
"timeInMs": 6
}
</TRACE>
由于我们可以轻松跟踪方法流程:
getBookInfomethod is called with input 2getBookInfocallsgetTitlemethod with input 2getTitlereturns with output "Of mice and men" in 3 ms.getBookInfocallsgetPricewith input 2getPricereturns with output 20.0 in 1 ms.getBookInfocallsgetContentwith input 2getContentreturns with output "Lorem ipsum content 2." in 0 ms.getBookInfomethod returns with output "Title :Of mice and men Price:20.0 Content:Lorem ipsum content 2." in 6 ms.
我们的跟踪实现适用于我们简单的REST服务调用。
进一步的改进应该是:
- 如果有任何方法获得异常,则使用
@AfterThrowing处理异常。 - 具有可缓存方法的打开/关闭跟踪机制,该方法从服务或数据库中读取可跟踪方法列表。
- 使用记录器实现(sl4j)将跟踪打印到单独的日志文件中。
感谢阅读!

使用Spring Boot和AspectJ实现方法跟踪基础结构的更多相关文章
- Spring Boot 初始化运行特定方法
Spring Boot提供了两种 “开机自启动” 的方式,ApplicationRunner和CommandLineRunner 这两种方式的目的是为了满足,在容器启动时like执行某些方法.我们可以 ...
- Spring Boot 2.0 + zipkin 分布式跟踪系统快速入门
原文:https://www.jianshu.com/p/9bfe103418e2 注意 Spring Boot 2.0之后,使用EnableZipkinServer创建自定义的zipkin服务器已经 ...
- Spring boot 打包瘦身方法
背景 随着spring boot 的流行.越来越多的来发着选择使用spring boot 来发 web 应用. 不同于传统的 web 应用 需要 war 包来发布应用. spring boot 应用可 ...
- Spring Boot 之异步执行方法
前言: 最近的时候遇到一个需求,就是当服务器接到请求并不需要任务执行完成才返回结果,可以立即返回结果,让任务异步的去执行.开始考虑是直接启一个新的线程去执行任务或者把任务提交到一个线程池去执行,这两种 ...
- TDDL与Spring Boot集成Version报错——跟踪与解决
先说背景:公司采用diamond+tddl,这套技术来做web管理.本人处于好奇率先体验了下spring-boot,于是就有了spring-boot+tddl的组合.但是jar包上线后,屡屡发现一条e ...
- spring boot启动后执行方法
@Componentpublic class InitProject implements ApplicationRunner { private static final Logger logger ...
- Spring boot异常统一处理方法:@ControllerAdvice注解的使用、全局异常捕获、自定义异常捕获
一.全局异常 1.首先创建异常处理包和类 2.使用@ControllerAdvice注解,全局捕获异常类,只要作用在@RequestMapping上,所有的异常都会被捕获 package com.ex ...
- Spring boot JPA读取数据库方法
方法1: 1 StringBuffer sb = new StringBuffer(300); 2 sb.append("SELECT v.id, v.container_number, v ...
- Spring Boot重定向的使用方法
@RequestMapping(value = "/redirect", method = RequestMethod.GET) public void redirecttest( ...
随机推荐
- php Swoole实现毫秒级定时任务
项目开发中,如果有定时任务的业务要求,我们会使用linux的crontab来解决,但是它的最小粒度是分钟级别,如果要求粒度是秒级别的,甚至毫秒级别的,crontab就无法满足,值得庆幸的是swoole ...
- 在阿里云服务器中配置JDK、tomcat、mysql
阿里云服务器搭建配置 linux命令:参考:https://www.cnblogs.com/itdansan/p/8545187.html cat 文件名: 查看文件内容 ctrl+D : 退出查看 ...
- router-link传递参数
有个功能: 依据传入值,跳到产品详情页,但是详情页的内容依据传入值来相应变化. 如果使用点击事件@clic来实现,则有三个重复的跳转代码. 避免多次定义重复函数,可以使用router-link 传参数 ...
- 解决django或者其他线程中调用scrapy报ReactorNotRestartable的错误
官网中关于ReactorNotRestartable的错误描述(摘自:https://twistedmatrix.com/documents/16.1.0/api/twisted.internet.e ...
- ArcGIS 切片与矢量图图层顺序问题
在项目中有个需求:根据图层索引添加图层 看到这个需求一下子想到 map.addLayer(layer,index?) 接口 但是问题出现了,我切片图加载顺序在矢量图之后就不行! map = new M ...
- uwsgi启动报错 chdir(): No such file or directory [core/uwsgi.c line 2591]
今天在使用 uwsgi --ini uwsgi.ini 命令时总说找不到我的项目路径,可是我的路径是绝对没有问题的 解决方法: 把你的uwgis.ini文件里的全部注释删除,再运行uwsgi 启动成功 ...
- Redis高可用演进(一)
原文链接:http://www.cnblogs.com/chenty/p/5152878.html 最近整理Redis,对sentinel有了更深入的理解,特地总结如下 1.主从Redis 主从red ...
- Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)
随着Kotlin的推广,一些国内公司的安卓项目开发,已经从Java完全切成Kotlin了.虽然Kotlin在各类编程语言中的排名比较靠后(据TIOBE发布了 19 年 8 月份的编程语言排行榜,Kot ...
- 【C/C++】之C语言库函数
这个帖子记录一下 C语言 中经常用到的函数库中的函数及其用法. 1.<math.h> math.h是进行数学操作的函数库.使用这个函数库,需要先导入包: #include <math ...
- Vsftp与PAM虚拟用户
使用yum 安装vsftp yum install vsftpd pam pam-* db4 db4-* 创建一个保存用户及密码的文件 cd /etc/vsftpd/ touch virtual_lo ...
