springboot利用AOP完成日志统计的详细步骤

网友投稿 691 2022-11-12

springboot利用AOP完成日志统计的详细步骤

springboot利用AOP完成日志统计的详细步骤

目录1、创建日志表2、创建实体类3、创建枚举类4、创建自定义注解5、获取ip的util6、线程池util7、HttpServletRequest实现类8、添加过滤器9、添加AOP核心类10、接口测试

步骤写的很详细,可以直接复制拿来用的,其中用到了过滤器、自定义注解以及AOP切面,来完成日志记录统计,感兴趣的收藏起来,以后遇到了可以直接用。

可能步骤会比较多,但是整体跟着思路下来,应该没什么大问题的。

项目用到了过滤器,可能有的人会不理解,之所以用过滤器是因为想要在日志记录post请求的json数据

请求的时候,是通过request的body来传输的。在AOP后置方法中获取request里面的body,是取不到,直接为空。

原因很简单:因为是流。想想看,java中的流也是只能读一次,因为我是在AOP后置方法获取的,控制器实际上已经读过了一次,后置方法再读自然为空了。所以用过滤器来进行解决了这个问题。

1、创建日志表

这里我用的是mysql,假如您用的别的数据库,可以自行根据数据库类型进行修改。

CREATE TABLE `log` (

`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',

`create_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAUTgVuMJuSLT NULL COMMENT '创建人',

`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',

`update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '最近更新时间',

`update_time` datetime NULL DEFAULT NULL COMMENT '最近更新人',

`update_count` int(11) NULL DEFAULT NULL COMMENT '更新次数',

`delete_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标志',

`delete_time` datetime NULL DEFAULT NULL COMMENT '删除日期',

`delete_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除人',

`cost_time` int(11) NULL DEFAULT NULL COMMENT '花费时间',

`ip` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'ip',

`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '日志描述',

`request_param` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数',

`request_json` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求json数据',

`request_type` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求类型',

`request_url` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求路径',

`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求用户',

`operation_type` int(3) NULL DEFAULT NULL COMMENT '操作类型',

PRIMARY KEY (`id`) USING BTREE

) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2、创建实体类

我的项目运用到了mybatisplus、swagger、lombok,你们可以根据自己项目框架写对应的实体类。BaseModel 是我们封装了一个基础实体类,专门存放关于操作人的信息,然后实体类直接继承。

import com.baomidou.mybatisplus.annotation.TableField;

import com.baomidou.mybatisplus.annotation.TableName;

import io.swagger.annotations.ApiModelProperty;

import cn.org.xaas.mybatis.model.BaseModel;

import lombok.Data;

import lombok.ToString;

@TableName(value = "log")

@Data

@ToString(callSuper = true)

public class Log extends BaseModel {

@ApiModelProperty(value = "花费时间")

@TableField(value = "cost_time")

private Integer costTime;

@ApiModelProperty(value = "ip")

@TableField(value = "ip")

private String ip;

@ApiModelProperty(value = "日志描述")

@TableField(value = "description")

private String description;

@ApiModelProperty(value = "请求参数")

@TableField(value = "request_param")

private String requestParam;

@ApiModelProperty(value = "请求json数据")

@TableField(value = "request_json")

private String requestJson;

@ApiModelProperty(value = "请求类型")

@TableField(value = "request_type")

private String requestType;

@ApiModelProperty(value = "请求路径")

@TableField(value = "request_url")

private String requestUrl;

@ApiModelProperty(value = "请求用户")

@TableField(value = "username")

private String username;

@ApiModelProperty(value = "操作类型")

@TableField(value = "operation_type")

private Integer operationType;

}

3、创建枚举类

用来记录日志操作类型

public enum OperationType {

/**

* 操作类型

*/

UNKNOWN("unknown"),

DELETE("delete"),

SELECT("select"),

UPDATE("update"),

INSERT("insert");

OperationType(String s) {

this.value = s;

}

private String value;

public String getValue() {

return value;

}

public void setValue(String value) {

this.value = value;

}

}

4、创建自定义注解

import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.METHOD})//作用于参数或方法上

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface SystemLog {

/**

* 日志名称

*

* @return

*/

String description() default "";

/**

* 操作类型

*

* @return

*/

OperationType type() default OperationType.UNKNOWN;

}

