了解如何使用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: 此类保留ThreadLocalValuesThreadLocal对象。该对象在线程的整个生命周期中一直存在。

 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>

由于我们可以轻松跟踪方法流程:

  • 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)将跟踪打印到单独的日志文件中。

感谢阅读!

使用Spring Boot和AspectJ实现方法跟踪基础结构的更多相关文章

  1. Spring Boot 初始化运行特定方法

    Spring Boot提供了两种 “开机自启动” 的方式,ApplicationRunner和CommandLineRunner 这两种方式的目的是为了满足,在容器启动时like执行某些方法.我们可以 ...

  2. Spring Boot 2.0 + zipkin 分布式跟踪系统快速入门

    原文:https://www.jianshu.com/p/9bfe103418e2 注意 Spring Boot 2.0之后,使用EnableZipkinServer创建自定义的zipkin服务器已经 ...

  3. Spring boot 打包瘦身方法

    背景 随着spring boot 的流行.越来越多的来发着选择使用spring boot 来发 web 应用. 不同于传统的 web 应用 需要 war 包来发布应用. spring boot 应用可 ...

  4. Spring Boot 之异步执行方法

    前言: 最近的时候遇到一个需求,就是当服务器接到请求并不需要任务执行完成才返回结果,可以立即返回结果,让任务异步的去执行.开始考虑是直接启一个新的线程去执行任务或者把任务提交到一个线程池去执行,这两种 ...

  5. TDDL与Spring Boot集成Version报错——跟踪与解决

    先说背景:公司采用diamond+tddl,这套技术来做web管理.本人处于好奇率先体验了下spring-boot,于是就有了spring-boot+tddl的组合.但是jar包上线后,屡屡发现一条e ...

  6. spring boot启动后执行方法

    @Componentpublic class InitProject implements ApplicationRunner { private static final Logger logger ...

  7. Spring boot异常统一处理方法:@ControllerAdvice注解的使用、全局异常捕获、自定义异常捕获

    一.全局异常 1.首先创建异常处理包和类 2.使用@ControllerAdvice注解,全局捕获异常类,只要作用在@RequestMapping上,所有的异常都会被捕获 package com.ex ...

  8. Spring boot JPA读取数据库方法

    方法1: 1 StringBuffer sb = new StringBuffer(300); 2 sb.append("SELECT v.id, v.container_number, v ...

  9. Spring Boot重定向的使用方法

    @RequestMapping(value = "/redirect", method = RequestMethod.GET) public void redirecttest( ...

随机推荐

  1. GitHub远程库的搭建以及使用

    GitHub远程库的搭建 一).配置SSH 步骤: 1).注册GitHub账号 2).本地git仓库与远程的GitHub仓库的传输要通过SSH进行加密 3).创建SSH key ​ 1.检查在用户主目 ...

  2. FIddler+Proxifer工具对windows PC客户端进行抓包

    python的大火,带动了python爬虫. 爬虫就必定绕不开抓包. 目前最常见的就是网页抓包了,可以使用chrome进行,或者配合其他抓包软件 fiddler. 小程序有些兴起是,如跳一跳之类的,也 ...

  3. 记一次Pod中java进程内存“异常”消耗

    背景 环境:openshift3.11 开发反映部署在容器中的java应用内存持续增长,只升不降,具体为: java应用部署在容器中,配置的jvm参数为-Xms1024m -Xmx1024m,容器me ...

  4. 【2018寒假集训 Day2】【动态规划】又上锁妖塔

    又上锁妖塔 (tower.in/tower.out) [题目描述] 小D在X星买完了想要的东西,在飞往下一个目的地的途中,正无聊的他转头看了看身边的小A,发现小A正在玩<仙剑>,可是小A很 ...

  5. 音视频入门-14-JPEG文件格式详解

    * 音视频入门文章目录 * JPEG 文件格式解析 JPEG 文件使用的数据存储方式有多种.最常用的格式称为 JPEG 文件交换格式(JPEG File Interchange Format,JFIF ...

  6. 01_Numpy基本使用

    1.Numpy读取txt/csv文件 读取数据 import numpy as np # numpy打开本地txt文件 world_alcohol = np.genfromtxt("D:\\ ...

  7. nginx离线部署脚本

    #! /bin/bashbasepath=$(cd `dirname $0`; pwd)nginx_path=/usr/localfile_name=nginxecho "--------- ...

  8. Spring Data Jpa的四种查询方式

    一.调用接口的方式 1.基本介绍 通过调用接口里的方法查询,需要我们自定义的接口继承Spring Data Jpa规定的接口 public interface UserDao extends JpaR ...

  9. UINavigationController-自定义导航栏标题按钮.

    见视频0416 自定义导航栏标题按钮,在Bar Button Item中加入UIButton,设置UIButton的图片和标题,还可以自定义自定义UIButton实现特效按钮.

  10. 02 JavaScript数据类型、类型转换、注释

    JavaScript 数据类型 JavaScript 变量能够保存多种数据类型:数值.字符串值.数组.对象.undefined.null等等 var length = 7; // 数字 var las ...