實(shí)戰(zhàn):第四章:java后端日志埋點(diǎn)實(shí)現(xiàn)

前段時(shí)間架構(gòu)讓我弄日志埋點(diǎn),因?yàn)槠渌ぷ鞑粩嘌悠?,而且到現(xiàn)在也沒給明確的需求,這里自己手寫一套簡單的日志埋點(diǎn):

第一步:引入依賴

    <!--aop-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
     
    <!--log4j-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.3</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>

第二步:因?yàn)楣居凶约旱娜罩九渲?,這里我簡單配置一條湊合用就行,在application.properties配置:

    #日志文件路徑 默認(rèn)生成文件名:spring.log 為了簡單便于學(xué)習(xí)這里我使用默認(rèn)的
    logging.path=F:/Log4j

第三步:自定義注解:

    package com.javaliao.portal.annotations;
     
    import org.springframework.web.bind.annotation.ResponseBody;
    import java.lang.annotation.*;
     
    /**
     *  app controller 統(tǒng)一包裝注解類
     */
    @Target({ElementType.PARAMETER, ElementType.METHOD})//作用在參數(shù)和方法上
    @Retention(RetentionPolicy.RUNTIME)//運(yùn)行時(shí)注解
    @Documented//表明這個(gè)注解應(yīng)該被 javadoc工具記錄
    @ResponseBody//響應(yīng)時(shí)轉(zhuǎn)JSON格式
    public @interface AppController {
     
        /**
         * 業(yè)務(wù)描述
         * @return
         */
        String description() default "";
        
        /**
         * 是否打日志 默認(rèn)打
         */
        boolean isLog() default true;
    }

