AOP结合注解实现项目中接口调用情况监控
创始人
2024-04-11 20:26:56
0

一、概述

项目中经常会遇到这样一个需求,需要监控每个controller中接口被调用的情况。

比如某个接口被调用的时间,哪个用户调用的,请求参数是什么,返回值是什么等等。

并且调用情况需要存储到数据库中,此时就可以AOP为核心来封装这个功能。

二、引入依赖


org.springframework.bootspring-boot-starter-aop

要使用AOP,是需要先引入依赖才可以使用的

三、创建保存日志信息的实体类

/*** 操作日志记录表*/
@Data
@TableName("sys_log_oper")
public class SysLogOperEntity
{private static final long serialVersionUID = 1L;@TableIdprivate Long id;                    // 日志主键private String title;               // 操作模块// 业务类型(0=其它,1=新增,2=修改,3=删除,4=授权,5=导出,6=导入,7=强退,8=生成代码,9=清空数据)private Integer businessType;       @TableField(exist = false)private Integer[] businessTypes;    // 业务类型数组private String method;              // 请求方法的路径private String requestMethod;       // 请求方式private Integer operatorType;       // 操作类别(0其它 1后台用户 2手机端用户)private String operName;            // 操作人员private String orgName;             // 部门名称private String operUrl;             // 请求接口地址private String operIp;              // 操作ip地址private String operLocation;        // 操作地点private String operParam;           // 请求参数private String jsonResult;          // 返回参数private Integer status;             // 操作状态(0正常 1异常)private String errorMsg;            // 错误消息private Date operTime;              // 操作时间}

四、定义枚举类

实体类中涉及的状态信息,比如:

业务类型businessType、操作类别operatorType、操作状态status

为了程序的可读性和可维护性,应该给它们设置对应枚举类,来统一维护这些状态信息的含义。

4.1 业务类型枚举

public enum BusinessType{OTHER,		// 其它INSERT,		// 新增UPDATE,		// 修改DELETE,		// 删除GRANT,		// 授权EXPORT,		// 导出IMPORT,		// 导入FORCE,		// 强退GENCODE,	// 生成代码CLEAN,		// 清空数据
}

4.2 操作类别枚举

public enum OperatorType{OTHER,		// 其它MANAGE,		// 后台用户MOBILE		// 手机端用户
}

4.3 操作状态

public enum BusinessStatus{SUCCESS,	// 成功 FAIL,		// 失败
}

五、准备一个servlet工具类

准备这个工具类可以更方便的在AOP的切面类中操作servlet

import net.maku.framework.common.text.Convert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;/*** Servlet户端工具类*/
public class ServletUtils
{/*** 获取String参数*/public static String getParameter(String name) {return getRequest().getParameter(name);}/*** 获取String参数*/public static String getParameter(String name, String defaultValue) {return Convert.toStr(getRequest().getParameter(name), defaultValue);}/*** 获取Integer参数*/public static Integer getParameterToInt(String name) {return Convert.toInt(getRequest().getParameter(name));}/*** 获取Integer参数*/public static Integer getParameterToInt(String name, Integer defaultValue) {return Convert.toInt(getRequest().getParameter(name), defaultValue);}/*** 获取request*/public static HttpServletRequest getRequest() {return getRequestAttributes().getRequest();}/*** 获取response*/public static HttpServletResponse getResponse() {return getRequestAttributes().getResponse();}/*** 获取session*/public static HttpSession getSession() {return getRequest().getSession();}public static ServletRequestAttributes getRequestAttributes() {RequestAttributes attributes = RequestContextHolder.getRequestAttributes();return (ServletRequestAttributes) attributes;}/*** 将字符串渲染到客户端* @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}/*** 是否是Ajax异步请求* @param request*/public static boolean isAjaxRequest(HttpServletRequest request) {String accept = request.getHeader("accept");if (accept != null && accept.indexOf("application/json") != -1){return true;}String xRequestedWith = request.getHeader("X-Requested-With");if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1){return true;}String uri = request.getRequestURI();if (inStringIgnoreCase(uri, ".json", ".xml")){return true;}String ajax = request.getParameter("__ajax");if (inStringIgnoreCase(ajax, "json", "xml")){return true;}return false;}/*** 判断str是否包含参数中的某个字符串* @param str 验证字符串* @param strs 字符串组* @return 包含返回true*/public static boolean inStringIgnoreCase(String str, String... strs) {if (str != null && strs != null) {for (String s : strs) {String target = s == null ? "" : s.trim();if (str.equalsIgnoreCase(target)) {return true;}}}return false;}
}

六、准备一个ip地址解析工具类

