springboot-23-aspectj日志記錄及threadlocal內存泄漏


對於請求參數的處理和響應, 如果在代碼中體現日志會顯得很繁瑣, 普遍的解決方案是使用spring的切面方案去解決. 

這兒使用的是springboot的切面: http://www.cnblogs.com/wenbronk/p/6848984.html

最開始的aspectj切面解決: 

package com.iwhere.easy.travel.aspect;

import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.Enumeration;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
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;

import com.alibaba.fastjson.JSONObject;

@Aspect
@Component
public class ControllerAspect {
    
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());  
    
    private String name = "easy-travel-server";
    
    @Pointcut("execution(public * com.wenbronk.controller.*.*(..))")
    public void controllerLog(){}
    
    @Pointcut("execution(public * com.wenbronk.service.*.*(..))")
    public void serviceLog(){}
    
    private ThreadLocal<Long> startTime = new ThreadLocal<>();
    
    private ThreadLocal<String> requestId = new ThreadLocal<>();
    
    private ThreadLocal<String> interfaceName = new ThreadLocal<>();
    
    private ThreadLocal<String> param = new ThreadLocal<>();
    
    private SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
    

    @Before("controllerLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 設置請求開始時間
        startTime.set(System.currentTimeMillis());
        Date stTimeDate = new Date(startTime.get());
        String dateStr = dataFormat.format(stTimeDate);
        // 設置請求標識
        String requestIdStr = UUID.randomUUID().toString();
        requestId.set(requestIdStr);
        // 提取全部參數  paramJson
        Enumeration<String> paramNames = request.getParameterNames();
        JSONObject paramJson = new JSONObject();
        while(paramNames.hasMoreElements()){
            String paramName = paramNames.nextElement();
            paramJson.put(paramName, request.getParameter(paramName));
        }
        
        // 提取接口標識(url中截取)
        String requestUrl = request.getRequestURL().toString();
        int start = requestUrl.lastIndexOf("/")+1;
        String interfaceNameStr = null;
        if (requestUrl.contains("?")){
            interfaceNameStr = requestUrl.substring(start, requestUrl.indexOf("?"));
        } else {
            interfaceNameStr = requestUrl.substring(start);
        }
        param.set(paramJson.toJSONString());
        interfaceName.set(interfaceNameStr);
        // 將requst的唯一標識放置在request中,在其他環節可以穿起來
        request.setAttribute("requestId", requestId.get());
    }
    
    @AfterReturning(returning="rvt",pointcut="controllerLog()")
    public void doAfterReturning(JoinPoint joinPoint,Object rvt) throws Throwable {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        logger.info("finished" + " " + name + " " + interfaceName.get() + " " + requestId.get() + " " 
                 + request.getRequestURL().toString() + " " + param.get()
                 + (System.currentTimeMillis() - startTime.get())
                 + " " + rvt.toString());
    }
    
    @AfterThrowing(throwing="ex", pointcut="controllerLog()")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable ex) throws Throwable {
        // 接收到請求,記錄請求內容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
         // 發生地點
         int lineNum = 0;
         String className = null;
         String methodName = null;
         StackTraceElement[] st = ex.getStackTrace();
         for (StackTraceElement stackTraceElement : st) {
             lineNum = stackTraceElement.getLineNumber();
             className = stackTraceElement.getClassName();
             methodName = stackTraceElement.getMethodName();
            System.out.println("[類:" + className + "]調用"
            + methodName + "時在第" + lineNum
            + "行代碼處發生異常!異常類型:" + ex.getClass().getName());
            break;
         }
         String exceptionMessage = "[類:" + className + "]調用"+ methodName + "時在第" + lineNum + "行代碼處發生異常!異常類型:" + ex.getClass().getName();
        logger.info("exception" + " " + name + " " + interfaceName.get() + " " + requestId.get() + " "
                 + request.getRequestURL().toString() + " " + param.get()
                 + " " + exceptionMessage);
    }
}

