实战:第四章:java后端日志埋点实现

简介: 实战:第四章:java后端日志埋点实现

前段时间架构让我弄日志埋点,因为其他工作不断延期,而且到现在也没给明确的需求,这里自己手写一套简单的日志埋点:

第一步:引入依赖

<!--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>

第二步:因为公司有自己的日志配置,这里我简单配置一条凑合用就行,在application.properties配置:

#日志文件路径 默认生成文件名:spring.log 为了简单便于学习这里我使用默认的
logging.path=F:/Log4j

第三步:自定义注解:

package com.javaliao.portal.annotations;
import org.springframework.web.bind.annotation.ResponseBody;
import java.lang.annotation.*;
/**
 *  app controller 统一包装注解类
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})//作用在参数和方法上
@Retention(RetentionPolicy.RUNTIME)//运行时注解
@Documented//表明这个注解应该被 javadoc工具记录
@ResponseBody//响应时转JSON格式
public @interface AppController {
  /**
   * 业务描述
   * @return
   */
  String description() default "";
  /**
   * 是否打日志 默认打
   */
  boolean isLog() default true;
}

第四步:拦截用户请求

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注解主要用在取消一些编译器产生的警告对代码左侧行列的遮挡,有时候这会挡住我们断点调试时打的断点
public class AppControllerAspect {
    //注入Service用于把日志保存数据库,实际项目入库采用队列做异步
    @Resource
    private ActionService actionService;
    //日志工厂获取日志对象
    static Logger logger = LoggerFactory.getLogger(AppControllerAspect.class);
    /**
     * ThreadLocal多线程环境下,创建多个副本,各自执行,互不干扰
     */
    //startTime存放开始时间
    ThreadLocal<Map<String, Long >> startTime = new ThreadLocal<>();
    //count存放方法被调用的次数O 使用volatile利用它的三大特性:保证可见性(遵守JMM的可见性),不保证原子性,禁止指令重排
    volatile ThreadLocal<Map<String, Long>> count = new ThreadLocal<>();
    //timeConsuming存放方法总耗时
    ThreadLocal<Map<String, Long >> timeConsuming = new ThreadLocal<>();
    //fromType存放渠道
    ThreadLocal<Map<String, String >> fromType = new ThreadLocal<>();
    //tbLogVisit日志访问对象
    ThreadLocal<TbLogVisit> tbLogVisit = new ThreadLocal<>();
    //Controller层切点
    @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("请求开始时间:" + currentTime);
        tbLogVisit.setVisitStartTime(new Date());
        String resultString = "";
        // 是否打日志 默认打
        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("请求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("请求ip:"+ ipAddress);
                tbLogVisit.setVisitIpAddress(ipAddress);
                String hostName = InetAddress.getLocalHost().getHostName();
                logger.info("机器名:" + 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("方法参数:" + 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("请求发生异常,异常信息:" + message);
        }finally {
            this.tbLogVisit.set(tbLogVisit);
        }
    }
    @Around("controllerAspectse()")
    public Object around(ProceedingJoinPoint pjp)throws Throwable {
        String result = JSON.toJSONString(pjp.proceed());
        logger.info("请求结果:" + result);
        TbLogVisit tbLogVisit = this.tbLogVisit.get();
        tbLogVisit.setVisitResult(result);
        this.tbLogVisit.set(tbLogVisit);
        return CommonResult.success("");
    }
    /**
     * 对Controller下面的方法执行后进行切入,统计方法执行的次数和耗时情况
     *  注意,这里的执行方法统计的数据不止包含Controller下面的方法,也包括环绕切入的所有方法的统计信息
     * @param jp
     */
    @AfterReturning("controllerAspectse()")
    public void afterMehhod(JoinPoint jp) {
        logger.info("==============方法执行完成==============");
        TbLogVisit tbLogVisit = this.tbLogVisit.get();
        try {
            //获取方法名
            String methodName = jp.getSignature().getName();
            //开始统计数量与耗时
            if(count.get().get(methodName) == null){
                //第一次赋值为0
                count.get().put(methodName,0L);
            }
            //使用原子整型进行增值
            AtomicLong atomicInteger = new AtomicLong(count.get().get(methodName));
            //加一 这里暂时不解决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("执行总耗时为:" +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("请求结束时间:" + endTime);
            tbLogVisit.setVisitEndTime(new Date());
            /**
             * 从原来的map中将最后的连接点方法给移除了,替换成最终的,避免连接点方法多次进行叠加计算,
             * 由于原来的map受ThreadLocal的保护,这里不支持remove,因此,需要单开一个map进行数据交接
             */
            //重新new一个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("调用次数:" + num);
                        tbLogVisit.setVisitNum(num);
                    }
                }
            }
            //这里的渠道暂时写死
            Map<String, String> stringMap = fromType.get();
            Map<String, String> fromMap ;
            if(CollectionHelp.isMapNotEmpty(stringMap)){
                fromMap = stringMap;
            }else {
                fromMap = new HashMap<>();
                fromMap.put(methodName,"个人开发电商平台");
            }
            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("请求发生异常!!!");
        } finally {
            this.tbLogVisit.set(tbLogVisit);
            //添加日志信息入库
            actionService.insertLogVisit(this.tbLogVisit.get());
        }
    }
    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     */
    public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();//目标方法名
        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;
    }
}