@Aspect
@Component
@Slf4j
public class MethodLogAspect {// 缓存线程池ExecutorService executorService = Executors.newCachedThreadPool();@Resourceprivate SysLogOperService sysLogOperService;         // 日志操作@Resourceprivate SysOrgService sysOrgService;                 // 机构操作// 配置织入点 之后下面的注解可以使用它作为切点// 意思拦截带有Log注解的方法@Pointcut("@annotation(net.maku.Log.annotation.Log)")public void logPointCut() {}/*** 处理完请求后执行* @param joinPoint 切点*/@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {handleLog(joinPoint, null, jsonResult);}/*** 拦截异常操作* 抛出异常后执行* @param joinPoint 切点* @param e         异常*/@AfterThrowing(value = "logPointCut()", throwing = "e")public void doAfterThrowing(JoinPoint joinPoint, Exception e) {handleLog(joinPoint, e, null);}protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {try {// 获得注解Log controllerLog = getAnnotationLog(joinPoint);if (controllerLog == null) {return;}// 获取当前的用户;UserDetail loginUser = SecurityUser.getUser();;// *========数据库日志=========*//SysLogOperEntity operLog = new SysLogOperEntity();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 请求的地址String ip = IpUtils.getIpAddr(ServletUtils.getRequest());operLog.setOperIp(ip);// 返回参数operLog.setJsonResult(JSON.toJSONString(jsonResult));operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());if (loginUser != null) {operLog.setOperName(loginUser.getUsername());}if (e != null) {// 获取方法异常信息// ordinal 用于获取当前枚举在定义时的索引, 从 0 开始 依次累加,operLog.setStatus(BusinessStatus.FAIL.ordinal());operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog);// 设置保存时间operLog.setOperTime(new Date());// 设置操作位置operLog.setOperLocation(AddressUtils.getAddressByIP(operLog.getOperIp()));// 保存数据库// 为了减轻未来并发量提升保存日志带来的性能损耗,使用线程池去执行保存日志记录的操作executorService.submit(new Runnable() {@Overridepublic void run() {try {// 设置机构名称Long orgId = loginUser.getOrgId();if (orgId != null) {SysOrgEntity sysOrgEntity = sysOrgService.getById(orgId);if (sysOrgEntity != null) {operLog.setOrgName(sysOrgEntity.getName());}}// 保存日志sysLogOperService.save(operLog);}catch (Exception te) {log.error("操作日志异常信息(线程池):{}", te.getMessage());te.printStackTrace();}}});} catch (Exception exp) {// 记录本地异常日志log.error("操作日志异常信息:{}", exp.getMessage());exp.printStackTrace();}}/*** 获取注解中对方法的描述信息 用于Controller层注解* @param log     日志* @param operLog 操作日志* @throws Exception*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLogOperEntity operLog) throws Exception {// 设置action动作System.out.println(log.businessType());operLog.setBusinessType(log.businessType().ordinal());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().ordinal());// 是否需要保存request,参数和值if (log.isSaveRequestData()) {// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog);}}/*** 获取请求的参数,放到log中* @param operLog 操作日志* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, SysLogOperEntity operLog) throws Exception {String requestMethod = operLog.getRequestMethod();if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {String params = argsArrayToString(joinPoint.getArgs());operLog.setOperParam(StringUtils.substring(params, 0, 2000));} else {Map paramsMap = (Map) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));}}/*** 是否存在注解,如果存在就获取*/private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();if (method != null) {return method.getAnnotation(Log.class);}return null;}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray) {String params = "";if (paramsArray != null && paramsArray.length > 0) {for (int i = 0; i < paramsArray.length; i++) {if (!isFilterObject(paramsArray[i])) {Object jsonObj = JSON.toJSON(paramsArray[i]);params += jsonObj.toString() + " ";}}}return params.trim();}/*** 判断是否需要过滤的对象。* @param o 对象信息。* @return 如果是需要过滤的对象,则返回true;否则返回false。*/@SuppressWarnings("rawtypes")public boolean isFilterObject(final Object o) {Class clazz = o.getClass();if (clazz.isArray()) {return clazz.getComponentType().isAssignableFrom(MultipartFile.class);} else if (Collection.class.isAssignableFrom(clazz)) {Collection collection = (Collection) o;for (Iterator iter = collection.iterator(); iter.hasNext();) {return iter.next() instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {Map map = (Map) o;for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {Map.Entry entry = (Map.Entry) iter.next();return entry.getValue() instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;}
}

八、自定义注解

为了程序的可拓展性、可维护性、灵活性。

我定义一个自定义注解,思路是只有加了这个注解的controller方法,才会被记录操作日志。

因为可能业务中并不是所有需求都是要监听全部controller中的方法的。这样灵活性会更强一些。

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log{public String title() default "";			// 属于哪个模块,填写功能模块名称,例如:用户管理 public BusinessType businessType() default BusinessType.OTHER;	  // 功能类别public OperatorType operatorType() default OperatorType.MANAGE;	  // 操作人类别public boolean isSaveRequestData() default true;				  // 是否保存请求的参数
}

七、定义AOP切面

核心来了。

这里的操作思路是,使用AOP配置织入点,让所有加了@Lag注解方法都可以被监控到。