5、获取ip的util

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import java-.InetAddress;

import java-.UnknownHostException;

@Slf4j

@Component

public class IpInfoUtil {

/**

* 获取客户端IP地址

*

* @param request 请求

* @return

*/

public String getIpAddr(HttpServletRequest request) {

String ip = request.getHeader("x-forwarded-for");

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

ip = request.getHeader("Proxy-Client-IP");

}

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

ip = request.getHeader("WL-Proxy-Client-IP");

}

if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {

ip = request.getRemoteAddr();

if ("127.0.0.1".equals(ip)) {

//根据网卡取本机配置的IP

InetAddress inet = null;

try {

inet = InetAddress.getLocalHost();

} catch (UnknownHostException e) {

e.printStackTrace();

}

ip = inet.getHostAddress();

}

}

// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割

if (ip != null && ip.length() > 15) {

if (ip.indexOf(",") > 0) {

ip = ip.substring(0, ip.indexOf(","));

}

}

if ("0:0:0:0:0:0:0:1".equals(ip)) {

ip = "127.0.0.1";

}

return ip;

}

}

6、线程池util

利用线程异步记录日志。所以直接用了一个util维护线程池。

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

public class ThreadPoolUtil {

/**

* 线程缓冲队列

*/

private static BlockingQueue bqueue = new ArrayBlockingQueue(100);

/**

* 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量

*/

private static final int SIZE_CORE_POOL = 5;

/**

* 线程池维护线程的最大数量

*/

private static final int SIZE_MAX_POOL = 10;

/**

* 线程池维护线程所允许的空闲时间

*/

private static final long ALIVE_TIME = 2000;

private static ThreadPoolExecutor pool = new ThreadPoolExecutor(SIZE_CORE_POOL, SIZE_MAX_POOL, ALIVE_TIME, TimeUnit.MILLISECONDS, bqueue, new ThreadPoolExecutor.CallerRunsPolicy());

static {

pool.prestartAllCoreThreads();

}

public static ThreadPoolExecutor getPool() {

return pool;

}

public static void main(String[] args) {

System.out.println(pool.getPoolSize());

}

}

7、HttpServletRequest实现类

这个就是重写的一个HttpServletRequest类。

import javax.servlet.ReadListener;

import javax.servlet.ServletInputStream;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletRequestWrapper;

import java.io.*;

public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {

private final String body;

/**

* @param request

*/

public BodyReaderRequestWrapper(HttpServletRequest request) {

super(request);

StringBuilder sb = new StringBuilder();

InputStream ins = null;

BufferedReader isr = null;

try {

ins = request.getInputStream();

if (ins != null) {

isr = new BufferedReader(new InputStreamReader(ins));

char[] charBuffer = new char[128];

int readCount = 0;

while ((readCount = isr.read(charBuffer)) != -1) {

sb.append(charBuffer, 0, readCount);

}

} else {

sb.append("");

}

} catch (IOException e) {

e.printStackTrace();

} finally {

try {

if (isr != null) {

isr.close();

}

} catch (IOException e) {

e.printStackTrace();

}

try {

if (ins != null) {

ins.close();

}

} catch (IOException e) {

e.printStackTrace();

}

}

body = sb.toString();

}

@Override

public BufferedReader getReader() throws IOException {

return new BufferedReader(new InputStreamReader(this.getInputStream()));

}

@Override

public ServletInputStream getInputStream() throws IOException {

final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());

ServletInputStream servletIns = new ServletInputStream() {

@Override

public boolean isFinished() {

return false;

}

@Override

public boolean isReady() {

return false;

}

@Override

public void setReadListener(ReadListener readListener) {

}

@Override

public int read() throws IOException {

return byteArrayIns.read();

}

};

return servletIns;

}

}

8、添加过滤器

这个过滤器我添加了一个路径,就是代表需要json日志的接口,可以在list当中添加路径,不需要取request当中json数据的可以不配置。

import javax.servlet.*;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.util.ArrayList;

import java.util.List;

import java.util.regex.Pattern;

