Filter实现请求日志记录

将锁有得外部访问都记录在日志文件里面,设计这个功能是为了(为什么):

1. 在不引入Promentheus进行接口监控时,基于日志文件就可以实现整个项目得监控。

2. 当出现问题时,可以基于此进行流量重放。

效果如下(预期):

来看一下请求日志记录得实现。

技术方案(选型)

如果单纯的将这个记录接口的请求信息,当作一个普通的需求来设计,我们可以怎么来实现呢?

        基于过滤器Filter,来拦截web请求,记录请求相关信息。

        基于AOP来实现方法拦截,借助@Around来实现请求方法执行前后增强,记录请求相关的信息

方案的选择?

Filter过滤器方案

关于过滤器的知识点,可以参考之前文章。若使用过滤器,则主要就是拦截web请求,具体的实现流程如下:

在过滤器的doFilter方法中,划分为三块:

  1.         doBefore:表示将请求转发到Controller执行之前
  •                 记录开始执行时间
  •                 记录请求相关信息
  •         doFilter: 即将请求转发到Controller去执行
  •         doAfter: Controller方法执行完
  •                 记录结束时间,计算执行耗时
  •                 日志输出

使用这种方式的优缺点比较突出,优点是适用性强,实现简单,缺点是只能记录Controller的请求相关信息,如果我们想统计某个Service方法、Mapper方方法,那么这种方法不太合适

AOP切面方案

若使用AOP来实现,则关键点在于我需要拦截那些方法,即定义切点

基本策略与前面差不多,不过有几个关键点

        定义切点:可以是直接拦截包路径方式,也可以是配合自定义注解,拦截某些特定注解的方式

  •         使用Around环绕方式
  •         使用AOP来实现的优缺点也比较明显
  • 优点:
  • 灵活性高,可以拦截任何共有方法
  • 缺点:
  • 需要自定义切点,通常不太容易一次编写,所有项目适用。

实现实例(Filter方案)

实现类

包路径:com/github/paicoding/forum/web/hook/filter/ReqRecordFilter.java

package com.github.paicoding.forum.web.hook.filter;import com.github.paicoding.forum.api.model.context.ReqInfoContext;
import com.github.paicoding.forum.core.util.CrossUtil;
import com.github.paicoding.forum.core.util.IpUtil;
import com.github.paicoding.forum.service.statistics.service.StatisticsSettingService;
import com.github.paicoding.forum.web.global.GlobalInitService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;/*** 1. 请求参数日志输出过滤器* 2. 判断用户是否登录** @date 2022/7/6*/
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "reqRecordFilter", asyncSupported = true)
public class ReqRecordFilter implements Filter {private static Logger REQ_LOG = LoggerFactory.getLogger("req");@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {long start = System.currentTimeMillis();HttpServletRequest request = null;try {//构建请求上下文request = this.initReqInfo((HttpServletRequest) servletRequest);CrossUtil.buildCors(request, (HttpServletResponse) servletResponse);filterChain.doFilter(request, servletResponse);} finally {//根据请求上下文,输出请求日志buildRequestLog(ReqInfoContext.getReqInfo(), request, System.currentTimeMillis() - start);ReqInfoContext.clear();}}private HttpServletRequest initReqInfo(HttpServletRequest request) {String uri = request.getRequestURI();if (uri.startsWith("/js/") || uri.startsWith("/css/") || uri.endsWith(".js") || uri.endsWith(".css")) {// 静态资源直接放行return request;}try {ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();reqInfo.setHost(request.getHeader("host"));reqInfo.setPath(request.getPathInfo());reqInfo.setReferer(request.getHeader("referer"));reqInfo.setClientIp(IpUtil.getClientIp(request));reqInfo.setUserAgent(request.getHeader("User-Agent"));request = this.wrapperRequest(request, reqInfo);// 初始化登录信息globalInitService.initLoginUser(reqInfo);ReqInfoContext.addReqInfo(reqInfo);} catch (Exception e) {log.error("init reqInfo error!", e);}return request;}private void buildRequestLog(ReqInfoContext.ReqInfo req, HttpServletRequest request, long costTime) {// fixme 过滤不需要记录请求日志的场景if (request == null|| req == null|| request.getRequestURI().endsWith("css")|| request.getRequestURI().endsWith("js")|| request.getRequestURI().endsWith("png")|| request.getRequestURI().endsWith("ico")|| request.getRequestURI().endsWith("svg")) {return;}StringBuilder msg = new StringBuilder();msg.append("method=").append(request.getMethod()).append("; ");if (StringUtils.isNotBlank(req.getReferer())) {msg.append("referer=").append(URLDecoder.decode(req.getReferer())).append("; ");}msg.append("remoteIp=").append(req.getClientIp());msg.append("; agent=").append(req.getUserAgent());if (req.getUserId() != null) {// 打印用户信息msg.append("; user=").append(req.getUserId());}msg.append("; uri=").append(request.getRequestURI());if (StringUtils.isNotBlank(request.getQueryString())) {msg.append('?').append(URLDecoder.decode(request.getQueryString()));}msg.append("; payload=").append(req.getPayload());msg.append("; cost=").append(costTime);REQ_LOG.info("{}", msg);// 保存请求计数statisticsSettingService.saveRequestCount(req.getClientIp());}private HttpServletRequest wrapperRequest(HttpServletRequest request, ReqInfoContext.ReqInfo reqInfo) {if (!HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) {return request;}//封装请求参数BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);reqInfo.setPayload(requestWrapper.getBodyString());return requestWrapper;}}

排除静态资源

因为pc前台的网页也是集成在项目中的,因此在我们实际的日志输出时,需要将一些静态资源访问给排除掉,主要是基request.getRequestURI后缀来进行过滤的。

