springboot 手写一个请求记录日志的功能

博主由于工作需要,写了一个http请求记录日志的功能,就是简单记录一下请求时间,请求路径,请求方法,携带的参数信息等等,下面是效果图:
目录结构:
目录结构
内容信息:

2019-09-30 10:49:02:086	method:POST	path:/jmt/cgi-bin/lottery/init/10201/205927	pathParam:(userId:13095)	bodyParam:{"shopId": "3213"}
2019-09-30 10:49:26:034	method:PUT	path:/jmt/cgi-bin/lottery/init/10201/205927	pathParam:(userId:13095)	bodyParam:{"shopId": "3213"}
2019-09-30 10:49:34:676	method:GET	path:/jmt/cgi-bin/lottery/draw/10201/205927	pathParam:(userId:13095)	
2019-09-30 10:49:39:442	method:DELETE	path:/jmt/cgi-bin/lottery/draw/10201/205927	pathParam:(userId:13095)	

博主这里使用了springBoot2.0.3。

其实从实现思路上没什么难,无非就是写一个拦截器或者过滤器,启动时,创建日志文件,通过ServletRequest抓取信息打到文件就可以了。

但这里面有两个坑点:
1.建议别用拦截器,使用拦截器时如果使用错误的路径或错误的动作(get.post等等)访问系统,造成404或405错误,会被spring转发到一个/error的路径,而且正确的访问路径不会经过我们定义的拦截器,反而拦截到了/error请求,过滤器就不会存在这个问题。
2.关于获取请求消息体参数的,首先请求消息体的内容是存在流里面的,通过request.getInputStream()方法获取这个流,而流的特性是只能读取一次的,那么如果我们在过滤器里面读过了这个流抓取了消息体内容,那么在我们的控制层,springmvc也要去读取这个流,帮我们加载控制层方法参数时就会出问题了。

第二个坑点的解决方法就是重写request.getInputStream()方法,通过一定手段让这个方法返回的流一直可以读到消息体内容就可以了,这里博主先上代码;
主要有两个类,一个过滤器,一个ServletRequest的装饰类;

/**
 * 添加http请求记录日志的过滤器
 * 开启定时任务,定时打印日志内容,定时生成日志文件
 *
 * @author zeng wenbin
 * @date Created in 2019/9/29
 */
@WebFilter(urlPatterns = "/*", filterName = "requestLogFilter")
@EnableScheduling
public class RequestLogFilter implements Filter {

	/**
	 * 线程同步List
	 * 用于做http请求记录缓存,定时将内容往日志中记录一次并清空
	 */
	private List<String> logs_1 = Collections.synchronizedList(new LinkedList<>());
	/**
	 * 线程同步List
	 * 用于做http请求记录缓存,定时将内容往日志中记录一次并清空
	 */
	private List<String> logs_2 = Collections.synchronizedList(new LinkedList<>());
	/**
	 * 用于判断存储数据到集合或写入数据到日志时用logs_1还是logs_2
	 * 存储数据到list使用logs_1时,写入数据到日志使用logs_2
	 * 这样设计能提升性能
	 */
	private Boolean flag = true;
	/**
	 * 日志内容日期格式
	 */
	private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
	/**
	 * 日志文件位置
	 */
	@Value("${jmtg.data.logPosition:logs/lottery/}")
	private String logPosition;
	/**
	 * 打印流,用于输出数据到日志文件
	 */
	private PrintWriter writer;
	/**
	 * 生产环境暂不使用此功能
	 */
	@Value("${jmtg.data.isProd:true}")
	private Boolean isProd;

	private final static Logger log = LoggerFactory.getLogger(RequestLogFilter.class);

	/**
	 * 项目初始化时解析http请求日志位置
	 * 生成日志文件,默认项目目录下logs/lottery/requestLog.日期.log
	 * 初始化PrintWriter
	 *
	 * @throws IOException
	 */
	@Override
	public void init(FilterConfig filterConfig) {
		try {
			//存一份日志目录,在最后还原logPosition
			String position = logPosition;
			//使用java的文件分隔符File.separator来代替分隔符“/”,保证代码在多系统下的兼容性
			String[] split = logPosition.split("/");
			//生成文件目录,文件名
			StringBuilder builder = new StringBuilder();
			for (String aSplit : split) {
				builder.append(aSplit).append(File.separator);
			}
			DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
			String fileName = "requestLog." + format.format(new Date()) + ".log";
			logPosition = builder.append(fileName).toString();
			File logFile = new File(logPosition);
			if (!logFile.exists()) {
				logFile.getParentFile().mkdirs();
				logFile.createNewFile();
			}
			//生成流,内容采用追加的形式
			OutputStream outputStream = new FileOutputStream(logFile, true);
			writer = new PrintWriter(outputStream);
			//还原logPosition
			logPosition = position;
		} catch (Exception e) {
			log.error("获取http请求日志打印流失败!", e);
			throw new RuntimeException("获取http请求日志打印流失败!");
		}
	}

