- 前言
- 一、在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:
- 二、ApiOperation注解
- 三、创建库表与实体
- 1.在clickhouse中创建一张t_log表,用于保存用户的操作日志:
- 2.库表对应的实体
- 四、保存日志的方法
- 1、 mapper
- 2、 使用mybatisplus来操作clickhouse数据库。定义一个IOpenAppIdLogService接口,包含一个保存操作日志的抽象方法:
- 3、 接口的实现类
- 4、切面和切点
- 四、测试
- 总结
前言
提示:Spring Boot AOP记录用户操作日志并保存到clickhouse中:
记录用户操作日志功能,是使用AOP配合@ApiOperation注解的方式实现用户操作的监控。用户访问指定包包下面的Controller,从请求的参数列表中提取出用户的appId,方法参数。然后在Request中提取出请求的url,请求的方式,ip。将appId,操作内容,耗时,操作的方法,请求的url,请求的方式,操作的时间以及系统返回给用户的应答码等保存到clickhouse中表中。以此完成记录用户操作日志的功能。
提示:以下是本篇文章正文内容
一、在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:pom.xml中引入依赖:
org.springframework.boot
spring-boot-starter-aop
ru.yandex.clickhouse
clickhouse-jdbc
0.2.4
com.baomidou
mybatis-plus-boot-starter
3.4.2
com.jthinking.common
ip-info
2.1.4
二、ApiOperation注解
@ApiOperation注解,用于标注需要监控的方法:
@ApiIgnore
@RequestMapping(path = "health", method = RequestMethod.GET)
@ResponseBody
@ApiOperation(value = "我要监控这个接口", notes = "")
public RestResp test() {
return RestResp.ok();
}
三、创建库表与实体
1.在clickhouse中创建一张t_log表,用于保存用户的操作日志:
代码如下(示例):
CREATE TABLE 库名.t_log
(
`username` String COMMENT '用户名',
`operation` String COMMENT '操作内容',
`time` Decimal(11,
0) COMMENT '耗时',
`method` String COMMENT '操作方法',
`params` String COMMENT '方法参数',
`request_url` String COMMENT '请求URL',
`request_method` String COMMENT '请求方式',
`ip` String COMMENT '操作者IP',
`create_time` DateTime COMMENT '创建时间',
`location` String COMMENT '操作地点',
`response_code` Nullable(Int32) COMMENT '应答码',
`type` Int32 COMMENT '0系统内部操作日志,
rnrn1系统外部请求日志'
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(create_time)
PRIMARY KEY app_id
ORDER BY (app_id,
create_time)
TTL create_time + toIntervalDay(365)
SETTINGS index_granularity = 8192
2.库表对应的实体
代码如下(示例):
@Data
@Document
@TableName("t_log")
public class AppIdLog {
@TableField("username")
private String userName;
@TableField("operation")
private String operation;
@TableField("time")
private Long time;
@TableField("method")
private String method;
@TableField("params")
private String params;
@TableField("ip")
private String ip;
@TableField("create_time")
private Date createTime;
@TableField("location")
private String location;
@TableField("type")
private Long type;
@TableField("request_url")
private String requestUrl;
@TableField("request_method")
private String requestMethod;
@TableField("response_code")
private Integer responseCode;
}
四、保存日志的方法
提示:下面的@DS("clickhouse"),@DSTransactional()一定要加上否则的话访问的就是MySQL,而不是clickhouse!!!!!!
1、 mapper
@DS("clickhouse")
public interface AppIdLogMapper extends BaseMapper {
}
2、 使用mybatisplus来操作clickhouse数据库。定义一个IOpenAppIdLogService接口,包含一个保存操作日志的抽象方法:
@DS("clickhouse")
public interface IAppIdLogService extends IService {
@Async(FebsConstant.ASYNC_POOL)
void saveLog(ProceedingJoinPoint point, Method targetMethod, String ip, String operation, long start, int type, String url, String method);
}
3、 接口的实现类
@Slf4j @Service @RequiredArgsConstructor //如果有多数据源这里需要@DS注解。这样就是显示的指明操作的clickhouse数据库了,而不会去访问MySQL @DSTransactional() public class AppIdLogServiceImpl extends ServiceImpl4、切面和切点implements IAppIdLogService { private final ObjectMapper objectMapper; private final OpenAppIdLogMapper openAppIdLogMapper; @Override public void saveLog(ProceedingJoinPoint point, Method targetMethod, String ip, String operation, long start, int type, String url, String method) { AppIdLog openappidlog = new AppIdLog(); openappidlog.setIp(ip); //耗时 openappidlog.setTime(System.currentTimeMillis() - start); openappidlog.setOperation(operation); String className = point.getTarget().getClass().getName(); String methodName = targetMethod.getName(); openappidlog.setMethod(className + "." + methodName + "()"); Object[] args = point.getArgs(); LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = u.getParameterNames(targetMethod); if (args != null && paramNames != null) { StringBuilder params = new StringBuilder(); params = handleParams(params, args, Arrays.asList(paramNames),openappidlog); openappidlog.setParams(params.toString()); } //获取返回的数据,并且提前其中的code码 RestResp restResp = null; try { restResp = (RestResp) point.proceed(); } catch (Throwable throwable) { openappidlog.setResponseCode(null); log.error("不能转换为RestResp类型"); throwable.printStackTrace(); } if (restResp != null) { openappidlog.setResponseCode(restResp.getCode()); } //操作的时间 openappidlog.setCreateTime(new Date()); //根据ip获取地点 openappidlog.setLocation( IPInfoUtils.getIpInfo(ip)); openappidlog.setType((long) type); openappidlog.setRequestUrl(url); openappidlog.setRequestMethod(method); try { this.save(openappidlog); } catch (Exception e) { e.printStackTrace(); log.error("目标用户日志添加到表中失败:{},用户日志信息:{}", e, openappidlog); } } @SuppressWarnings("all") private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames, OpenAppIdLog openappidlog) { try { for (int i = 0; i < args.length; i++) { if (args[i] instanceof Map) { Set set = ((Map) args[i]).keySet(); List
@Aspect
@Slf4j
@Component
public class ControllerEndpointAspect extends BaseAspectSupport {
@Autowired
private IAppIdLogService itLogService;
@Pointcut("execution(* xxx.xxx.xxx.controller..*.*(..)) && @annotation(io.swagger.annotations.ApiOperation)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point){
Object result = null;
Method targetMethod = resolveMethod(point);
ApiOperation annotation = targetMethod.getAnnotation(ApiOperation.class);
String operation = annotation.value();
long start = System.currentTimeMillis();
try {
// 启动目标方法执行
result = point.proceed();
} catch (Throwable e) {
log.error("proceed方法启动目标方法执行失败:{}", e);
}
if (StringUtils.isNotBlank(operation)) {
//获取请求的url,方式,ip数据
HttpServletRequest request = getHttpServletRequest();
String ip = getHttpServletRequestIpAddress();
String url = request.getRequestURI();
String method = request.getMethod();
itLogService.saveLog(point, targetMethod, ip, operation, start, 1, url, method);
}
return result;
}
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
public static String getHttpServletRequestIpAddress() {
HttpServletRequest request = getHttpServletRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOW.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
四、测试
访问下列方法
@ApiIgnore
@RequestMapping(path = "health", method = RequestMethod.GET)
@ResponseBody
@ApiOperation(value = "我要监控这个接口", notes = "")
public RestResp test() {
return RestResp.ok();
}
在clickhouse对应的表中就可以查看到具体的数据
总结:
总的来说还是比较简单的,具体的可以参考https://mrbird.cc/Spring-Boot-AOP%20log.html。注意的是在配置文件中如:application.yaml配置了多数据源的情况下如下面的情况:
datasource:
base:
username: xxx
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${mysql.url}:3308/server?allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8
clickhouse:
username: xxx
password: xxxxxxx
driver-class-name: ru.yandex.clickhouse.ClickHouseDriver
url: jdbc:clickhouse://192.168.1.xxx:8123/jjj?socket_timeout=300000
druid:
min-evictable-idle-time-millis: 60000
上面同时配置了clickhouse与MySQL的数据源,这种情况的话就需要在mapper类上显示的打上@DS注解比如下面这种:
@DS("clickhouse")//这里与配置文件中配置的数据源名称一致
public interface OpenAppIdLogMapper extends BaseMapper {
}
...
//Service接口
@DS("clickhouse")
public interface IAppIdLogService extends IService {}
...
//接口实现类
@DSTransactional()
public class AppIdLogServiceImpl extends ServiceImpl implements IAppIdLogService {}
@DS("base")//因为默认是访问MySQL数据库可以不配置,但是如果出现了访问库出错的情况下,还是需要显示的配置的并且操作MySQL数据库,只需要在mapper上标记一下注解@DS
public interface AppidMapper extends BaseMapper {
}
当然不只是局限于clickhouse与MySQL,还有clickhouse,mongodb,MySQL三者同时使用也是上面的这样解决的。
希望对您有帮助!!!!!