第四步:攔截用戶請(qǐng)求

    package com.javaliao.portal.aspect;
     
    import com.alibaba.fastjson.JSON;
    import com.javaliao.portal.annotations.AppController;
    import com.javaliao.portal.common.CommonResult;
    import com.javaliao.portal.model.TbLogVisit;
    import com.javaliao.portal.service.ActionService;
    import com.javaliao.portal.util.CollectionHelp;
    import com.javaliao.portal.util.TimeUtils;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.aspectj.lang.reflect.MethodSignature;
    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 javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import java.lang.reflect.Method;
    import java.net.InetAddress;
    import java.util.*;
    import java.util.concurrent.atomic.AtomicLong;
     
    @Component
    @Aspect
    @SuppressWarnings("all") //@SuppressWarnings("all")抑制所有警告.@SuppressWarnings注解主要用在取消一些編譯器產(chǎn)生的警告對(duì)代碼左側(cè)行列的遮擋,有時(shí)候這會(huì)擋住我們斷點(diǎn)調(diào)試時(shí)打的斷點(diǎn)
    public class AppControllerAspect {
     
        //注入Service用于把日志保存數(shù)據(jù)庫,實(shí)際項(xiàng)目入庫采用隊(duì)列做異步
        @Resource
        private ActionService actionService;
        //日志工廠獲取日志對(duì)象
        static Logger logger = LoggerFactory.getLogger(AppControllerAspect.class);
     
        /**
         * ThreadLocal多線程環(huán)境下,創(chuàng)建多個(gè)副本,各自執(zhí)行,互不干擾
         */
     
        //startTime存放開始時(shí)間
        ThreadLocal<Map<String, Long >> startTime = new ThreadLocal<>();
        //count存放方法被調(diào)用的次數(shù)O 使用volatile利用它的三大特性:保證可見性(遵守JMM的可見性),不保證原子性,禁止指令重排
        volatile ThreadLocal<Map<String, Long>> count = new ThreadLocal<>();
        //timeConsuming存放方法總耗時(shí)
        ThreadLocal<Map<String, Long >> timeConsuming = new ThreadLocal<>();
        //fromType存放渠道
        ThreadLocal<Map<String, String >> fromType = new ThreadLocal<>();
        //tbLogVisit日志訪問對(duì)象
        ThreadLocal<TbLogVisit> tbLogVisit = new ThreadLocal<>();
     
        //Controller層切點(diǎn)
        @Pointcut("@annotation(com.javaliao.portal.annotations.AppController)")
        public void controllerAspectse() {
        }
     
        //前置通知  用于攔截Controller層記錄用戶的操作
        @Before("controllerAspectse()")
        public void before(JoinPoint pjp) {
            //初始化
            TbLogVisit tbLogVisit = this.tbLogVisit.get();
            tbLogVisit = new TbLogVisit();
            Map<String, Long> countMap = this.count.get();
            countMap = new HashMap<>();
            this.count.set(countMap);
            Map<String, Long> timeConsumingMap = this.timeConsuming.get();
            timeConsumingMap = new HashMap<>();
            this.timeConsuming.set(timeConsumingMap);
            Map<String, String> fromTypeMap = this.fromType.get();
            fromTypeMap = new HashMap<>();
            this.fromType.set(fromTypeMap);
            Map<String, Long> map = new HashMap<>();
            map.put("startTime",System.currentTimeMillis());
            this.startTime.set(map);
            logger.info("==============前置通知開始:記錄用戶的操作==============");
            String currentTime = TimeUtils.getCurrentTime("YYYY-MM-dd HH:mm:ss");
            logger.info("請(qǐng)求開始時(shí)間:" + currentTime);
            tbLogVisit.setVisitStartTime(new Date());
            String resultString = "";
            // 是否打日志 默認(rèn)打
            boolean isLog = true;
            try {
                MethodSignature signature = (MethodSignature) pjp.getSignature();
                AppController appController = signature.getMethod().getAnnotation(AppController.class);
                //是否開啟日志打印
                isLog = appController.isLog();
                if(isLog){
                    //開始打印日志
                    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                    HttpSession session = request.getSession();
                    String api = pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName();
                    logger.info("請(qǐng)求API:" + api);
                    tbLogVisit.setVisitApi(api);
     
                    String methodDescription = getControllerMethodDescription(pjp);
                    logger.info("方法描述:" + methodDescription);
                    tbLogVisit.setVisitDescription(methodDescription);
     
                    String ipAddress = InetAddress.getLocalHost().toString().substring(InetAddress.getLocalHost().toString().lastIndexOf("/") + 1);
                    logger.info("請(qǐng)求ip:"+ ipAddress);
                    tbLogVisit.setVisitIpAddress(ipAddress);
     
                    String hostName = InetAddress.getLocalHost().getHostName();
                    logger.info("機(jī)器名:" + hostName);
                    tbLogVisit.setVisitHostName(hostName);
     
                    Enumeration<?> enu = request.getParameterNames();
                    String params = "{";
                    while (enu.hasMoreElements()) {
                        String paraName = (String) enu.nextElement();
                        params += "\"" + paraName + "\":\"" + request.getParameter(paraName) + "\",";
                    }
                    String methodParams = params + "}";
                    String substring = methodParams.substring(0, methodParams.length() - 2);
                    substring = substring + "}";
                    logger.info("方法參數(shù):" + substring);
                    tbLogVisit.setVisitParams(substring);
     
                    StringBuffer url = request.getRequestURL();
                    logger.info("URL:" + url);
                    tbLogVisit.setVisitUrl(String.valueOf(url));
                }
            } catch (Exception e) {
                StackTraceElement stackTraceElement2 = e.getStackTrace()[2];
                String reason = "異常:【"+
                        "類名:"+stackTraceElement2.getClassName()+";"+
                        "文件:"+stackTraceElement2.getFileName()+";"+"行:"+
                        stackTraceElement2.getLineNumber()+";"+"方法:"
                        +stackTraceElement2.getMethodName() + "】";
                //記錄本地異常日志
                logger.error("==============前置通知異常:記錄訪問異常信息==============");
                String message = e.getMessage() + "|" + reason;
                logger.error("異常信息:",message);
                tbLogVisit.setVisitThrowingErro(message);
                tbLogVisit.setVisitResult("請(qǐng)求發(fā)生異常,異常信息:" + message);
            }finally {
                this.tbLogVisit.set(tbLogVisit);
            }
        }
     
        @Around("controllerAspectse()")
        public Object around(ProceedingJoinPoint pjp)throws Throwable {
            String result = JSON.toJSONString(pjp.proceed());
            logger.info("請(qǐng)求結(jié)果:" + result);
            TbLogVisit tbLogVisit = this.tbLogVisit.get();
            tbLogVisit.setVisitResult(result);
            this.tbLogVisit.set(tbLogVisit);
            return CommonResult.success("");
        }
     
     
        /**
         * 對(duì)Controller下面的方法執(zhí)行后進(jìn)行切入,統(tǒng)計(jì)方法執(zhí)行的次數(shù)和耗時(shí)情況
         *  注意,這里的執(zhí)行方法統(tǒng)計(jì)的數(shù)據(jù)不止包含Controller下面的方法,也包括環(huán)繞切入的所有方法的統(tǒng)計(jì)信息
         * @param jp
         */
     
        @AfterReturning("controllerAspectse()")
        public void afterMehhod(JoinPoint jp) {
            logger.info("==============方法執(zhí)行完成==============");
            TbLogVisit tbLogVisit = this.tbLogVisit.get();
            try {
                //獲取方法名
                String methodName = jp.getSignature().getName();
                //開始統(tǒng)計(jì)數(shù)量與耗時(shí)
                if(count.get().get(methodName) == null){
                    //第一次賦值為0
                    count.get().put(methodName,0L);
                }
                //使用原子整型進(jìn)行增值
                AtomicLong atomicInteger = new AtomicLong(count.get().get(methodName));
                //加一 這里暫時(shí)不解決ABA問題,僅保證原子性 解決了volatile不保證原子性的問題 getAndIncrement()先返回再加1,incrementAndGet()先加1再返回
                long increment = atomicInteger.incrementAndGet();
                //然后增加新值
                count.get().replace(methodName,increment);
                Long end = System.currentTimeMillis();
                Long total =  end - startTime.get().get("startTime");
                logger.info("執(zhí)行總耗時(shí)為:" +total);
                if(timeConsuming.get().containsKey(methodName)){
                    timeConsuming.get().replace(methodName, total);
                }else {
                    timeConsuming.get().put(methodName, (total));
                }
                tbLogVisit = this.tbLogVisit.get();
                tbLogVisit.setVisitTimeConsuming(String.valueOf(total));
                String endTime = TimeUtils.getCurrentTime("YYYY-MM-dd HH:mm:ss");
                logger.info("請(qǐng)求結(jié)束時(shí)間:" + endTime);
                tbLogVisit.setVisitEndTime(new Date());
                /**
                 * 從原來的map中將最后的連接點(diǎn)方法給移除了,替換成最終的,避免連接點(diǎn)方法多次進(jìn)行疊加計(jì)算,
                 * 由于原來的map受ThreadLocal的保護(hù),這里不支持remove,因此,需要單開一個(gè)map進(jìn)行數(shù)據(jù)交接
                 */
                //重新new一個(gè)map
                Map<String, Long> map = new HashMap<>();
                for(Map.Entry<String, Long> entry:timeConsuming.get().entrySet()){
                    if(entry.getKey().equals(methodName)){
                        map.put(methodName, total);
                    }else{
                        map.put(entry.getKey(), entry.getValue());
                    }
                }
                for (Map.Entry<String, Long> entry :count.get().entrySet()) {
                    for(Map.Entry<String, Long> entry2 :map.entrySet()){
                        if(entry.getKey().equals(entry2.getKey())){
                            Long num = entry.getValue();
                            logger.info("調(diào)用次數(shù):" + num);
                            tbLogVisit.setVisitNum(num);
                        }
                    }
                }
                //這里的渠道暫時(shí)寫死
                Map<String, String> stringMap = fromType.get();
                Map<String, String> fromMap ;
                if(CollectionHelp.isMapNotEmpty(stringMap)){
                    fromMap = stringMap;
                }else {
                    fromMap = new HashMap<>();
                    fromMap.put(methodName,"個(gè)人開發(fā)電商平臺(tái)");
                }
                String channel = fromMap.get(methodName);
                logger.info("渠道:" + channel);
                tbLogVisit.setVisitChannel(channel);
            } catch (Exception e) {
                StackTraceElement stackTraceElement2 = e.getStackTrace()[2];
                String reason = "異常:【"+
                        "類名:"+stackTraceElement2.getClassName()+";"+
                        "文件:"+stackTraceElement2.getFileName()+";"+"行:"+
                        stackTraceElement2.getLineNumber()+";"+"方法:"
                        +stackTraceElement2.getMethodName() + "】";
                //記錄本地異常日志
                logger.error("==============通知異常:記錄訪問異常信息==============");
                String message = e.getMessage() + "|" + reason;
                logger.error("異常信息:",message);
                tbLogVisit.setVisitThrowingErro(message);
                tbLogVisit.setVisitResult("請(qǐng)求發(fā)生異常?。?!");
            } finally {
                this.tbLogVisit.set(tbLogVisit);
                //添加日志信息入庫
                actionService.insertLogVisit(this.tbLogVisit.get());
            }
        }
     
     
     
        /**
         * 獲取注解中對(duì)方法的描述信息 用于Controller層注解
         */
        public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
            String targetName = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();//目標(biāo)方法名
            Object[] arguments = joinPoint.getArgs();
            Class targetClass = Class.forName(targetName);
            Method[] methods = targetClass.getMethods();
            String description = "";
            for (Method method:methods) {
                if (method.getName().equals(methodName)){
                    Class[] clazzs = method.getParameterTypes();
                    if (clazzs.length==arguments.length){
                        description = method.getAnnotation(AppController.class).description();
                        break;
                    }
                }
            }
            return description;
        }
     
    }

