公司系统需要新增一个需求,可以在需要的地方记录下接口调用的日志。手动在每个需要的接口里面写记录日志的功能不现实,所以这个需求嘛,首选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注解的接口,查看数据库
成功生成了日志记录!