public class BodyReaderRequestFilter implements Filter {

private static final Pattern SHOULD_NOT_FILTER_URL_PATTERN;

static {

List urlList = new ArrayList<>();

// 想要通过aop记录request当中body数据的,就需要进行配置路径

urlList.add("(socket/.*)");

urlList.add("(test/test1)");

urlList.add("(test/test2)");

StringBuilder sb = new StringBuilder();

for (String url : urlList) {

sb.append(url);

sb.append("|");

}

sb.setLength(sb.length() - 1);

SHOULD_NOT_FILTER_URL_PATTERN = Pattern.compile(sb.toString());

}

@Override

public void init(FilterConfig filterConfig) {

}

@Override

public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;

HttpServletResponse response = (HttpServletResponse) res;

// 获取访问的url

String servletPath = request.getServletPath();

if (SHOULD_NOT_FILTER_URL_PATTERN.matcher(servletPath).find()) {

BodyReaderRequestWrapper requestWrapper = new BodyReaderRequestWrapper(request);

if (requestWrapper == null) {

filterChain.doFilter(request, response);

} else {

filterChain.doFilter(requestWrapper, response);

}

}else {

filterChain.doFilter(request, response);

}

}

@Override

public void destroy() {

}

}

想要让过滤器生效需要注入到容器当中。

import cn.org.bjca.szyx.xaas.equipment.filter.BodyReaderRequestFilter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

public class MyServerConfig {

@Bean

public FilterRegistrationBean myFilter(){

FilterRegistrationBean registrationBean = new FilterRegistrationBean();

registrationBean.setFilter(new BodyReaderRequestFilter());

return registrationBean;

}

}

9、添加AOP核心类

对于切面,我们可以通过指定包名,进行日志统计,也可以选择根据自定义的注解在方法上添加,然后进行统计,根据自己的实际情况,在切点进行配置即可。

LogDao我是没有提供的,每个项目框架不一样,自行根据情况进行编写,就是保存数据库就可以了。

import cn.hutool.core.util.IdUtil;

import cn.hutool.json.JSONUtil;

import cn.org.xaas.core.util.HeaderSecurityUtils;

import cn.org.xaas.equipment.annotation.SystemLog;

import cn.org.xaas.equipment.dao.LogDao;

import cn.org.xaas.equipment.model.base.Log;

import cn.org.xaas.equipment.utils.IpInfoUtil;

import cn.org.xaas.equipment.utils.ThreadPoolUtil;

import lombok.extern.slf4j.Slf4j;

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.beans.factory.annotation.Autowired;

import org.springframework.core.NamedThreadLocal;

import org.springframework.stereotype.Component;

import javax.servlet.ServletInputStream;

import javax.servlet.http.HttpServletRequest;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.lang.reflect.Method;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

@Aspect

@Component

@Slf4j

public class SystemLogAspect {

private static final ThreadLocal beginTimeThreadLocal = new NamedThreadLocal("ThreadLocal beginTime");

@Autowired

private LogDao logDao;

@Autowired

private IpInfoUtil ipInfoUtil;

@Autowired(required = false)

private HttpServletRequest request;

/**

* Controller层切点,注解方式

*/

//@Pointcut("execution(* *..controller..*Controller*.*(..))")

@Pointcut("@annotation(cn.org.xaas.equipment.annotation.SystemLog)")

public void controllerAspect() {

}

/**

* 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间

*

* @param joinPoint 切点

* @throws InterruptedException

*/

@Before("controllerAspect()")

public void doBefore(JoinPoint joinPoint) throws InterruptedException {

//线程绑定变量(该数据只有当前请求的线程可见)

Date beginTime = new Date();

beginTimeThreadLocal.set(beginTime);

}

/**

* 后置通知(在方法执行之后并返回数据) 用于拦截Controller层无异常的操作

*

* @param joinPoint 切点

*/

@AfterReturning("controllerAspect()")

public void after(JoinPoint joinPoint) {

try {

// 获取操作人,每个系统不一样,一般存储与session,此处就不展示了

String username = HeaderSecurityUtils.getUserName();

// 读取json数据

String openApiRequestData = getJSON(request);

Map requestPahttp://rams = request.getParameterMap();

Log log = new Log();

if (openApiRequestData != null) {

log.setRequestJson(JSONUtil.toJsonStr(openApiRequestData));

}

log.setId(IdUtil.simpleUUID());

log.setUsername(username);

//日志标题

String description = getControllerMethodInfo(joinPoint).get("description").toString();

log.setDescription(description);

//日志类型

log.setOperationType((int) getControllerMethodInfo(joinPoint).get("type"));

//日志请求url

log.setRequestUrl(request.getRequestURI());

//请求方式

log.setRequestType(request.getMethod());

//请求参数

log.setRequestParam(JSONUtil.toJsonStr(requestParams));

//其他属性

log.setIp(ipInfoUtil.getIpAddr(request));

log.setCreateBy(username);

log.setUpdateBy(username);

log.setCreateTime(new Date());

log.setUpdateTime(new Date());

log.setDeleteFlag("0");

//请求开始时间

long beginTime = beginTimeThreadLocal.get().getTime();

long endTime = System.currentTimeMillis();

//请求耗时

Long logElapsedTime = endTime - beginTime;

log.setCostTime(logElapsedTime.intValue());

//持久化(存储到数据或者ES,可以考虑用线程池)

ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logDao));

} catch (Exception e) {

log.error("AOP后置通知异常", e);

}

}

