springboot+aop+线程池+ip2region+mybatisplus实现异步系统日志记录

公司系统需要新增一个需求,可以在需要的地方记录下接口调用的日志。手动在每个需要的接口里面写记录日志的功能不现实,所以这个需求嘛,首选aop切面,废话不多说,搞起

使用ip2region是为了根据客户端ip得到客户端的归属地,没有该需求的话不必引入该依赖

首先创建系统日志(sys_log)表结构,这里用的是postgresql

CREATE SEQUENCE "sde"."sys_log_seq"
INCREMENT 1
MINVALUE  1
MAXVALUE 9223372036854775807
START 1
CACHE 1;

DROP TABLE IF EXISTS "sde"."sys_log";
CREATE TABLE "sde"."sys_log" (
     "id" varchar(255) COLLATE "pg_catalog"."default" NOT NULL DEFAULT nextval('"sde".sys_log_seq'::regclass),
     "description" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "account" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
     "user_id" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
     "ip" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
     "q_cell_core" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character varying,
     "url" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "req_method" varchar(255) COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "req_param" text COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "header" text COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "stack_trace" text COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "type" varchar(4) COLLATE "pg_catalog"."default" DEFAULT NULL::character VARYING,
     "create_time" timestamp
)
;
COMMENT ON COLUMN "sde"."sys_log"."id" IS 'id';
COMMENT ON COLUMN "sde"."sys_log"."description" IS '描述';
COMMENT ON COLUMN "sde"."sys_log"."account" IS '账号';
COMMENT ON COLUMN "sde"."sys_log"."user_id" IS '用户id';
COMMENT ON COLUMN "sde"."sys_log"."ip" IS 'IP地址';
COMMENT ON COLUMN "sde"."sys_log"."q_cell_core" IS '归属地';
COMMENT ON COLUMN "sde"."sys_log"."url" IS '请求路径';
COMMENT ON COLUMN "sde"."sys_log"."req_method" IS '请求类型';
COMMENT ON COLUMN "sde"."sys_log"."req_param" IS '请求参数';
COMMENT ON COLUMN "sde"."sys_log"."header" IS '请求头';
COMMENT ON COLUMN "sde"."sys_log"."stack_trace" IS '异常栈信息';
COMMENT ON COLUMN "sde"."sys_log"."type" IS '日志类型';
COMMENT ON COLUMN "sde"."sys_log"."create_time" IS '创建时间';
COMMENT ON TABLE "sde"."sys_log" IS '系统日志表';

-- ----------------------------
-- Primary Key structure for table sys_log
-- ----------------------------
ALTER TABLE "sde"."sys_log" ADD CONSTRAINT "sys_log_pkey" PRIMARY KEY ("id");

然后导入aop和ip2region的所需依赖(一些包含mybatisplus在内的springboot基础依赖就不再放出来了)

<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>1.7.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

将下载的ip2region.db文件放到项目的resources目录下

在这里插入图片描述

创建SysLog实体类

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

/**
 * @Author iwlnner
 * @Date 2022-09-05 17:28
 * @Description
 * @Version 1.0
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "SysLog对象", description = "系统日志")
public class SysLog implements Serializable {

    @ApiModelProperty(value = "主键")
    @TableId(value = "id", type = IdType.AUTO)
    private String id;

    @ApiModelProperty(value = "描述")
    private String description;

    @ApiModelProperty(value = "账号")
    private String account;

    @ApiModelProperty(value = "用户id")
    private String userId;

    @ApiModelProperty(value = "IP地址")
    private String ip;

    @ApiModelProperty(value = "归属地")
    private String qCellCore;

    @ApiModelProperty(value = "请求路径")
    private String url;

    @ApiModelProperty(value = "请求方式")
    private String reqMethod;

    @ApiModelProperty(value = "请求参数")
    private String reqParam;

    @ApiModelProperty(value = "请求头")
    private String header;

    @ApiModelProperty(value = "异常信息栈")
    private String stackTrace;

    @ApiModelProperty(value = "日志类型")
    private String type;

    @ApiModelProperty(value = "创建时间")
    private Date createTime;
}

准备工作到此结束,接下来要动脑子了,由于暂时不清楚生成日志的目标是在哪些包的哪些类下,不确定,所以切入点(Pointcut)不能是execution表达式来指定某些类了,因此使用自定义注解就显得更加灵活了

接下来创建自定义注解我起名叫Logx

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author iwlnner
 * @Date 2022-09-05 17:40
 * @Description 该注解实现的目标是:Logx注解贴在哪个接口上,
 * 客户端调用这个接口就会生成操作日志并存储到表sys_log中,
 * 由于考虑到不能影响到接口的响应速度,所以这个生成日志操作用异步线程处理
 * @Version 1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Logx {

    /**
     * 日志描述
     * @return
     */
    String value() default "";

}

