SpringBoot 系列(4)- 记录请求日志

用户头像
引花眠
关注
发布于: 2020 年 10 月 18 日

需求

当我们开发一个网站,比如说一个电商网站,如果能够记录客户的请求信息,对于我们将来的一些操作是非常重要的。以下是一些可记录的点



  1. 用户使用什么浏览器、操作系统、手机等

  2. 用户访问了哪些网页、停留了多长时间等

  3. 用户从哪来的(网页搜索、收藏夹等)

  4. 。。。 通过分析这些数据,可以获知许多对网站运营至关重要的信息。采集的数据越全面,分析就能越精准。甚至于到后期还可以使用大数据的技术进行深度的挖掘,给每个用户推荐合适的产品(就像现在天猫、京东等等)。



如何采集日志

一般来讲采集日志信息有几种方式:



  1. 访问Web服务器时直接记录

  2. 通过在页面嵌入js代码来获取信息(百度统计就是这么干的),ajax发送到后台



拦截日志请求

为了统计或其他目的,我们需要对访问我们网站的数据进行记录,那么在这种情况下,就需要对请求的日志进行拦截。



在Spring终拦截可以有两种方式实现,一种是实现HandlerInterceptor,另一种是使用实现Filter。接下来我将会一一介绍这两种实现方式



HandlerInterceptor 拦截器

这里说的拦截器指的的是SpringMVC框架中的HandlerInterceptor,它是由Spring框架支持的。HandlerInterceptor接口中有三个方法:



  1. preHandle 在请求处理之前进行调用

  2. postHandle 当前请求进行处理之后,也就是Controller 方法调用之后执行

  3. afterCompletion 该方法将在整个请求结束之后执行



在SpringMvc中 Interceptor的调用是链式调用(与Filter类似),在请求中会将适用与该请求的所有拦截器按照其声明的顺序依次调用。



  1. 先依次调用preHandle方法,如果preHandle方法返回true继续调用下一个Interceptor 的preHandle方法,执行完就调用当前请求的Controller方法;如果返回false,则直接返回,不再继续执行。

  2. Controller执行结束之后,视图渲染之前,postHandle会以与preHandle方法相反的顺序开始执行

  3. 当DispatcherServlet渲染了对应的视图之后,开始执行afterCompletion方法。其执行顺序与postHandle相同。 这个方法的主要作用是用于进行资源清理工作的。



public class LogInterceptor implements HandlerInterceptor {
private static final ThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<Long>(
"ThreadLocal StartTime");
private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("ceshi", "shishi");
if (logger.isDebugEnabled()) {
long beginTime = System.currentTimeMillis();// 1、开始时间
startTimeThreadLocal.set(beginTime); // 线程绑定变量(该数据只有当前请求的线程可见)
logger.debug("访问IP:{} 开始计时: {} URI: {}",
StringUtils.getRemoteAddr(request),
new SimpleDateFormat("hh:mm:ss.SSS").format(beginTime),
request.getRequestURI());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null) {
logger.info("ViewName: " + modelAndView.getViewName());
}
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 保存日志
LogUtils.saveLog(request, handler, ex, request.getRequestURI());
// 打印JVM信息。
if (logger.isDebugEnabled()) {
long beginTime = startTimeThreadLocal.get();// 得到线程绑定的局部变量(开始时间)
long endTime = System.currentTimeMillis(); // 2、结束时间
logger.debug(
"访问IP:{} 计时结束:{} 耗时:{} URI: {} 最大内存: {}m 已分配内存: {}m 已分配内存中的剩余空间: {}m 最大可用内存: {}m",
StringUtils.getRemoteAddr(request),
new SimpleDateFormat("hh:mm:ss.SSS").format(endTime),
DateUtils.formatDateTime(endTime - beginTime),
request.getRequestURI(),
Runtime.getRuntime().maxMemory() / 1024 / 1024,
Runtime.getRuntime().totalMemory() / 1024 / 1024,
Runtime.getRuntime().freeMemory() / 1024 / 1024,
(Runtime.getRuntime().maxMemory()
- Runtime.getRuntime().totalMemory()
+ Runtime.getRuntime().freeMemory()) / 1024 / 1024);
}
}
}



使用Filter拦截日志

除了使用 HandlerInterceptor ,也可以使用Filter记录日志,与 HandlerInterceptor 类似,可以在请求前或请求后对日志进行记录。



request请求参数分两种类型:



  1. url中的参数,可以使用request.getParameterMap()获取,没有任何影响

  2. requestBody中的参数,如果在过滤器中getInputStream后就会被消耗掉,后面的程序就无法再次获取,所以需要对流进行复制,避免后续的处理不能读取该流中的数据。