第五步:业务层发消息:

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 {
            // 连接消息服务器
            Connection connection = activeMQUtil.getConnection();
            connection.start();
            //第一个值表示是否使用事务,如果选择true,第二个值相当于选择0
            Session session = connection.createSession(true, Session.SESSION_TRANSACTED);
            // 发送消息
            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();// 事务型消息,必须提交后才生效
            connection.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

第六步:接收消息执行添加业务:

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();
        }
    }
}

执行业务:

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;
    //这里去掉了之前发消息的代码
    /**
     * 添加日志信息入库
     * @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) {
                    //统计调用次数
                    Long visitNum = logVisit.getVisitNum();
                    nums = tbLogVisit.getVisitNum() + visitNum;
                    //统计耗时
                    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));
                //统计平均耗时
                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;
    }
}

这里提供我写的项目地址:https://github.com/javaliao/personalmall


一开始没有设计好,后面强迫改动,导致访客的开始时间和结束时间都是最近一次的,不过我把每次请求的耗时改为平均耗时,勉强达到效果(不过有些请求异常的耗时时间长的就比较影响耗时统计了,唉不说了,最开始没有设计好,也算勉强了),效率也不算太差,不会太影响性能,

 

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
692 70
|
JavaScript Java Docker
干货含源码!如何用Java后端操作Docker(命令行篇)
只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
Java API Docker
在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境
以上内容是一个简单的实现在Java后端中通过DockerClient操作Docker生成python环境并执行代码,最后销毁的案例全过程,也是实现一个简单的在线编程后端API的完整流程,你可以在此基础上添加额外的辅助功能,比如上传文件、编辑文件、查阅文件、自定义安装等功能。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
在线编程实现!如何在Java后端通过DockerClient操作Docker生成python环境
|
SQL druid Oracle
【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
客户Java日志中出现异常,影响Druid的merge SQL功能(将SQL字面量替换为绑定变量以统计性能),但不影响正常业务流程。原因是Druid在merge SQL时传入null作为dbType,导致无法解析递归查询中的`start`关键字。
|
监控 前端开发 Java
构建高效Java后端与前端交互的定时任务调度系统
通过以上步骤,我们构建了一个高效的Java后端与前端交互的定时任务调度系统。该系统使用Spring Boot作为后端框架,Quartz作为任务调度器,并通过前端界面实现用户交互。此系统可以应用于各种需要定时任务调度的业务场景,如数据同步、报告生成和系统监控等。
752 9
|
XML JSON Java
Java中Log级别和解析
日志级别定义了日志信息的重要程度,从低到高依次为:TRACE(详细调试)、DEBUG(开发调试)、INFO(一般信息)、WARN(潜在问题)、ERROR(错误信息)和FATAL(严重错误)。开发人员可根据需要设置不同的日志级别,以控制日志输出量,避免影响性能或干扰问题排查。日志框架如Log4j 2由Logger、Appender和Layout组成,通过配置文件指定日志级别、输出目标和格式。
|
SQL JavaScript 安全
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
626 11
【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战
|
存储 小程序 前端开发
微信小程序与Java后端实现微信授权登录功能
微信小程序极大地简化了登录注册流程。对于用户而言,仅仅需要点击授权按钮,便能够完成登录操作,无需经历繁琐的注册步骤以及输入账号密码等一系列复杂操作,这种便捷的登录方式极大地提升了用户的使用体验
3872 12
|
前端开发 NoSQL Java
【Java若依框架】RuoYi-Vue的前端和后端配置步骤和启动步骤
本文介绍了如何配置和启动基于Java的若依(RuoYi)项目,涵盖后端和前端的详细步骤。首先,准备Redis、MySQL以及IDE(如Idea和VS)。接着,通过GitHub获取代码并导入到IDE中,执行必要的SQL文件和配置数据库密码。然后,启动Redis并进行相关配置。最后,按照前端配置步骤克隆前端代码库,打开终端执行命令完成前端配置。整个过程详细记录了每一步的操作,帮助开发者顺利部署若依项目。 如果你觉得有帮助,请点赞、关注和收藏,这将是我持续分享的动力!
8387 4
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
766 2

热门文章

最新文章