创建ip工具类IpUtils.java以及请求参数转化工具类RequestUtil.java(两者可合并成一个工具类)

import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.lionsoul.ip2region.Util;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

 /**
 * @Author iwlnner
 * @Date 2022-09-06 16:55
 * @Description ip查询处理
 * @Version 1.0
 */
public final class IpUtils {

    private static final String UNKOWN_ADDRESS = "未知位置";

    /**
     * 根据IP获取地址
     *
     * @return 国家|区域|省份|城市|ISP
     */
    public static String getAddress(String ip) throws Exception {
        return getAddress(ip, DbSearcher.MEMORY_ALGORITYM);
    }

    /**
     * 根据IP获取归属地
     *
     * @param ip
     * @param algorithm 查询算法
     * @return 国家|区域|省份|城市|ISP
     */
    public static String getAddress(String ip, int algorithm) {
        DbSearcher searcher = null;
        //java8自动关闭流
        try(InputStream is=IpUtils.class.getClassLoader().getResourceAsStream("ip2region.db");
            ByteArrayOutputStream baos=new ByteArrayOutputStream()){
            if (!Util.isIpAddress(ip)) {
                return UNKOWN_ADDRESS;
            }
            //将ip2region.db文件读取到字节数组输出流
            byte[] buffer=new byte[1024*4];
            int len=0;
            while ((len=is.read(buffer))!=-1){
                baos.write(buffer,0,len);
            }
            //字节输出流转化为字节数组
            byte[] bytes = baos.toByteArray();
            searcher = new DbSearcher(new DbConfig(), bytes);
            DataBlock dataBlock;
            //选择查询算法
            switch (algorithm) {
                case DbSearcher.BTREE_ALGORITHM:
                    dataBlock = searcher.btreeSearch(ip);
                    break;
                case DbSearcher.BINARY_ALGORITHM:
                    dataBlock = searcher.binarySearch(ip);
                    break;
                case DbSearcher.MEMORY_ALGORITYM:
                    dataBlock = searcher.memorySearch(ip);
                    break;
                default:
                    return UNKOWN_ADDRESS;
            }
            return dataBlock.getRegion();
        }catch (Exception e){
            e.printStackTrace();
        }
        return UNKOWN_ADDRESS;
    }

    public static void main(String[] args) throws Exception {
        //System.out.println(IpUtils.getAddress("136.27.231.86"));
        //System.out.println(IpUtils.getAddress("101.227.131.220"));
        System.out.println(IpUtils.getAddress("183.160.213.85"));
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author iwlnner
 * @Date 2022-09-06 17:27
 * @Description
 * @Version 1.0
 */
public class RequestUtil {

	//将请求参数转化成json格式
    public static String getRequestParamsJson(HttpServletRequest request){
        Map<String, String[]> parameterMap = request.getParameterMap();
        ObjectMapper objectMapper=new ObjectMapper();
        String paramJson = null;
        try {
            paramJson = objectMapper.writeValueAsString(parameterMap);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return paramJson;
    }

	//将请求头转化成json格式
    public static String getRequestHeaderJson(HttpServletRequest request){
        Map<String,String> headerMap=new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()){
            String key = headerNames.nextElement();
            headerMap.put(key,request.getHeader(key));
        }
        ObjectMapper objectMapper=new ObjectMapper();
        String headerJson = null;
        try {
            headerJson = objectMapper.writeValueAsString(headerMap);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return headerJson;
    }

	//由于公司使用了nginx代理,所以不能直接通过request.getRemoteAddr()获取,使用该方法获取真实ip的前提要保证nginx开启相关配置,后续会进行说明
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        } else {
            String ip = request.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Forwarded-For");
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Real-IP");
            }

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }

            return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
        }
    }
}