    private boolean isStaticURI(HttpServletRequest request) {return request == null|| request.getRequestURI().endsWith("css")|| request.getRequestURI().endsWith("js")|| request.getRequestURI().endsWith("png")|| request.getRequestURI().endsWith("ico")|| request.getRequestURI().endsWith("svg")|| request.getRequestURI().endsWith("min.js.map")|| request.getRequestURI().endsWith("min.css.map");}

上面这种方式虽然实现简单,但是也有缺陷:

  • 如静态资源请求带url参数
  • 除上述这几种静态资源资源之外还有(XML、MP3等)

请求上下文

来看一下,请求上下文的构建,主要是基于HttpServletRequest来获取相关参数

 private HttpServletRequest initReqInfo(HttpServletRequest request) {String uri = request.getRequestURI();if (uri.startsWith("/js/") || uri.startsWith("/css/") || uri.endsWith(".js") || uri.endsWith(".css")) {// 静态资源直接放行return request;}try {ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();reqInfo.setHost(request.getHeader("host"));reqInfo.setPath(request.getPathInfo());reqInfo.setReferer(request.getHeader("referer"));reqInfo.setClientIp(IpUtil.getClientIp(request));reqInfo.setUserAgent(request.getHeader("User-Agent"));//传参的封装的处理: 主要是为了避免post的输入流,读取一次后无法在获取的问题request = this.wrapperRequest(request, reqInfo);// 初始化登录信息globalInitService.initLoginUser(reqInfo);ReqInfoContext.addReqInfo(reqInfo);} catch (Exception e) {log.error("init reqInfo error!", e);}return request;}

重点关注两个:

1.请求者的ip获取

实现类的核心如下(通用的工具类,需要注意的是若使用nginx做反向代理的话,那么请不要把用户的信息吃掉了,否则下面这个方法拿不到)
 

    /*** 获取请求来源的ip地址** @param request* @return*/public static String getClientIp(HttpServletRequest request) {try {String xIp = request.getHeader("X-Real-IP");String xFor = request.getHeader("X-Forwarded-For");if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) {//多次反向代理后会有多个ip值,第一个ip才是真实ipint index = xFor.indexOf(",");if (index != -1) {return xFor.substring(0, index);} else {return xFor;}}xFor = xIp;if (StringUtils.isNotEmpty(xFor) && !UNKNOWN.equalsIgnoreCase(xFor)) {return xFor;}if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {xFor = request.getHeader("Proxy-Client-IP");}if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {xFor = request.getHeader("WL-Proxy-Client-IP");}if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {xFor = request.getHeader("HTTP_CLIENT_IP");}if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {xFor = request.getHeader("HTTP_X_FORWARDED_FOR");}if (StringUtils.isBlank(xFor) || UNKNOWN.equalsIgnoreCase(xFor)) {xFor = request.getRemoteAddr();}if ("localhost".equalsIgnoreCase(xFor) || "127.0.0.1".equalsIgnoreCase(xFor) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(xFor)) {return getLocalIp4Address();}return xFor;} catch (Exception e) {log.error("get remote ip error!", e);return "x.0.0.1";}}

2.请求参数封装

首先需要理解一下为啥需要封装请求参数?

对于post之类的请求,若是传参json,那么需要从HttpServletRequest的请求流中读取,但是这个流是一次性的,如果打印日志的时候把这个参数读取出来了,那么在实际业务中,就拿不到对应的参数了,为了解决这个问题,我们需要将这个InputStream进行封装一下,所以技术派封装了一个BodyReaderHttpServletRequestWrapper类,来封装一下请求。

核心实现如下

  • 只拿post,put请求,非二进制、非文件上传、非表单数据上传的场景
  • 将请求参数读取到 byte[] body
  • 基于body封装 ServletInputStream,用于后续的传参获取。

package com.github.paicoding.forum.web.hook.filter;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;/*** post 流数据封装,避免因为打印日志导致请求参数被提前消费** todo 知识点: 请求参数的封装,避免输入流读取一次就消耗了** @author YiHui* @date 2022/7/6*/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {private static final List<String> POST_METHOD = Arrays.asList("POST", "PUT");private final Logger logger = LoggerFactory.getLogger(this.getClass());private final byte[] body;private final String bodyString;public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {super(request);if (POST_METHOD.contains(request.getMethod()) && !isMultipart(request) && !isBinaryContent(request) && !isFormPost(request)) {bodyString = getBodyString(request);body = bodyString.getBytes(StandardCharsets.UTF_8);} else {bodyString = null;body = null;}}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {if (body == null) {return super.getInputStream();}final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}public boolean hasPayload() {return bodyString != null;}public String getBodyString() {return bodyString;}private String getBodyString(HttpServletRequest request) {BufferedReader br;try {br = request.getReader();} catch (IOException e) {logger.warn("Failed to get reader", e);return "";}String str;StringBuilder body = new StringBuilder();try {while ((str = br.readLine()) != null) {body.append(str);}} catch (IOException e) {logger.warn("Failed to read line", e);}try {br.close();} catch (IOException e) {logger.warn("Failed to close reader", e);}return body.toString();}/*** is binary content** @param request http request* @return ret*/private boolean isBinaryContent(final HttpServletRequest request) {return request.getContentType() != null &&(request.getContentType().startsWith("image") || request.getContentType().startsWith("video") ||request.getContentType().startsWith("audio"));}/*** is multipart content** @param request http request* @return ret*/private boolean isMultipart(final HttpServletRequest request) {return request.getContentType() != null && request.getContentType().startsWith("multipart/form-data");}private boolean isFormPost(final HttpServletRequest request) {return request.getContentType() != null && request.getContentType().startsWith("application/x-www-form-urlencoded");}
}

日志输出

最后再看一下日志输出,我们直接将上面封装的请求相关信息,按照具体的日志格式进行打印

   private void buildRequestLog(ReqInfoContext.ReqInfo req, HttpServletRequest request, long costTime) {if (req == null || isStaticURI(request)) {return;}StringBuilder msg = new StringBuilder();msg.append("method=").append(request.getMethod()).append("; ");if (StringUtils.isNotBlank(req.getReferer())) {msg.append("referer=").append(URLDecoder.decode(req.getReferer())).append("; ");}msg.append("remoteIp=").append(req.getClientIp());msg.append("; agent=").append(req.getUserAgent());if (req.getUserId() != null) {// 打印用户信息msg.append("; user=").append(req.getUserId());}msg.append("; uri=").append(request.getRequestURI());if (StringUtils.isNotBlank(request.getQueryString())) {msg.append('?').append(URLDecoder.decode(request.getQueryString()));}msg.append("; payload=").append(req.getPayload());msg.append("; cost=").append(costTime);REQ_LOG.info("{}", msg);// 保存请求计数statisticsSettingService.saveRequestCount(req.getClientIp());}

小结

上面介绍了技术派中基于Filter实现的请求日志记录,将所有外部请求,都统一写道req日志文件中,可以基于此,查看一下当前项目的请求情况,接口耗时等。

其中涉及到的知识点如下:

  • Filter基本使用
  • Filter/AOP实现请求参数记录的方案
  • 如何从HttpServletRequest中获取你需要的请求参数
  • 请求参数的封装,允许请求参数InputStream的重复读取
  • 如何获取请求者ip
  • 日志输出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/541739.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 深入Http(2)加密与编码

可以对二进制数据&#xff08;比如图片、视频&#xff09; 经典算法&#xff1a; DES&#xff08;密钥短被弃用了&#xff09; AES &#xff08;密钥很长 很顶&#xff09; 速度快&#xff0c;效率高 IDEA 3DES&#xff08;三重DES&#xff0c;听起来就很慢和重 &#xf…

详解IPD流程之任务书(Charter)

IPD体系是一种全新的产品研发管理模式&#xff0c;它将研发合格产品整个过程分为确保开发做正确的事和如何正确地做事两个阶段。 确保开发做正确的事是指在产品进入研发之初就清晰地定义出有竞争力的产品&#xff0c;核心是确保产品能够对准客户需求&#xff0c;能够给客户带来…

【C++】手撕AVL树

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;能直接手撕AVL树。 > 毒鸡汤&#xff1a;放弃自…

第二十四节 Java 异常处理

什么是异常&#xff1f; 程序运行时&#xff0c;发生的不被期望的事件&#xff0c;它阻止了程序按照程序员的预期正常执行&#xff0c;这就是异常。异常发生时&#xff0c;是任程序自生自灭&#xff0c;立刻退出终止&#xff0c;还是输出错误给用户&#xff1f;或者用C语言风格…

Vue.js+SpringBoot开发天沐瑜伽馆管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 瑜伽课程模块2.3 课程预约模块2.4 系统公告模块2.5 课程评价模块2.6 瑜伽器械模块 三、系统设计3.1 实体类设计3.1.1 瑜伽课程3.1.2 瑜伽课程预约3.1.3 系统公告3.1.4 瑜伽课程评价 3.2 数据库设计3.2.…

使用opencv进行图片分析

opencv学习 一、配置环境并打开编译器 配置opencv在你的任意一个盘里创建一个专属于opencv的文件夹便于学习与整理 打开控制台winr输入cmd&#xff0c;进入后输入conda activate opencv&#xff0c;进入环境以后进入你所设置的opencv文件的盘&#xff0c;我的是D盘&#xff0…

专业款希亦、小米、必胜、云鲸洗地机怎么样?深度测评利弊

洗地机可以说是一种非常实用的清洁工具&#xff0c;尤其是对于那些需要经常给家里地板清洁的人来说。它能够高效、彻底清洁地板&#xff0c;去除顽固污渍、灰尘和细菌&#xff0c;让家居环境更加洁净卫生。可是面对型号繁多的洗地机&#xff0c;我们应该怎么挑选呢&#xff1f;…

综合利用Cisco Packet Tracer模拟器配置园区网

1. 内容 1.在课室交换机中创建各个课室的VLAN&#xff0c;并将1-20端口平均分配给各个课室。 2.使用课室交换机的每个端口只能接入一台计算机&#xff0c;发现违规就丢弃未定义地址的包。3.网络内部使用DHCP分配各课室的IP地址&#xff0c;在课室交换机按照第一题划分的VLAN地…

使用 Docker Compose 快速搭建监控网站 uptime-kuma

有时候需要监控自己搭建的一些网站、服务是否正常运行&#xff0c; 这时候可以考虑使用一个监控网站&#xff0c; 定时的进行检测&#xff0c; 记录网站、服务的运行状态&#xff0c; 在这推荐使用 uptime-kuma。 博主博客 https://blog.uso6.comhttps://blog.csdn.net/dxk539…

【BOM笔记】基本概述、window对象常见事件、定时器、JS执行机制、location/navigator/history对象

文章目录 1 BOM概述1.1 什么是BOM1.2 BOM的构成 2 window 对象的常见事件2.1 窗口加载事件2.2 调整窗口大小事件 3 定时器3.1 setTimeout() 定时器3.2 setInterval() 定时器3.3 this 4 JS 执行机制4.1 JS 是单线程4.2 同步和异步4.3 JS 执行机制 5 location 对象5.1 属性5.2 方…

【bioinformation 6】分子对接

&#x1f31e;欢迎来到机器学习的世界 &#x1f308;博客主页&#xff1a;卿云阁 &#x1f48c;欢迎关注&#x1f389;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f31f;本文由卿云阁原创&#xff01; &#x1f4c6;首发时间&#xff1a;&#x1f339;2024年3月15日&…

APP自动化测试-Appium Inspector入门操作指南

上一篇博客APP自动化测试-入门示例-CSDN博客介绍了APP自动化测试的入门示例,下面详细介绍下Appium 实现的页面元素查看器工具:Appium Inspector的使用方法。 Appium Inspector简介 Appium Inspector 是 Appium 测试框架中的一个工具,用于可视化和调试移动应用程序的 UI 结…