@Aspect
@Component
@Slf4j
public class MethodLogAspect {// 缓存线程池ExecutorService executorService = Executors.newCachedThreadPool();@Resourceprivate SysLogOperService sysLogOperService;         // 日志操作@Resourceprivate SysOrgService sysOrgService;                 // 机构操作// 配置织入点 之后下面的注解可以使用它作为切点// 意思拦截带有Log注解的方法@Pointcut("@annotation(net.maku.Log.annotation.Log)")public void logPointCut() {}/*** 处理完请求后执行* @param joinPoint 切点*/@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {handleLog(joinPoint, null, jsonResult);}/*** 拦截异常操作* 抛出异常后执行* @param joinPoint 切点* @param e         异常*/@AfterThrowing(value = "logPointCut()", throwing = "e")public void doAfterThrowing(JoinPoint joinPoint, Exception e) {handleLog(joinPoint, e, null);}protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {try {// 获得注解Log controllerLog = getAnnotationLog(joinPoint);if (controllerLog == null) {return;}// 获取当前的用户;UserDetail loginUser = SecurityUser.getUser();;// *========数据库日志=========*//SysLogOperEntity operLog = new SysLogOperEntity();operLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 请求的地址String ip = IpUtils.getIpAddr(ServletUtils.getRequest());operLog.setOperIp(ip);// 返回参数operLog.setJsonResult(JSON.toJSONString(jsonResult));operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());if (loginUser != null) {operLog.setOperName(loginUser.getUsername());}if (e != null) {// 获取方法异常信息// ordinal 用于获取当前枚举在定义时的索引, 从 0 开始 依次累加,operLog.setStatus(BusinessStatus.FAIL.ordinal());operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(ServletUtils.getRequest().getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog);// 设置保存时间operLog.setOperTime(new Date());// 设置操作位置operLog.setOperLocation(AddressUtils.getAddressByIP(operLog.getOperIp()));// 保存数据库// 为了减轻未来并发量提升保存日志带来的性能损耗,使用线程池去执行保存日志记录的操作executorService.submit(new Runnable() {@Overridepublic void run() {try {// 设置机构名称Long orgId = loginUser.getOrgId();if (orgId != null) {SysOrgEntity sysOrgEntity = sysOrgService.getById(orgId);if (sysOrgEntity != null) {operLog.setOrgName(sysOrgEntity.getName());}}// 保存日志sysLogOperService.save(operLog);}catch (Exception te) {log.error("操作日志异常信息(线程池):{}", te.getMessage());te.printStackTrace();}}});} catch (Exception exp) {// 记录本地异常日志log.error("操作日志异常信息:{}", exp.getMessage());exp.printStackTrace();}}/*** 获取注解中对方法的描述信息 用于Controller层注解* @param log     日志* @param operLog 操作日志* @throws Exception*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysLogOperEntity operLog) throws Exception {// 设置action动作System.out.println(log.businessType());operLog.setBusinessType(log.businessType().ordinal());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().ordinal());// 是否需要保存request,参数和值if (log.isSaveRequestData()) {// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog);}}/*** 获取请求的参数,放到log中* @param operLog 操作日志* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, SysLogOperEntity operLog) throws Exception {String requestMethod = operLog.getRequestMethod();if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {String params = argsArrayToString(joinPoint.getArgs());operLog.setOperParam(StringUtils.substring(params, 0, 2000));} else {Map paramsMap = (Map) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));}}/*** 是否存在注解,如果存在就获取*/private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();if (method != null) {return method.getAnnotation(Log.class);}return null;}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray) {String params = "";if (paramsArray != null && paramsArray.length > 0) {for (int i = 0; i < paramsArray.length; i++) {if (!isFilterObject(paramsArray[i])) {Object jsonObj = JSON.toJSON(paramsArray[i]);params += jsonObj.toString() + " ";}}}return params.trim();}/*** 判断是否需要过滤的对象。* @param o 对象信息。* @return 如果是需要过滤的对象,则返回true;否则返回false。*/@SuppressWarnings("rawtypes")public boolean isFilterObject(final Object o) {Class clazz = o.getClass();if (clazz.isArray()) {return clazz.getComponentType().isAssignableFrom(MultipartFile.class);} else if (Collection.class.isAssignableFrom(clazz)) {Collection collection = (Collection) o;for (Iterator iter = collection.iterator(); iter.hasNext();) {return iter.next() instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {Map map = (Map) o;for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {Map.Entry entry = (Map.Entry) iter.next();return entry.getValue() instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;}
}

八、使用示例

@RestController
@RequestMapping("user")
@AllArgsConstructor
public class SysUserController {private final SysUserService sysUserService;@Log(title = "用户管理", businessType = BusinessType.Query, operatorType = OperatorType.MANAGE)@GetMapping("page")public Result> page(@Valid SysUserQuery query){PageResult page = sysUserService.page(query);return Result.ok(page);}
}    

这样一来,page方法被调用时,就会就AOP拦截,把当前请求的所有信息保存到数据库当中。
在这里插入图片描述

之后再辅以前端页面,就可以实现对各个接口操作情况的分析。

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...