相关推荐recommended
三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏
作者:mmseoamin日期:2024-04-01

🚀 注重版权,转载请注明原作者和原文链接

🌐 Open开袁 的官方网站已全面升级!探索更多精彩内容,尽在:https://open-yuan.com。

在这里,你将发现丰富的编程资源、深度的技术文章,以及开源项目的地址,一起加入我们的技术交流社区吧!

📱 想要随时随地获取最新动态?微信搜索公众号“全栈小袁”,一手掌握最新项目动态和技术分享。让我们一起,开启技术的无限可能!🌈


效果展示

三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏,image.png,第1张

三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏,image.png,第2张

MySQL

建表

CREATE TABLE `access_log` (
  `access_log_id` bigint NOT NULL AUTO_INCREMENT,
  `access_time` datetime NOT NULL COMMENT '访问时间',
  `access_ip` varchar(30) NOT NULL COMMENT '访问IP',
  `api_group` varchar(50) NOT NULL DEFAULT '默认' COMMENT '接口分组',
  `req_url` varchar(100) NOT NULL COMMENT '请求URL',
  `req_method` varchar(10) NOT NULL COMMENT '请求方式',
  `os` varchar(100) NULL DEFAULT NULL COMMENT '操作系统',
  `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器',
  `lsp` varchar(15) NULL DEFAULT NULL COMMENT '运营商',
  `country` varchar(15) NULL DEFAULT NULL COMMENT '国家',
  `province` varchar(15) NULL DEFAULT NULL COMMENT '省',
  `city` varchar(15) NULL DEFAULT NULL COMMENT '城市',
  PRIMARY KEY (`access_log_id`)
) COMMENT='访问日志表';

后端

POJO实体

@Data
@TableName(value = "access_log")
public class AccessLog implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 访问日志ID  
     */
    @TableId(type = IdType.AUTO)
    private Long accessLogId;
    /**
     * 访问时间  
     */
    private Date accessTime;
    /**
     * 访问IP  
     */
    private String accessIp;
    /**
     * 接口分组  
     */
    private String apiGroup;
    /**
     * 请求URL  
     */
    private String reqUrl;
    /**
     * 请求方式  
     */
    private String reqMethod;
    /**
     * 操作系统  
     */
    private String os;
    /**
     * 浏览器  
     */
    private String browser;
    /**
     * 运营商  
     */
    private String lsp;
    /**
     * 国家  
     */
    private String country;
    /**
     * 省  
     */
    private String province;
    /**
     * 城市  
     */
    private String city;
}

Mapper接口

@Repository
public interface AccessLogMapper extends BaseMapper {
    
}

自定义注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 给接口分组
     */
    String apiGroup() default "默认";
}

案例:

@Log(apiGroup = "文章模块")
@RestController
@RequestMapping("/article")
public class ArticleController {
    ......
}

拦截器

ps:https://api.vvhan.com/api/getIpInfo?ip=[你的IP],这个网址是一个免费获取国家、省、市、运营商的地址

当然这种对IP地址的解析应该是放在定时任务中,每天晚上定时解析日志IP,如果解析IP的API挂了,接口会受到影响,我这里只是为了方便写在这里

@Slf4j
@Component
public class AccessLogInterceptor implements HandlerInterceptor {
    
    @Autowired
    private AccessLogMapper accessLogMapper;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        
        try {
            // 获取客户端真是IP地址,这种网上很多现成代码
            String accessIp = NetUtil.getRemoteHost(request);
            // 获取User-Agent
            String requestUserAgent = request.getHeader("User-Agent");
            // 获取浏览器用户标识
            UserAgent userAgent = UserAgentUtil.parse(requestUserAgent);
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Log logAnnotation = handlerMethod.getMethod().getDeclaringClass().getAnnotation(Log.class);
            AccessLog accessLog = new AccessLog();
            accessLog.setAccessIp(accessIp);
            accessLog.setAccessTime(new Date());
            if (logAnnotation != null) {
                accessLog.setApiGroup(logAnnotation.apiGroup());
            }
            accessLog.setReqUrl(request.getRequestURI());
            accessLog.setReqMethod(request.getMethod());
            accessLog.setOs(userAgent.getOs().getName());
            accessLog.setBrowser(userAgent.getBrowser().getName());
            // 解析IP
            try {
                String ipParseStr = HttpUtil.get("https://api.vvhan.com/api/getIpInfo?ip=" + accessIp);
                JSONObject ipParseJson = JSONUtil.parseObj(ipParseStr);
                if (ipParseJson.getBool("success")) {
                    JSONObject infoJson = ipParseJson.getJSONObject("info");
                    accessLog.setLsp(infoJson.getStr("lsp"));
                    accessLog.setCountry(infoJson.getStr("country"));
                    accessLog.setProvince(infoJson.getStr("prov"));
                    accessLog.setCity(infoJson.getStr("city"));
                }
            } catch (Exception e) {
                accessLog.setLsp("未知");
                accessLog.setCountry("未知");
                accessLog.setProvince("未知");
                accessLog.setCity("未知");
            }
            accessLogMapper.insert(accessLog);
        } catch (Exception e) {
            log.error("", e);
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

注册拦截器

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
    
    @Autowired
    private AccessLogInterceptor accessLogInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册全局日志拦截器
        registry.addInterceptor(accessLogInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/static/**")
                .excludePathPatterns("/error");
        // 其他拦截器......
    }
}

测试

到这里就完成一个简单的全局日志拦截器了,随便发几个请求测试一下,成功记录入库!

三步实现SpringBoot全局日志记录,整合Echarts实现数据大屏,image.png,第3张

进阶——整合Echarts实现数据大屏

数据VO实体

浏览器访问占比情况VO
@Data
public class AccessBrowserGroupVo {
    
    private String browser;
    
    private Integer count;
}
运营商访问占比情况VO
@Data
public class AccessLspGroupVo {
    
    private String lsp;
    
    private Integer count;
}
各省份访问情况VO
@Data
public class AccessProvinceGroupVo {
    
    private String province;
    
    private Integer count;
}
每天访问情况VO
@Data
public class AccessTimeGroupVo {
    
    private String accessTime;
    
    private Integer count;
}

查询SQL

浏览器访问统计

运营商访问统计

各省份访问统计

近15天内访问统计

后端业务

封装统一接口返回
/**
 * FileName:    R
 * Author:      小袁
 * Date:        2022/3/12 12:23
 * Description: 统一结果返回的类
 */
@Data
public class R {
    private Boolean success;
    private Integer code;
    private String message;
    private T data;
    // 成功静态方法
    public static  R success() {
        R r = new R<>();
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(HttpStatusEnum.SUCCESS.getName());
        return r;
    }
    public static  R success(String message) {
        R r = new R<>();
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.message(message);
        return r;
    }
    public static  R success(T object) {
        R r = new R<>();
        r.setData(object);
        r.setSuccess(true);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(HttpStatusEnum.SUCCESS.getName());
        return r;
    }
    public static  R success(String msg, T object) {
        R r = new R<>();
        r.setData(object);
        r.setCode(HttpStatusEnum.SUCCESS.getCode());
        r.setMessage(msg);
        return r;
    }
    // 失败静态方法
    public static  R fail() {
        R r = new R<>();
        r.setSuccess(false);
        r.setCode(HttpStatusEnum.FAIL.getCode());
        r.setMessage(HttpStatusEnum.FAIL.getName());
        return r;
    }
    public static  R fail(String msg) {
        R r = new R<>();
        r.setSuccess(false);
        r.setCode(HttpStatusEnum.FAIL.getCode());
        r.setMessage(msg);
        return r;
    }
    public static  R fail(HttpStatusEnum httpStatusEnum) {
        R r = new R<>();
        r.setSuccess(false);
        r.setCode(httpStatusEnum.getCode());
        r.setMessage(httpStatusEnum.getName());
        return r;
    }
    public R message(String message){
        this.setMessage(message);
        return this;
    }
    public R code(Integer code){
        this.setCode(code);
        return this;
    }
    public R data(T data){
        this.setData(data);
        return this;
    }
}
封装客户端响应码
/**
 * FileName:    Code
 * Author:      小袁
 * Date:        2022/5/1 23:29
 * Description: 客户端响应状态码
 */
public enum HttpStatusEnum implements BaseCodeEnum {
    SUCCESS(200, "成功"),
    FAIL(20001, "失败"),
    INTERNAL_SERVER_ERROR(500, "服务器异常"),
    private final Integer code;
    private final String name;
    HttpStatusEnum(int code, String msg) {
        this.code = code;
        this.name = msg;
    }
    @Override
    public Integer getCode() {
        return this.code;
    }
    @Override
    public String getName() {
        return this.name;
    }
}
Mapper接口
@Repository
public interface AccessLogMapper extends BaseMapper {
    List countLspGroupAccess();
    List countBrowserGroupAccess();
    List countProvinceGroupAccess();
    List countTimeGroupAccess();
}
Service接口
public interface AccessLogService extends IService {
    List countLspGroupAccess();
    List countBrowserGroupAccess();
    List countProvinceGroupAccess();
    List countTimeGroupAccess();
}
Service实现类
@Slf4j
@Service
public class AccessLogServiceImpl extends ServiceImpl implements AccessLogService {
    
    @Override
    public List countLspGroupAccess() {
        return this.baseMapper.countLspGroupAccess();
    }
    @Override
    public List countBrowserGroupAccess() {
        return this.baseMapper.countBrowserGroupAccess();
    }
    @Override
    public List countProvinceGroupAccess() {
        return this.baseMapper.countProvinceGroupAccess();
    }
    @Override
    public List countTimeGroupAccess() {
        return this.baseMapper.countTimeGroupAccess();
    }
}
Controller接口
@RestController
@RequestMapping("/stat/access")
public class AccessStatController {
    
    @Autowired
    private AccessLogService accessLogService;
    /**
     * 查询15天内的访问次数情况-折线图
     */
    @GetMapping("/query_line_by_day")
    public R> queryAccessLogByTimeGroup() {
        return R.success(accessLogService.countTimeGroupAccess());
    }
    /**
     * 查询省份访问占比-柱形图
     */
    @GetMapping("/query_col_by_province")
    public R> queryAccessLogByProvinceGroup() {
        return R.success(accessLogService.countProvinceGroupAccess());
    }
    /**
     * 查询运营商访问占比-饼图
     */
    @GetMapping("/query_pie_by_lsp")
    public R> queryAccessLogByLspGroup() {
        return R.success(accessLogService.countLspGroupAccess());
    }
    /**
     * 查询浏览器访问占比-饼图
     */
    @GetMapping("/query_pie_by_browser")
    public R> queryAccessLogByBrowserGroup() {
        return R.success(accessLogService.countBrowserGroupAccess());
    }
}

前端配置

安装axios、echarts
npm install axios
npm install echarts
封装request
import axios from 'axios'
import { Message, MessageBox,} from 'element-ui'
import store from '../store'
import { getToken } from '@/utils/auth'
import router from '@/router'
// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api 的 base_url
  // timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(
  config => {
    if (store.getters.token) {
     config.headers['token'] = getToken()
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)
// response 拦截器
service.interceptors.response.use(
  response => {
    /**
     * code为非200是抛错 可结合自己业务进行修改
     */
    const res = response.data
    const url = response.config.url
    if (res.code !== 200) {
      if (url.indexOf("/login") < 0 && res.code === 40005) {
        store.dispatch('FedLogOut').then(() => {
          router.push(`/login`)
        })
        Message({
          message: res.message,
          type: 'warning',
          duration: 2 * 1000,
        })
        return Promise.resolve(res)
      }else if (res.code >= 40000) {
        Message({
          message: res.message,
          type: 'error',
          duration: 3 * 1000
        })
        return Promise.resolve(res)
      }else {
        Message({
          message: res.message,
          type: 'error',
          duration: 5 * 1000
        })
        return Promise.reject(new Error(res.message || 'Error'))
      }
    } else {
      return res
    }
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)
export default service
定义API
import request from "../../utils/request";
export default {
  getAccessStatByTime() {
    return request({
      url: '/stat/access/query_line_by_day',
      method: 'get'
    })
  },
  getAccessStatByProvince() {
    return request({
      url: '/stat/access/query_col_by_province',
      method: 'get'
    })
  },
  getAccessStatByLsp() {
    return request({
      url: '/stat/access/query_pie_by_lsp',
      method: 'get'
    })
  },
  getAccessStatByBrowser() {
    return request({
      url: '/stat/access/query_pie_by_browser',
      method: 'get'
    })
  },
}
封装echarts


饼图-浏览器访问占比

饼图-运营商访问占比

折线图-每天访问量情况

柱状图-各省份访问情况

首页

直接引入



结束

到这里整篇文章就结束了,我们重新捋一下整个流程

  • 全局过滤器拦截请求
  • 对请求信息进行解析入库
  • 定义API接口
  • 前端引入axios、echarts
  • 编写图形Vue组件
  • 前后端数据交互

    🚀 注重版权,转载请注明原作者和原文链接

    🌐 Open开袁 的官方网站已全面升级!探索更多精彩内容,尽在:https://open-yuan.com。

    在这里,你将发现丰富的编程资源、深度的技术文章,以及开源项目的地址,一起加入我们的技术交流社区吧!

    📱 想要随时随地获取最新动态?微信搜索公众号“全栈小袁”,一手掌握最新项目动态和技术分享。让我们一起,开启技术的无限可能!🌈