在实现自定义日志之前,我们需要了解AOP。
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,使得代码更模块化、更易于维护。横切关注点指的是那些存在于应用程序中多个模块中、并且对整个应用程序产生影响的功能,比如日志记录、事务管理、安全性等。
AOP的核心概念包括:
切面(Aspect): 切面是一组横切关注点的模块化单元,它定义了在何处(切点)、何时(通知)以及如何执行横切关注点。
连接点(Join Point): 连接点是在应用程序执行过程中能够应用切面的点,比如方法调用、异常处理等。
切点(Pointcut): 切点是一组连接点的集合,用于定义在何处应用切面。通常,切点通过表达式或者正则表达式定义。
通知(Advice): 通知是切面在连接点上执行的动作,它定义了在连接点的何时执行什么操作,比如在方法执行前后、抛出异常时执行的操作。
引入(Introduction): 引入允许我们向现有的类添加新的方法或属性,而不需要修改它们的源代码。
目标对象(Target Object): 目标对象是一个应用程序类,它可能包含切点,也可能不包含。
织入(Weaving): 织入是将切面应用到目标对象并创建新的代理对象的过程。织入可以在编译时、类加载时、运行时进行。
AOP主要用于解决横切关注点的问题,这些关注点通常涉及多个模块并且难以在代码中统一管理。通过使用AOP,可以使得这些关注点更加集中和可维护,提高了代码的可读性和可维护性。常见的AOP框架包括Spring AOP、AspectJ等。
**
**
<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
@Override
public 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 {
@Resource
AsyncOperLogService 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中进行使用。