springboot利用AOP完成日志统计的详细步骤
步骤写的很详细,可以直接复制拿来用的,其中用到了过滤器、自定义注解以及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 DEFAULT 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 {
String description() default "";
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.net.InetAddress;
import java.net.UnknownHostException;
@Slf4j
@Component
public class IpInfoUtil {
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<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(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;
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<String> 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<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
@Autowired
private LogDao logDao;
@Autowired
private IpInfoUtil ipInfoUtil;
@Autowired(required = false)
private HttpServletRequest request;
//@Pointcut("execution(* *..controller..*Controller*.*(..))")
@Pointcut("@annotation(cn.org.xaas.equipment.annotation.SystemLog)")
public void controllerAspect() {
}
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws InterruptedException {
//线程绑定变量(该数据只有当前请求的线程可见)
Date beginTime = new Date();
beginTimeThreadLocal.set(beginTime);
}
@AfterReturning("controllerAspect()")
public void after(JoinPoint joinPoint) {
try {
// 获取操作人,每个系统不一样,一般存储与session,此处就不展示了
String username = HeaderSecurityUtils.getUserName();
// 读取json数据
String openApiRequestData = getJSON(request);
Map<String, String[]> requestParams = 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);
}
}
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);
}
}
public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception {
Map<String, Object> map = new HashMap<String, Object>(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) {
//比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可以重载哦
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);
}
}
调用第一个测试接口:
调用第二个测试接口:
到此这篇关于springboot利用AOP完成日志统计的文章就介绍到这了,更多相关springboot日志统计内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341