	/**
	 * 过滤器核心方法,请求到来时触发
	 * 		记录请求信息到List集合
	 * @param servletRequest	请求对象
	 * @param servletResponse	响应对象
	 * @param filterChain	过滤器链
	 * @throws IOException IO异常
	 * @throws ServletException Servlet异常
	 */
	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) servletRequest;

		StringBuilder builder = new StringBuilder();

		builder.append(dateFormat.format(new Date())).append("\t");

		//请求方式
		builder.append("method:").append(request.getMethod()).append("\t");


		//请求路径
		builder.append("path:").append(request.getRequestURI()).append("\t");

		//获取请求参数
		builder.append("pathParam:");
		Enumeration<String> parameterNames = request.getParameterNames();
		int i = 0;
		//路径参数
		while (parameterNames.hasMoreElements()) {
			if (i == 0) {
				builder.append("(");
			}
			String paramName = parameterNames.nextElement();
			builder.append(paramName).append(":").append(request.getParameter(paramName)).append(",");
			i++;
		}
		if (i > 0) {
			builder.deleteCharAt(builder.length() - 1);
			builder.append(")").append("\t");
		}
		//生产环境暂不处理消息体参数
		if (!isProd) {
			//消息体参数
			if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) {
				//将servletRequest转换为我们编写的request装饰类 RequestWrapper
				RequestWrapper wrapper = new RequestWrapper((HttpServletRequest) servletRequest);
				request = wrapper;
				builder.append("bodyParam:").append(wrapper.getBody());
			}
		}
		//一个小优化,由于logs_1和2都是线程同步的
		//当logs_1的内容正被输出到文件时,采用logs_2来存储请求信息,反之亦然,可以提升效率
		if (flag) {
			logs_1.add(builder.toString());
		} else {
			logs_2.add(builder.toString());
		}
		filterChain.doFilter(request, servletResponse);
	}

	/**
	 * 过滤器销毁时关闭打印流
	 */
	@Override
	public void destroy() {
		writer.close();
	}

	/**
	 * 定时写入http请求日志,默认10秒每次
	 *
	 * @throws IOException
	 */
	@Scheduled(cron = "${jmtg.data.recordTime:*/10 * * * * ?}")
	public void record() {
		flag = !flag;
		if (flag) {
			for (String data : logs_2) {
				writer.println(data);
			}
			logs_2.clear();
			writer.flush();
		} else {
			for (String data : logs_1) {
				writer.println(data);
			}
			logs_1.clear();
			writer.flush();
		}
	}

	/**
	 * 默认每3天生成一个新的日志文件
	 * 向新的文件输入日志记录
	 *
	 * @throws IOException IO异常
	 */
	@Scheduled(cron = "${jmtg.data.updateLogTime:0 0 0 */3 * ?}")
	public void updateLog() {
		writer.close();
		init(null);
	}
}

/**
 * HttpServletRequest的装饰类
 * 	编写目的:重写getInputStream方法,使得重复调用getInputStream获取到的输入流都是可以读的
 * 	编写原因:由于流的特性,流只能被读取一次,
 * 		而在此项目中,我们可能需要在RequestLogFilter中读取一次流获取消息体的参数,输出到请求日志中
 * 		而在controller层,springMvc会进行读取第二次,将消息体数据解析为参数。
 *
 * @author zeng wenbin
 * @date Created in 2019/9/29
 */
public class RequestWrapper extends HttpServletRequestWrapper {
	/**
	 * 消息体数据,此项目都是json字符串
	 */
	private final String body;

