项目中经常会遇到这样一个需求,需要监控每个controller
中接口被调用的情况。
比如某个接口被调用的时间,哪个用户调用的,请求参数是什么,返回值是什么等等。
并且调用情况需要存储到数据库中,此时就可以AOP
为核心来封装这个功能。
org.springframework.boot spring-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
。
为了程序的可读性和可维护性,应该给它们设置对应枚举类,来统一维护这些状态信息的含义。
public enum BusinessType{OTHER, // 其它INSERT, // 新增UPDATE, // 修改DELETE, // 删除GRANT, // 授权EXPORT, // 导出IMPORT, // 导入FORCE, // 强退GENCODE, // 生成代码CLEAN, // 清空数据
}
public enum OperatorType{OTHER, // 其它MANAGE, // 后台用户MOBILE // 手机端用户
}
public enum BusinessStatus{SUCCESS, // 成功 FAIL, // 失败
}
准备这个工具类可以更方便的在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;}
}
@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
配置织入点,让所有加了@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
拦截,把当前请求的所有信息保存到数据库当中。
之后再辅以前端页面,就可以实现对各个接口操作情况的分析。
下一篇:运算放大器的理解与应用