/**

* 获取request的body

*

* @param request

* @return

*/

public String getJSON(HttpServletRequest request) {

ServletInputStream inputStream = null;

InputStreamReader inputStreamReader = null;

BufferedReader streamReader = null;

StringBuilder responseStrBuilder = new StringBuilder();

try {

inputStream = request.getInputStream();

inputStreamReader = new InputStreamReader(inputStream, "UTF-8");

streamReader = new BufferedReader(inputStreamReader);

String inputStr;

while ((inputStr = streamReader.readLine()) != null) {

responseStrBuilder.append(inputStr);

}

} catch (IOException ioException) {

ioException.printStackTrace();

} finally {

try {

if (inputStream != null) {

inputStream.close();

}

} catch (IOException e) {

e.printStackTrace();

}

try {

if (inputStreamReader != null) {

inputStreamReader.close();

}

} catch (IOException e) {

e.printStackTrace();

}

try {

if (streamReader != null) {

streamReader.close();

}

} catch (IOException e) {

e.printStackTrace();

}

}

return responseStrBuilder.toString();

}

/**

* 保存日志至数据库

*/

private static class SaveSystemLogThread implements Runnable {

private Log log;

private LogDao logDao;

public SaveSystemLogThread(Log esLog, LogDao logDao) {

this.log = esLog;

this.logDao = logDao;

}

@Override

public void run() {

logDao.insert(log);

}

}

/**

* 获取注解中对方法的描述信息 用于Controller层注解

*

* @param joinPoint 切点

* @return 方法描述

* @throws Exception

*/

public static Map getControllerMethodInfo(JoinPoint joinPoint) throws Exception {

Map map = new HashMap(16);

//获取目标类名

String targetName = joinPoint.getTarget().getClass().getName();

//获取方法名

String methodName = joinPoint.getSignature().getName();

//获取相关参数

Object[] arguments = joinPoint.getArgs();

//生成类对象

Class targetClass = Class.forName(targetName);

//获取该类中的方法

Method[] methods = targetClass.getMethods();

String description = "";

Integer type = null;

for (Method method : methods) {

if (!method.getName().equals(methodName)) {

continue;

}

Class[] clazzs = method.getParameterTypes();

if (clazzs.length != arguments.length) {

//比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可TgVuMJuS以重载哦

continue;

}

description = method.getAnnotation(SystemLog.class).description();

type = method.getAnnotation(SystemLog.class).type().ordinal();

map.put("description", description);

map.put("type", type);

}

return map;

}

}

10、接口测试

import cn.org.xaas.equipment.annotation.SystemLog;

import cn.org.xaas.equipment.constant.OperationType;

import org.springframework.web.bind.annotation.*;

@RestController

@RequestMapping("/test")

public class TestController {

@PostMapping("/test1")

@SystemLog(description = "根据id查询某某数据",type = OperationType.SELECT)

public void test1(@RequestParam("id")String id){

System.out.println(id);

}

@PostMapping("/test2")

@SystemLog(description = "根据id查询某某数据,传json",type = OperationType.SELECT)

public void test2(@RequestBody String id){

System.out.println(id);

}

}

调用第一个测试接口:

调用第二个测试接口:

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:IP地址、子网掩码、网络号、主机号、网络地址、主机地址
下一篇:【微服务测试教程】使用Python测试gRPC接口案例
相关文章

 发表评论

暂时没有评论,来抢沙发吧~