	/**
	 * 传入request,获取消息体流,获取流数据转换为字符串赋值给body
	 *
	 * @param request HttpServletRequest
	 */
	public RequestWrapper(HttpServletRequest request) {
		super(request);
		StringBuilder stringBuilder = new StringBuilder();
		BufferedReader bufferedReader = null;
		InputStream inputStream = null;
		try {
			inputStream = request.getInputStream();
			if (inputStream != null) {
				bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
				char[] charBuffer = new char[128];
				int bytesRead;
				while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
					stringBuilder.append(charBuffer, 0, bytesRead);
				}
			}
		} catch (IOException ex) {
			throw new RuntimeException("ServletRequest装饰类初始化失败!");
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				}
				catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				}
				catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		body = stringBuilder.toString();
	}

	/**
	 * 重写getInputStream方法
	 * 	将body数据转回字节并生成流返回
	 *
	 * @return 数据输入流
	 */
	@Override
	public ServletInputStream getInputStream() {
		final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
		ServletInputStream servletInputStream = 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 byteArrayInputStream.read();
			}
		};
		return servletInputStream;
	}

	/**
	 * 获取数据输入缓存流
	 * @return
	 * @throws IOException
	 */
	@Override
	public BufferedReader getReader() throws IOException {
		return new BufferedReader(new InputStreamReader(this.getInputStream()));
	}

	/**
	 * 获取流数据
	 * @return 字符串流行
	 */
	public String getBody() {
		return this.body;
	}
}

如上实现的功能就是默认每天在logs/lottery/目录下生成一个requestLog.yyyy-MM-dd.log的文件,文件生成频率每天一次,文件内容写入频率,每10秒一次,对于文件位置,生成频率和写入频率都是可配置的,功能还是比较简单,如有需要,大家可借鉴扩展。

对于上述的第二个坑点,代码里写的很明白了,写一个ServletRequest的装饰类,在构造里传入我们在过滤器里拿到的request,然后取出其输入流,将消息体内容存下来,然后重写getIInputStream()方法,方法里将存下来的消息体内容存到新建的流里面再把这个流return回去,最后在过滤器中,调用filterChain.doFilter方法的时候,request传我们的装饰器类,这样springmvc取到的输入流就可用了。

这里也顺便扩展一下,对于我们传进去自定义的request装饰类不用有任何担心,调用这个对象getInputStream()以外的所有方法最后走的都是我们在doFilter上接收到的request对象的方法,其实我们接收到的request也是被重写过的,也是一个装饰器类,而只要我们的装饰器类的构造里调用了父类构造,即那一句super(request),这就够了,其他的事情,方法怎么调用已经被安排的妥妥当当。

最后由于我们使用到了过滤器,所以在启动类上需要加上@ServletComponentScan注解,否则我们的过滤器是不会被扫描到的。
在这里插入图片描述

当然可以!以下是一个简单的示例,展示了如何手写一个Spring Boot Starter: 首先,创建一个 Maven 项目,并添加以下依赖项: ```xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.5.4</version> </dependency> </dependencies> ``` 接下来,创建一个自定义的自动配置类,用于配置你的 Starter: ```java @Configuration @EnableConfigurationProperties(MyStarterProperties.class) public class MyStarterAutoConfiguration { private final MyStarterProperties properties; public MyStarterAutoConfiguration(MyStarterProperties properties) { this.properties = properties; } // 在此处定义你的自动配置逻辑 @Bean public MyStarterService myStarterService() { return new MyStarterService(properties); } } ``` 然后,创建一个属性类,用于将外部配置绑定到属性上: ```java @ConfigurationProperties("my.starter") public class MyStarterProperties { private String message; // 提供 getter 和 setter } ``` 最后,创建一个自定义的服务类,该服务类将在你的 Starter 中使用: ```java public class MyStarterService { private final MyStarterProperties properties; public MyStarterService(MyStarterProperties properties) { this.properties = properties; } public void showMessage() { System.out.println(properties.getMessage()); } } ``` 现在,你的 Spring Boot Starter 已经准备就绪了!你可以将其打包并使用在其他 Spring Boot 项目中。在其他项目的 `pom.xml` 文件中,添加你的 Starter 依赖: ```xml <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>my-starter</artifactId> <version>1.0.0</version> </dependency> </dependencies> ``` 然后,在你的应用程序中使用 `MyStarterService`: ```java @SpringBootApplication public class MyApplication implements CommandLineRunner { private final MyStarterService myStarterService; public MyApplication(MyStarterService myStarterService) { this.myStarterService = myStarterService; } public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Override public void run(String... args) throws Exception { myStarterService.showMessage(); } } ``` 这样,你就成功地创建了一个简单的 Spring Boot Starter!当其他项目引入你的 Starter 并运行时,将会输出预定义的消息。 当然,这只是一个简单的示例,真实的 Starter 可能包含更多的配置和功能。你可以根据自己的需求进行扩展和定制。希望对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值