使用Spring Boot和AspectJ实现方法跟踪基础结构


在我们的应用程序中,获取方法的堆栈跟踪信息可能会节省很多时间。具有输入输出参数值和方法所花费的时间可以使查找问题变得更加容易。在本文中,我们将研究如何使用Spring Boot,AspectJ和Threadlocal为方法跟踪基础结构实现起点。

您可能还会喜欢: 了解和利用Java堆栈跟踪 在此示例中,我使用了:

  • Spring Boot Starter Web 2.1.7
  • Java 1.8 +
  • AspectJ 1.8
  • Maven 3.2

1.概述 在本教程中,我们将准备一个简单的REST服务,该服务将在书店中检索有关一本书的详细信息。然后,我们将添加一个ThreadLocal 模型,该 模型将在整个线程生命周期中保持堆栈结构。最后,我们将增加一个方面来削减调用堆栈中的方法,以获取输入/输出参数值。让我们开始吧!

项目结构

trace-v1.jpg

2. Maven Dependencies

  • Spring Boot Starter Web — for RESTful services using Spring MVC
  • Spring — for Aspect capabilities
  • The AspectJ weaver introduces advice to Java classes
  • Apache Commons Lang — for string utilities
<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.Implementation 创建一个Spring Boot应用程序 您可以使用这些模板为逐步实现创建一个简单的Spring Boot Application,也可以在此处直接下载最终项目 。

对于IntelliJ:

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

对于Eclipse:

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

简单的休息服务和方法 首先,我们将创建我们的服务。我们会拿书的项目编号作为输入参数,并给予 Title, Price以及 Content 作为一个劳务输出信息。

我们将提供三个简单的服务:

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

标题:鼠与鼠价格:20.0内容:Lorem ipsum内容2。

线程本地模型 现在,我们将准备 Method 对象,该对象将保存任何方法调用的信息。稍后,我们将准备堆栈结构和 ThreadLocal 对象,这些对象将在线程的整个生命周期中保持堆栈结构。

方法:这将是我们的模型对象,它将保留有关方法执行的所有详细信息。它包含方法的输入/输出参数,在该方法中花费的时间以及一个 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:保留main方法的跟踪信息。方法 mainMethod 包含 List<Method>methodList 对象,该对象包含从主方法调用的子方法。

Deque<Method>methodStack是保留方法调用堆栈的对象。它贯穿线程的整个生命周期。调用子方法时,将 Method 对象推到 methodStack,然后返回子方法时,Method 从弹出顶部 对象 methodStack

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:这是在切入点返回方法之后将删除堆栈中顶部方法的方法。

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.Testing and Printing Stack 当我们运行我们的Spring Boot应用程序并发送一个get请求时:

http:// localhost:8080 / getBookInfo / 2

回报将是:

标题:鼠与人的价格:20.0内容:Lorem ipsum内容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 用输入2调用方法

    • getBookInfo getTitle 输入2的调用 方法

    • getTitle 在3毫秒内返回输出“鼠与人”。

    • getBookInfo getPrice 输入2的呼叫

    • getPrice 在1毫秒内返回20.0输出。

    • getBookInfo getContent 输入2的呼叫

    • getContent 返回输出“ Lorem ipsum内容2”。在0毫秒内

    • getBookInfo 方法返回输出“标题:男女”名称:价格:20.0内容:Lorem ipsum内容2。在6毫秒内

我们的跟踪实现适用于我们简单的REST服务调用。


原文链接:https://codingdict.com/