response的输出有两种方式,都需要考虑到并重写



  1. getOutputStream()

  2. getWrite()



请求日志过滤器



public class RequestParameterFilter implements Filter {
/**
* 日志对象
*/
private static final Logger logger = LoggerFactory.getLogger(RequestParameterFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
if (!ServletFileUpload
.isMultipartContent((HttpServletRequest) request)) {
// 获取请求参数
Map<String, String[]> parameterMap = request.getParameterMap();
String requestString = JsonMapper.toJsonString(parameterMap);
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(
(HttpServletRequest) request);
logger.debug("请求参数: {} 请求的body:{} URI: {}", requestString,
requestWrapper.getBody(),
requestWrapper.getRequestURI());
if (requestWrapper != null) {
request = requestWrapper;
}
}
}
if (logger.isDebugEnabled()) {
BodyReaderHttpServletResponseWrapper myresponse = new BodyReaderHttpServletResponseWrapper(
(HttpServletResponse) response);
chain.doFilter(request, myresponse);
byte out[] = myresponse.getBuffer();
// 将返回数据重新写入Response中
response.getOutputStream().write(out);
// 拿出缓存中的数据
logger.debug("返回类型: {}", response.getContentType());
if (!StringUtils.contains(response.getContentType(), "image")) {
logger.debug("返回数据: {}", new String(myresponse.getBuffer()));
}
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}



Request包装器,避免后续类无法获取request Body中的数据。



public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* 将request body先保存
*/
private final String body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = RequestUtils.getRequestBodyString(request);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(
body.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
/**
* 获取Request body
*
* @return
*/
public String getBody() {
return this.body;
}
}



Response包装器



public class BodyReaderHttpServletResponseWrapper
extends HttpServletResponseWrapper {
private ByteArrayOutputStream bout = new ByteArrayOutputStream();
private PrintWriter pw;
private HttpServletResponse response;
public BodyReaderHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new MyServletOutputStream(bout);
}
@Override
public PrintWriter getWriter() throws IOException {
pw = new PrintWriter(new OutputStreamWriter(bout,
this.response.getCharacterEncoding()));
return pw;
}
public byte[] getBuffer() {
try {
if (pw != null) {
pw.close();
}
if (bout != null) {
bout.flush();
return bout.toByteArray();
}
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

使用AOP记录日志

使用 HandlerInterceptor 和 Filter 记录日志只能记录web请求的日志,而不能记录其他方法调用产生的日志。 如果使用 AOP 的方式记录日志,那么就可以对其余的方法调用也进行记录。 必须知道的概念



AOP 的相关术语

  1. 通知(Advice) 通知描述了切面要完成的工作以及何时执行。前置通知(Before):在目标方法调用前调用通知功能;后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。

  2. 连接点(JoinPoint) 通知功能被应用的时机。

  3. 切点(Pointcut) 切点定义了通知功能被应用的范围。

  4. 切面(Aspect) 切面是通知和切点的结合,定义了何时、何地应用通知功能。

  5. 引入(Introduction) 在无需修改现有类的情况下,向现有的类添加新方法或属性。

  6. 织入(Weaving) 把切面应用到目标对象并创建新的代理对象的过程。



Spring 中使用 AOP 记录日志

通过不同的切点表达式,可以在不同的方法调用的时候使用日志,本次使用注解



定义日志注解



import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义操作日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebLog {
}
}



定义日志切面,只是简单的演示,可以把日志记录到数据库或其他日志系统



import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@Component
public class WebLogAspect {
private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.github.laozhaishaozuo.WebLog)")
public void webLogPoinCut() {
}
@Around(value = "webLogPoinCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//获取当前请求对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录请求信息
Object[] objs = joinPoint.getArgs();
logger.debug("请求信息");
//返回的结果
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.debug("可以计算执行时间等信息");
return result;
}
}



日志的使用



@Controller
@RequestMapping(value = "bank")
public class TransferController {
@Autowired
private TransferService transferService;
@GetMapping("transfer")
@ResponseBody
@WebLog
public Result<Boolean> transfer(String targetAccountNumber, BigDecimal amount, Long userId) {
return transferService.transfer(userId, targetAccountNumber, amount, "CNY");
}
}



发布于: 2020 年 10 月 18 日 阅读数: 24
用户头像

引花眠

关注

还未添加个人签名 2018.06.11 加入

还未添加个人简介

评论

发布
暂无评论
SpringBoot系列(4)- 记录请求日志