SpringBoot Shiro全局接口请求日志记录系统,线程池,ip解析缓存
作者:互联网
通过自定义注解的方式,切面记录请求,直接上代码
数据库结构
CREATE TABLE
oper_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;
}
}
以上就完成了,有疑问或纰漏 ,欢迎留言交流
标签:areaInfo,SpringBoot,ip,operLog,线程,utf8,public,String 来源: https://blog.csdn.net/weixin_44548582/article/details/115128201