SpringBoot Shiro全局接口请求日志记录系统,线程池,ip解析缓存

通过自定义注解的方式,切面记录请求,直接上代码

数据库结构

在这里插入图片描述CREATE TABLEoper_log(idbigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id主键',titlevarchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '模块标题',business_typeint(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',methodvarchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '方法名称',request_methodvarchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '请求方式',oper_urlvarchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '请求URL',oper_useridvarchar(50) DEFAULT NULL COMMENT '请求人',oper_ipvarchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '操作ip地址',oper_locationvarchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '操作地点',oper_paramvarchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '请求参数',record_json_resulttinyint(1) DEFAULT '0' COMMENT '是否记录返回值',json_resulttext CHARACTER SET utf8 COLLATE utf8_general_ci COMMENT '返回值',statusint(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',error_msgvarchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '错误消息',oper_timedatetime DEFAULT NULL COMMENT '操作时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

自定义注解

@Target({
    
    ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    
    
    /**
     * 描述
     */
    String title() default "";

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;

    /**
     * 参数位置
     *
     * @return link拼接参数,body请求体参数
     */
    DataType dataLocationType() default DataType.link;

    /**
     * 是否需要记录返回值
     * @return
     */
    boolean didNeedReturnValue() default false;
}

功能枚举类

public enum BusinessType
{
    
    
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 登出
     */
    FORCE,
    
    /**
     * 清空数据
     */
    CLEAN,

    /**
     * 查询
     */
    SELECT
}

参数位置枚举类

public enum DataType {
    
    

    /**
     * 请求行
     */
    link,

    /**
     * 请求体
     */
    body;
}

接口添加注解

    @GetMapping("/testApi")
    @Log(title = "测试接口注解", businessType = BusinessType.SELECT, dataLocationType = DataType.link,didNeedReturnValue=false)
    public BaseResponse testApi(){
    
    
        return BaseResponse.ok();
    }

切面实现

@Aspect
@Component
@Slf4j
public class LogAspect {
    
    

    public static final int businessStatusFail = 1;
    public static final int businessStatusSuccess = 0;


    @Autowired
    private LogService logService;

    /**
     * 专切有@Log日志注解的面
     */
    @Pointcut("@annotation(com.xxx.annotation.Log)")
    public void logPointCut() {
    
    
    }

    /**
     * 业务执行完成后 处理日志
     *
     * @param joinPoint
     * @param jsonResult
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
    
    
        operationLog(joinPoint, null, jsonResult);
    }

    /**
     * 业务抛出异常的日志处理
     *
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
    
    
        operationLog(joinPoint, e, null);
    }

    /**
     * 日志处理主逻辑
     *
     * @param joinPoint
     * @param e          异常
     * @param jsonResult 返回数据
     */
    void operationLog(JoinPoint joinPoint, Exception e, Object jsonResult) {
    
    
        try {
    
    
            //根据oper_log表生成的实体类
            OperLog operLog = new OperLog();
            //操作时间
            operLog.setOperTime(new Date());
            //先获取当前用户
            String userId = ShiroUserUtil.getShiroUserId();
            operLog.setOperUserid(userId);
            //业务操作状态
            if (e != null) {
    
    
                //异常信息
                operLog.setStatus(businessStatusFail);
                String errorMsg;
                if (EmptyUtil.isNullOrEmpty(e.getMessage())) {
    
    
                    errorMsg = e.toString();
                } else {
    
    
                    errorMsg = e.getMessage();
                }
                if (errorMsg != null && errorMsg.length() > 2000) {
    
    
                    errorMsg.substring(0, 2000);
                }
                operLog.setErrorMsg(errorMsg);
            } else if (userId != null && e == null) {
    
    
                //异常信息为空,有用户
                operLog.setStatus(businessStatusSuccess);
            } else if (userId == null) {
    
    
                //异常信息为空,且没有用户
                operLog.setOperUserid("未登录用户/游客");
            }
            //获取注解内容
            Log logContent = getLogContent(joinPoint);
            if (logContent == null) {
    
    
                return;
            }
            //是否需要记录返回值
            operLog.setRecordJsonResult(logContent.didNeedReturnValue());
            if (logContent.didNeedReturnValue()) {
    
    
                if (jsonResult != null) {
    
    
                    operLog.setJsonResult(JSONObject.toJSONString(jsonResult));
                }
            }
            //业务类型
            operLog.setBusinessType(logContent.businessType().ordinal());
            //业务标题
            operLog.setTitle(logContent.title());
            //解析请求地址
            operLog.setOperIp(ShiroUserUtil.getIp());
            //请求参数
            String jsonString = "";
            if (logContent.dataLocationType() == DataType.link) {
    
    
                //请求行参数
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                jsonString = JSONObject.toJSONString(request.getParameterMap());
            } else {
    
    
                //请求体参数
                jsonString = JSON.toJSONString(joinPoint.getArgs()[0]);
            }
            //如果超出数据库字段长度
            if (jsonString.length() > 2000) {
    
    
                jsonString.substring(0, 2000);
            }
            if(EmojiManager.containsEmoji(jsonString)){
    
    
                jsonString=EmojiParser.parseToAliases(jsonString);
            }
            operLog.setOperParam(jsonString);
            //方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            operLog.setOperUrl(request.getRequestURI());
            operLog.setRequestMethod(request.getMethod());
            //添加任务
            logService.saveOperLog(operLog);
        } catch (Exception ex) {
    
    
            log.error("记录日志异常,{}", ex.getMessage());
        }
    }

    /**
     * 获取注解的内容
     * @param joinPoint
     * @return
     */
    private Log getLogContent(JoinPoint joinPoint) {
    
    
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
    
    
            return method.getAnnotation(Log.class);
        }
        return null;
    }
}

关于切面中获取请求的当前用户id和请求的ip属于安全认证授权框架Shiro的内容,大家用法不同,自行解决

logService.saveOperLog(operLog)操作需要使用到线程池,先上线程池

线程池配置

@Configuration
@EnableAsync
public class AsyncConfig {
    
    

    /**
     * 配置核心线程数
     */
    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;

    /**
     * 配置最大线程数
     */
    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;

    /**
     * 配置队列大小
     */
    @Value("${async.executor.thread.queue_capacity}")
    private int queueCapacity;

    /**
     * 配置线程池中的线程的名称前缀
     */
    private String threadNamePrefix = "AsyncExecutorThread-";

    @Bean(name = "asyncExecutor")
    public Executor asyncExecutor() {
    
    
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //初始化
        executor.initialize();
        return executor;
    }
}

线程池参数在application.yml中

async:
  executor:
    thread:
      core_pool_size: 10
      max_pool_size: 20
      queue_capacity: 500

service层一个接口类一个实现类和Mapper的SQL插入到表就不多做赘述了

logService

@Service
@Slf4j
public class LogServiceImpl implements LogService {
    
    

    @Autowired
    private OperLogMapper operLogMapper;

	//ip地址库临时存放,因为淘宝ip解析请求qps应该是1,请求多了老拒绝,也不至于存在持久层
    private static Map<String, AreaInfo> addressMap = new ConcurrentHashMap<String, AreaInfo>();

    @Override
    @Async(value="asyncExecutor") //此注解使方法交给线程池处理,name配置在AsyncConfig中
    public void saveOperLog(OperLog operLog) {
    
    
        try {
    
    
            if (!EmptyUtil.isNullOrEmpty(operLog.getOperIp())) {
    
    
                AreaInfo address = getAddress(operLog.getOperIp());
                operLog.setOperLocation(address.getProvince() + " " + address.getCity());
            } else {
    
    
                log.error("[日志记录]无效的ip请求:{}", operLog.getOperIp());
            }
        } catch (Exception e) {
    
    
            log.error("解析请求ip:{}真实地址异常:{}", operLog.getOperIp(), e.getMessage());
        }
        //插入日志
        operLogMapper.insertSelective(operLog);
    }

	//获取地址
    public AreaInfo getAddress(String ip) {
    
    
        AreaInfo areaInfo = null;
        //存的有点儿多了
        if (addressMap.size() > 20000) {
    
    
            addressMap.clear();
        }
        //缓存中找
        areaInfo = addressMap.get(ip);
        if (areaInfo == null) {
    
    
        	//请求阿里
            areaInfo = AddressUtils.getRealAddressByIP(ip);
            //请求到有效地址
            if (areaInfo != null && !"XX".equals(areaInfo.getProvince()) && !"内网".equals(areaInfo.getProvince())) {
    
    
            	//放入缓存
                addressMap.put(ip, areaInfo);
            }
        }
        return areaInfo;
    }
}

AddressUtils工具类

@Slf4j
public class AddressUtils {
    
    

    public static final String IP_URL = "http://ip.taobao.com/outGetIpInfo";

    public static AreaInfo getRealAddressByIP(String ip) {
    
    
        AreaInfo areaInfo = new AreaInfo();
        // 内网不查询
        if (IpUtils.internalIp(ip)) {
    
    
            areaInfo.setProvince("内网");
            areaInfo.setCity("IP");
            return areaInfo;
        }
        String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&accessKey=alibaba-inc");
        if (StringUtils.isBlank(rspStr)) {
    
    
            log.error("获取地理位置异常 {}", ip);
            areaInfo.setProvince("XX");
            areaInfo.setCity("XX");
            return areaInfo;
        }
        JSONObject obj = JSONObject.parseObject(rspStr);
        JSONObject data = obj.getObject("data", JSONObject.class);
        String province = data.getString("region");
        String city = data.getString("city");
        areaInfo.setProvince(province);
        areaInfo.setCity(city);
        return areaInfo;
    }
 }

AreaInfo类是AliPay提供的,也可以自建,内容就两个属性

package com.alipay.api.domain;

import com.alipay.api.AlipayObject;
import com.alipay.api.internal.mapping.ApiField;

/**
 * 省份城市地区
 *
 * @author auto create
 * @since 1.0, 2016-10-26 17:43:41
 */
public class AreaInfo extends AlipayObject {
    
    

	private static final long serialVersionUID = 6841749274545331483L;

	/**
	 * 城市
	 */
	@ApiField("city")
	private String city;

	/**
	 * 省份
	 */
	@ApiField("province")
	private String province;

	public String getCity() {
    
    
		return this.city;
	}
	public void setCity(String city) {
    
    
		this.city = city;
	}

	public String getProvince() {
    
    
		return this.province;
	}
	public void setProvince(String province) {
    
    
		this.province = province;
	}

}

以上就完成了,有疑问或纰漏 ,欢迎留言交流

猜你喜欢

转载自blog.csdn.net/weixin_44548582/article/details/115128201