然后是SysLog的service,mapper相关业务类

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fw.hs.entity.SysLog;
import com.fw.hs.vo.query.SysLogQuery;

/**
 * 系统日志操作
 */
public interface ISysLogService extends IService<SysLog> {

    IPage<SysLog> getList(SysLogQuery query);

    SysLog detail(String id);
}
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fw.hs.entity.SysLog;
import com.fw.hs.mapper.SysLogMapper;
import com.fw.hs.service.ISysLogService;
import com.fw.hs.vo.query.SysLogQuery;
import org.springframework.stereotype.Service;

/**
 * 系统日志
 */
@Service
public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> implements ISysLogService {

    @Override
    public IPage<SysLog> getList(SysLogQuery query) {
        return baseMapper.getList(new Page<>(query.getPageNum(),query.getPageSize()),query);
    }

    @Override
    public SysLog detail(String id) {
        return baseMapper.selectById(id);
    }
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fw.hs.entity.SysLog;
import com.fw.hs.vo.query.SysLogQuery;
import org.apache.ibatis.annotations.Param;

/**
 * 系统日志
 */
public interface SysLogMapper extends BaseMapper<SysLog> {

    IPage<SysLog> getList(Page<SysLog> page, @Param("query") SysLogQuery query);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fw.hs.mapper.SysLogMapper">

    <select id="getList" resultType="com.fw.hs.entity.SysLog">
        select * from sys_log
        <where>
            <if test="query.type!='' and query.type!=null">
                and type=#{query.type}
            </if>
        </where>
    </select>
</mapper>

该有的都齐活了,于是编写最重要的aop增强类业务代码

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fw.hs.annotation.Logx;
import com.fw.hs.entity.SysLog;
import com.fw.hs.service.ISysLogService;
import com.fw.hs.utils.IpUtils;
import com.fw.hs.utils.RequestUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.*;

@Aspect
@Slf4j
public class SysLogAspect {
	
	//远程调用接口地址,以便获取当前系统登录用户信息
    @Value("${fw.bpm:http://demo:8008}")
    private String bpmUrl;
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private ISysLogService sysLogService;
    //公司应用服务器是cpu密集型服务器,核心线程数=系统核数+1
    private static final int MAXPOOLSIZE=Runtime.getRuntime().availableProcessors() + 1;

	//环绕通知
    @Around("@annotation(logx)")
    public Object around(ProceedingJoinPoint point, Logx logx) {
        Object obj=null;
        SysLog sysLog=new SysLog();
        sysLog.setType("操作日志");
        try{
        	//执行目标方法
            obj = point.proceed();
        } catch (Throwable throwable) {
            sysLog.setStackTrace(throwable.getStackTrace().toString());
            sysLog.setType("异常日志");
            writeData(point,sysLog,logx);
            throwable.printStackTrace();
        }
        //增强逻辑
        writeData(point,sysLog,logx);
        return obj;
    }

    private void writeData(ProceedingJoinPoint point,SysLog sysLog,Logx logx) {
    	//为了减轻服务器某一(理论上)高峰时刻的压力,以及减小创建-销毁线程的系统开销,使用线程池来托管线程
        ThreadPoolExecutor threadPool=new ThreadPoolExecutor(5
                ,MAXPOOLSIZE
                ,10L
                ,TimeUnit.SECONDS
                ,new LinkedBlockingDeque<>(3)
                ,Executors.defaultThreadFactory()
                ,new ThreadPoolExecutor.CallerRunsPolicy());
        try{
        	/*
        	这里一定要注意了,牵扯到request的操作不要放到线程里面,否则会有意想不到的“惊喜”,
        	避免这么做的原因简单来说会影响request域的生命周期,导致其他接口在复用该request对象的时候,
        	得到的是一个"被污染"的request,从而导致在某一段时间内,系统内某个原本正常的接口,
        	接收不到请求参数,这么说或许比较抽象,但如果你的业务中不需要用到request对象,那便再好不过了
        	*/
            String url = request.getRequestURI();
            //截取cookie中的token,放入请求头中
            String cookie = request.getHeader("Cookie");
            String requestMethod = request.getMethod();
            String requestParams = RequestUtil.getRequestParamsJson(request);
            String requestHeader = RequestUtil.getRequestHeaderJson(request);
            String ip = RequestUtil.getIpAddr(request);
            log.info("获取到当前请求客户端IP================》"+ip);
            //这里可以选择功能更加强大的CompletableFuture来实现
            FutureTask<SysLog> futureTask=new FutureTask(()->{
            	//为了防止该匿名内部类中的代码异常被子线程吞掉,try-catch它
                try {
                    String desc = logx.value();
                    if (StrUtil.isEmpty(desc)) {
                    	//如果没有为@Logx注解指定value,则获取被贴方法中@ApiOperation注解的value(接口注释)
                        MethodSignature signature = (MethodSignature) point.getSignature();
                        //获取目标方法,即被@Logx注解贴的方法
                        Method targetMethod = signature.getMethod();
                        //获取ApiOperation注解对象
                        ApiOperation annotation = targetMethod.getAnnotation(ApiOperation.class);
                        if (annotation != null) {
                            desc = annotation.value();
                        }
                    }
                    String qCellCore="未知位置";
                    if(StrUtil.isNotEmpty(ip)) {
                        qCellCore = IpUtils.getAddress(ip);
                    }
                    //获取当前用户
                    CloseableHttpClient client = HttpClients.createDefault();
                    HttpGet httpGet = new HttpGet(bpmUrl + "/fw/org/userResource/userMsg");
                    String auth = "";
                    if (StrUtil.isNotEmpty(cookie) && cookie.indexOf("Authorization=") >= 0) {
                        String authStr = cookie.substring(cookie.indexOf("Authorization="));
                        if(authStr.indexOf(";")>0) {
                            auth = authStr.substring(14, authStr.indexOf(";"));
                        }else {
                            auth=authStr.substring(14);
                        }
                    }
                    httpGet.setHeader("Authorization", auth);
                    HttpEntity entity = client.execute(httpGet).getEntity();
                    if (entity != null) {
                        String body = EntityUtils.toString(entity, "utf-8");
                        JSONObject jsonObject = JSON.parseObject(body);
                        if (jsonObject.getJSONObject("data") != null) {
                            JSONObject userJson = jsonObject.getJSONObject("data").getJSONObject("user");
                            String account = (String) userJson.get("account");
                            String userId = (String) userJson.get("userId");
                            sysLog.setAccount(account);
                            sysLog.setUserId(userId);
                        }
                    }
                    sysLog.setDescription(desc);
                    sysLog.setIp(ip);
                    sysLog.setQCellCore(qCellCore);
                    sysLog.setUrl(url);
                    sysLog.setReqMethod(requestMethod);
                    sysLog.setReqParam(requestParams);
                    sysLog.setHeader(requestHeader);
                    sysLog.setCreateTime(new Date());
                    sysLogService.save(sysLog);
                }catch (Throwable t){
                    t.printStackTrace();
                    log.error("*************生成系统日志异常:"+t.getMessage());
                }
                return sysLog;
            });
            //不需要获取线程计算结果,所以用execute方法即可
            threadPool.execute(futureTask);
        }finally{
            threadPool.shutdown();
        }
    }
}

那么到此,整个日志记录功能基本上已经实现了,先来感受一下效果

在这里插入图片描述

但是,别忘了,前面说的nginx真实ip相关配置还没有改,那么来吧,打开服务器上的nginx.conf文件,找到反向代理对应的location块,加上下面这段配置

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

加上之后效果

在这里插入图片描述

然后重启nginx服务和本地后端服务,访问上面被贴了@Logx注解的接口,查看数据库

在这里插入图片描述

成功生成了日志记录!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值