第五步:業(yè)務(wù)層發(fā)消息:

    package com.javaliao.portal.service.impl;
     
    import com.javaliao.portal.mapper.TbLogVisitMapper;
    import com.javaliao.portal.model.TbLogVisit;
    import com.javaliao.portal.service.ActionService;
    import com.javaliao.portal.util.ActiveMQUtil;
    import net.sf.json.JSONObject;
    import org.apache.activemq.command.ActiveMQMapMessage;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import javax.jms.*;
     
    @Service
    public class ActionServiceImpl implements ActionService {
     
        @Autowired
        TbLogVisitMapper tbLogVisitMapper;
     
        @Autowired
        ActiveMQUtil activeMQUtil;
     
        @Override
        public void insertLogVisit(TbLogVisit tbLogVisit) {
            try {
                // 連接消息服務(wù)器
                Connection connection = activeMQUtil.getConnection();
                connection.start();
                //第一個(gè)值表示是否使用事務(wù),如果選擇true,第二個(gè)值相當(dāng)于選擇0
                Session session = connection.createSession(true, Session.SESSION_TRANSACTED);
                // 發(fā)送消息
                Queue testqueue = session.createQueue("LOG_VISIT_QUEUE");
                MessageProducer producer = session.createProducer(testqueue);
                MapMessage mapMessage=new ActiveMQMapMessage();
                String toString = JSONObject.fromObject(tbLogVisit).toString();
                mapMessage.setString("tbLogVisit",toString);
                producer.setDeliveryMode(DeliveryMode.PERSISTENT);
                producer.send(mapMessage);
                session.commit();// 事務(wù)型消息,必須提交后才生效
                connection.close();
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
     
    }

第六步:接收消息執(zhí)行添加業(yè)務(wù):

    package com.javaliao.portal.listener;
     
    import com.javaliao.portal.log4j.BaseLogger;
    import com.javaliao.portal.model.TbLogVisit;
    import com.javaliao.portal.service.ActionService;
    import net.sf.json.JSONObject;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jms.annotation.JmsListener;
    import org.springframework.stereotype.Component;
    import javax.jms.JMSException;
    import javax.jms.MapMessage;
     
    @Component
    public class LogVisitListener {
     
        @Autowired
        ActionService actionService;
     
        @JmsListener(containerFactory = "jmsQueueListener" ,destination = "LOG_VISIT_QUEUE")
        public void consumeLogResult(MapMessage mapMessage){
            try {
                String object = mapMessage.getString("tbLogVisit");
                JSONObject jsonObject = new JSONObject().fromObject(object);
                TbLogVisit logVisit = (TbLogVisit) JSONObject.toBean(jsonObject, TbLogVisit.class);
                int count = actionService.insertLog(logVisit);
                if(count < 1){
                    BaseLogger.info("日志更新失敗");
                }
            } catch (JMSException e) {
                e.printStackTrace();
            }
        }
     
    }

執(zhí)行業(yè)務(wù):

    package com.javaliao.portal.service.impl;
     
    import com.javaliao.portal.mapper.TbLogVisitMapper;
    import com.javaliao.portal.model.TbLogVisit;
    import com.javaliao.portal.model.TbLogVisitExample;
    import com.javaliao.portal.service.ActionService;
    import com.javaliao.portal.util.ActiveMQUtil;
    import com.javaliao.portal.util.CollectionHelp;
    import com.javaliao.portal.util.NumberUtils;
    import net.sf.json.JSONObject;
    import org.apache.activemq.command.ActiveMQMapMessage;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import javax.jms.*;
    import java.util.Date;
    import java.util.List;
     
    @Service
    public class ActionServiceImpl implements ActionService {
     
        @Autowired
        TbLogVisitMapper tbLogVisitMapper;
     
        //這里去掉了之前發(fā)消息的代碼
        /**
         * 添加日志信息入庫
         * @param tbLogVisit
         * @return
         */
        @Override
        public int insertLog(TbLogVisit tbLogVisit) {
            tbLogVisit.setUpdateTime(new Date());
            int count = 0;
            //如果有異常直接添加
            if(StringUtils.isNoneEmpty(tbLogVisit.getVisitThrowingErro())){
                tbLogVisit.setCreateTime(new Date());
                count = tbLogVisitMapper.insert(tbLogVisit);
            }else {
                String visitIpAddress = tbLogVisit.getVisitIpAddress();
                String visitApi = tbLogVisit.getVisitApi();
                TbLogVisitExample tbLogVisitExample = new TbLogVisitExample();
                TbLogVisitExample.Criteria criteria = tbLogVisitExample.createCriteria();
                criteria.andVisitIpAddressEqualTo(visitIpAddress);
                criteria.andVisitApiEqualTo(visitApi);
                List<TbLogVisit> tbLogVisits = tbLogVisitMapper.selectByExample(tbLogVisitExample);
                if(CollectionHelp.isNotEmpty(tbLogVisits)){
                    Long nums = 0L;
                    Double sums = 0D;
                    for (TbLogVisit logVisit : tbLogVisits) {
                        //統(tǒng)計(jì)調(diào)用次數(shù)
                        Long visitNum = logVisit.getVisitNum();
                        nums = tbLogVisit.getVisitNum() + visitNum;
                        //統(tǒng)計(jì)耗時(shí)
                        Double visitTimeConsumingData = NumberUtils.Double(logVisit.getVisitTimeConsuming());
                        Double visitTimeConsumingParam = NumberUtils.Double(tbLogVisit.getVisitTimeConsuming());
                        Double sum = visitTimeConsumingData + visitTimeConsumingParam;
                        sums = sums + sum;
                    }
                    Double numDouble = NumberUtils.Double(String.valueOf(nums));
                    //統(tǒng)計(jì)平均耗時(shí)
                    Double avg = sums / numDouble;
                    tbLogVisit.setVisitTimeConsuming(avg.toString());
                    tbLogVisit.setVisitNum(nums);
                    count = tbLogVisitMapper.updateByExample(tbLogVisit,tbLogVisitExample);
                }else {
                    tbLogVisit.setCreateTime(new Date());
                    count = tbLogVisitMapper.insert(tbLogVisit);
                }
            }
     
            return count;
        }
    }

一開始沒有設(shè)計(jì)好,后面強(qiáng)迫改動(dòng),導(dǎo)致訪客的開始時(shí)間和結(jié)束時(shí)間都是最近一次的,不過我把每次請(qǐng)求的耗時(shí)改為平均耗時(shí),勉強(qiáng)達(dá)到效果(不過有些請(qǐng)求異常的耗時(shí)時(shí)間長的就比較影響耗時(shí)統(tǒng)計(jì)了,唉不說了,最開始沒有設(shè)計(jì)好,也算勉強(qiáng)了),效率也不算太差,不會(huì)太影響性能,