在实现自定义日志之前,我们需要了解AOP。
1.AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,使得代码更模块化、更易于维护。横切关注点指的是那些存在于应用程序中多个模块中、并且对整个应用程序产生影响的功能,比如日志记录、事务管理、安全性等。
AOP的核心概念包括:切面(Aspect): 切面是一组横切关注点的模块化单元,它定义了在何处(切点)、何时(通知)以及如何执行横切关注点。连接点(Join Point): 连接点是在应用程序执行过程中能够应用切面的点,比如方法调用、异常处理等。切点(Pointcut): 切点是一组连接点的集合,用于定义在何处应用切面。通常,切点通过表达式或者正则表达式定义。通知(Advice): 通知是切面在连接点上执行的动作,它定义了在连接点的何时执行什么操作,比如在方法执行前后、抛出异常时执行的操作。引入(Introduction): 引入允许我们向现有的类添加新的方法或属性,而不需要修改它们的源代码。目标对象(Target Object): 目标对象是一个应用程序类,它可能包含切点,也可能不包含。织入(Weaving): 织入是将切面应用到目标对象并创建新的代理对象的过程。织入可以在编译时、类加载时、运行时进行。
AOP主要用于解决横切关注点的问题,这些关注点通常涉及多个模块并且难以在代码中统一管理。通过使用AOP,可以使得这些关注点更加集中和可维护,提高了代码的可读性和可维护性。常见的AOP框架包括Spring AOP、AspectJ等。
2.实现
**
第一步:需要在pom.xml文件中引入AOP 的依赖
**
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
第二步:自定义日志注解
@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;/*** 是否保存响应的参数*/public boolean isSaveResponseData() default true;
}
解释一下这里的注解的意思,没记错的话应该是java8新特性(也可能记错了)里的东西,想深入了解的可以去看看。
@Target 用于指定注解的作用范围,即注解可以被应用在哪些元素上。
在提供的例子中,@Target({ElementType.PARAMETER, ElementType.METHOD})
表示这个注解可以应用在方法的参数和方法上。
@Retention 用于指定注解的保留策略,即注解在何时生效。 RetentionPolicy.RUNTIME
表示注解会在运行时保留,因此可以通过反射机制来获取注解信息。
@Documented 用于指定注解是否应该被 javadoc 工具记录。 如果一个注解被 @Documented 标记,那么它会被
javadoc 工具提取并文档化,这使得该注解的信息可以包含在生成的文档中。
这三个元注解的使用通常是一起的,它们提供了对注解的定义、应用范围和保留策略的灵活控制。在提供的例子中,这个注解的含义是可以应用在方法参数和方法上,在运行时保留,并且会被 javadoc 工具文档化。
第三步,构建你的操作类型,或者操作人(使用枚举)
* 业务操作类型**/
public enum BusinessType {/*** 其它*/OTHER,/*** 新增*/INSERT,/*** 修改*/UPDATE,/*** 删除*/DELETE,/*** 授权*/ASSGIN,/*** 导出*/EXPORT,/*** 导入*/IMPORT,/*** 强退*/FORCE,/*** 更新状态*/STATUS,/*** 清空数据*/CLEAN,/*** 批量删除*/BATCHDELETE,
}
* 操作人类别**/
public enum OperatorType {/*** 其它*/OTHER,/*** 后台用户*/MANAGE,/*** 手机端用户*/MOBILE
}
第四步,
在这里我们要建立业务层,用来将生产日志保存在数据库中
public interface AsyncOperLogService {public void savaOperLog(BhOperLog bhOperLog);
}
通过实现类完成具体插入代码,这里使用mybatisplus,不了解的可以看我之前的笔记MyBatisPlus
@Overridepublic void savaOperLog(BhOperLog bhOperLog) {bhOperLogMapper.insert(bhOperLog);}
下面是具体的实体类
@Data
@ApiModel(description = "系统操作日志")
@TableName("你的表名称")
public class OperLog extends BaseEntity {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "id")private long id;@ApiModelProperty(value = "模块标题")@TableField("title")private String title;@ApiModelProperty(value = "业务类型(0其它 1新增 2修改 3删除)")@TableField("business_type")private String businessType;@ApiModelProperty(value = "方法名称")@TableField("method")private String method;@ApiModelProperty(value = "请求方式")@TableField("request_method")private String requestMethod;@ApiModelProperty(value = "操作类别(0其它 1后台用户 2手机端用户)")@TableField("operator_type")private String operatorType;@ApiModelProperty(value = "操作人员")@TableField("oper_name")private String operName;@ApiModelProperty(value = "部门名称")@TableField("dept_name")private String deptName;@ApiModelProperty(value = "请求URL")@TableField("oper_url")private String operUrl;@ApiModelProperty(value = "主机地址")@TableField("oper_ip")private String operIp;@ApiModelProperty(value = "请求参数")@TableField("oper_param")private String operParam;@ApiModelProperty(value = "返回参数")@TableField("json_result")private String jsonResult;@ApiModelProperty(value = "操作状态(0正常 1异常)")@TableField("status")private Integer status;@ApiModelProperty(value = "错误消息")@TableField("error_msg")private String errorMsg;@ApiModelProperty(value = "操作时间")@TableField("oper_time")private Date operTime;
}
第五步,完成具体的日志配置开发
下面是完整代码
* 操作日志记录处理**/
@Aspect
@Component
public class LogAspect {@ResourceAsyncOperLogService asyncOperLogService;/*** 处理完请求后执行** @param joinPoint 切点*/@AfterReturning(pointcut = "@annotation(controllerLog)",returning = "jsonResult")public void doAfterReturnin(JoinPoint joinPoint, Log controllerLog, Object jsonResult){handleLog(joinPoint, controllerLog,null, jsonResult);}protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {try {RequestAttributes ra = RequestContextHolder.getRequestAttributes();ServletRequestAttributes sra = (ServletRequestAttributes) ra;HttpServletRequest request = sra.getRequest();// *========数据库日志=========*//BhOperLog operLog = new BhOperLog();operLog.setStatus(1);// 请求的地址 IpUtil.getIpAddr(ServletUtils.getRequest());String ip = IpUtil.getIpAddress(request);operLog.setOperIp(ip);operLog.setOperUrl(request.getRequestURI());String token = request.getHeader("token");String userName = JwtUtils.getUsername(token);operLog.setOperName(userName);if (e != null) {operLog.setStatus(0);operLog.setErrorMsg(e.getMessage());}// 设置方法名称String className = joinPoint.getTarget().getClass().getName();String methodName = joinPoint.getSignature().getName();operLog.setMethod(className + "." + methodName + "()");// 设置请求方式operLog.setRequestMethod(request.getMethod());// 处理设置注解上的参数getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);// 保存数据库asyncOperLogService.savaOperLog(operLog);} catch (Exception exp) {// 记录本地异常日志
// log.error("==前置通知异常==");
// log.error("异常信息:{}", exp.getMessage());exp.printStackTrace();}}/*** 获取注解中对方法的描述信息 用于Controller层注解** @param log 日志* @param operLog 操作日志* @throws Exception*/public void getControllerMethodDescription(JoinPoint joinPoint, Log log, BhOperLog operLog, Object jsonResult) throws Exception {// 设置action动作operLog.setBusinessType(log.businessType().name());// 设置标题operLog.setTitle(log.title());// 设置操作人类别operLog.setOperatorType(log.operatorType().name());// 是否需要保存request,参数和值if (log.isSaveRequestData()) {// 获取参数的信息,传入到数据库中。setRequestValue(joinPoint, operLog);}// 是否需要保存response,参数和值if (log.isSaveResponseData() && !StringUtils.isEmpty(jsonResult)) {operLog.setJsonResult(JacksonUtils.obj2String(jsonResult));}}/*** 获取请求的参数,放到log中** @param operLog 操作日志* @throws Exception 异常*/private void setRequestValue(JoinPoint joinPoint, BhOperLog 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(params);}}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray) {String params = "";if (paramsArray != null && paramsArray.length > 0) {for (Object o : paramsArray) {if (!StringUtils.isEmpty(o) && !isFilterObject(o)) {try {String jsonObj = JacksonUtils.obj2String(o);params += jsonObj + " ";} catch (Exception e) {}}}}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 (Object value : collection) {return value instanceof MultipartFile;}} else if (Map.class.isAssignableFrom(clazz)) {Map map = (Map) o;for (Object value : map.entrySet()) {Map.Entry entry = (Map.Entry) value;return entry.getValue() instanceof MultipartFile;}}return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse|| o instanceof BindingResult;}}
那么如何使用呢?
只需要在你需要的Controller中加入
这个注解就可以了
@Log(title = "菜单管理", businessType = BusinessType.STATUS) 其中title根据具体模块书写,businessType就是你自定义的操作类型
这样就完成了自定义日志的开发,也可以将这些代码通过自定义start进行打包,加入Maven中进行使用。