可見這個里面有一個before和after, 然后還有一個異常處理的方法

 

附: joinpoint的簡要api

AspectJ使用org.aspectj.lang.JoinPoint接口表示目標類連接點對象,如果是環繞增強時,使用org.aspectj.lang.ProceedingJoinPoint表示連接點對象,該類是JoinPoint的子接口。任何一個增強方法都可以通過將第一個入參聲明為JoinPoint訪問到連接點上下文的信息。我們先來了解一下這兩個接口的主要方法: 
1)JoinPoint 
 java.lang.Object[] getArgs():獲取連接點方法運行時的入參列表; 
 Signature getSignature() :獲取連接點的方法簽名對象; 
 java.lang.Object getTarget() :獲取連接點所在的目標對象; 
 java.lang.Object getThis() :獲取代理對象本身; 
2)ProceedingJoinPoint 
ProceedingJoinPoint繼承JoinPoint子接口,它新增了兩個用於執行連接點方法的方法: 
 java.lang.Object proceed() throws java.lang.Throwable:通過反射執行目標對象的連接點處的方法; 
 java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通過反射執行目標對象連接點處的方法,不過使用新的入參替換原來的入參。 

 

 

 

偶然間看到這個博客

http://blog.csdn.net/lhqj1992/article/details/52451136
https://my.oschina.net/xpbug/blog/113444
https://segmentfault.com/a/1190000000537475

由於此項目采用的是線程池, 所以可能存在內存一直上漲, 一直到線程池max之后達到一個穩定態, 也就發生了我們認為的內存泄漏

之后改成這個方法: 

package com.iwhere.scrapy.aspect;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
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.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSON;

/**
 * 日志記錄
 * @author wenbronk
 * @Date 上午9:33:47
 */
@Aspect
@Configuration
public class LogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

    // 定義切點 Pointcut
    @Pointcut("execution(* com.iwhere.scrapy.controller.*Controller.*(..))")
    public void excudeService() {}

    @Around("excudeService()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        
        Long startTime = System.currentTimeMillis();
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();

        String url = request.getRequestURL().toString();
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String queryString = request.getQueryString();
        
//        Object target = pjp.getTarget();
//        String name = target.getClass().getName();
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        
        
        LOGGER.info("請求開始, {}#{}() URI: {}, method: {}, URL: {}, params: {}",className, methodName, uri, method, url, queryString);

        // result的值就是被攔截方法的返回值
        Object result = pjp.proceed();
        Long endTime = System.currentTimeMillis();
        LOGGER.info("請求結束, {}#{}(), URI: {}, method: {}, URL: {}, time: {}, result: {} ", className, methodName, uri, method, url, (endTime - startTime), JSON.toJSONString(result));
        return result;
    }
    
    
//    @AfterThrowing(throwing="ex", pointcut="excudeService()")
//    public String doAfterThrowing(JoinPoint joinPoint, Throwable ex) throws Throwable {
//        // 接收到請求,記錄請求內容
//        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//        HttpServletRequest request = attributes.getRequest();
//         // 發生地點
//         int lineNum = 0;
//         String className = null;
//         String methodName = null;
//         StackTraceElement[] st = ex.getStackTrace();
//         if (ArrayUtils.isNotEmpty(st)) {
//             lineNum = st[0].getLineNumber();
//             className = st[0].getClassName();
//             methodName = st[0].getMethodName();
//         }
//        LOGGER.info("Exception: {}#{}() 在第{}行發生{}異常!!!", className, methodName, lineNum, ex.getClass().getName());
//        return "exception";
//    }
    
}

在里面處理異常, 還是會拋出, 所以單獨出一個異常處理

 

然后還需要加入一個全局異常處理框架: 

http://www.cnblogs.com/wenbronk/p/6850785.html

 

具體效果等待進一步測試

 推薦一個好的博客, 關於aspect的 : http://blog.csdn.net/lemon1003657090/article/details/52431584


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM