springboot通过注解+AOP切片实现日志记录

springboot通过注解+AOP切片实现日志记录


目录




内容

此日志记录基于springboot + mybis以及规范的Controller、dao、entity命名

3、日志记录

3.1、注解

  • 说明:在项目中需要把增删改操作记录日志,存如数据库,使用传统方式,在需要的地方new logger方式太麻烦,耦合性太高;在这里我们用注解+AOP切片的方式来记录日志。

  • 记录内容

    • 登录账号
    • 登录姓名
    • 操作内容
      • 添加操作
        • 添加对应的实体类名
        • 添加的实体类对象属性
          • 属性名
          • 属性值
      • 修改
        • 修改对应的实体类
        • 修改实体类对象属性
          • 属性名
          • 修改前值
          • 修改后的值
      • 删除操作
        • 删除对应的实体类名
        • 删除实体类对象属性
          • 属性名
          • 属性值
    • 时间:(用时或者操作时间)
    • IP:IP地址
  • 环境:人人权限管理系统(spingboot+shiro+springmvc+mybatis-plus)

详细注解及切片处理如下

3.1.1、@ClassDesc 实体类名

  • @ClassDesc:实体类名

      package io.renren.common.annotation;
    
      import java.lang.annotation.*;
    
      /**
       * 自定义注解
       *   类描述
       *   用于日志记录
       */
      @Target(ElementType.TYPE)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface ClassDesc {
      	// 类名称
      	String value() default "";
      }
    

3.1.2、@AttibuteDesc 属性描述

  • 作用:
    • 用于标志哪个属性需要被写入日志

    • 写入名称

        package io.renren.common.annotation;
      
        import java.lang.annotation.*;
      
        /**
         * 自定义注解
         *   属性描述
         *   用于日志记录
         */
        @Target(ElementType.FIELD)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface AttributeDesc {
      
        	//属性名称
        	String value() default "";
      
        //    //属性是否属于枚举类型,""不是,如果不为"",比如是"card-type",那说明,此是枚举类,并且对应的枚举的关键字是"card-type",只要从数据字典内,类型是"card-type"的一批数据里去匹配就好
        //    String enumType() default "";
        }
      

3.1.3、@ForeignKey 外键注解

  • 作用:
    • 标记某个属性为外键

    • 记录时把对应的外键值改为指定表中指定的字段值

        package io.renren.common.annotation;
      
        import org.springframework.core.annotation.AliasFor;
      
        import java.lang.annotation.*;
      
        /**
         * 外键标志
         * 自定义注解
         * 作用:日志记录时,把外键ID替换为指定的关联表中的字段名称
         * 属性:
         *  daoBeanName: 指定外键关联的dao的bean名称
         *  attr:  指定要替换表的字段
         */
        @Target(ElementType.FIELD)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface ForeignKey {
        	/**
        	 * 同daoBeanName
        	 */
        	@AliasFor("daoBeanName")
        	String value() default "";
        	/**
        	 *  dao的bean名称
        	 */
        	@AliasFor("value")
        	String daoBeanName() default "";
      
        	/**
        	 * 表中字段对应实体类属性名称
        	 */
        	String attr() default "name";
        }
      

3.1.4、@CommonState 通用状态

  • 作用
    • 标记某个属性为通用属性
    • 记录日志时,把状态值转换为代表的含义

3.1.5、State 状态枚举

	public enum State {

			/**
			 * 启用
			 */
			ENABLE("state",1, "启用"),

			/**
			 * 禁用
			 */
			DISABLE("state", 2, "禁用"),

			/**
			 * 允许
			 */
			PERMIT("permit", 1, "允许"),

			/**
			 * 不允许
			 */
			NOT_PERMIT("permit", 2, "不允许"),

			/**
			 * 审核中
			 */
			AUDITING("audit", 1, "审核中"),

			/**
			 * 同意
			 */
			AGREE("audit", 2, "同意"),

			/**
			 * 拒绝
			 */
			REFUSE("audit", 3, "拒绝"),
			;
			private String type;
			private int code;
			private String name;

			State(String type, int code, String name) {
				this.type = type;
				this.code = code;
				this.name = name;
			}

			public String getType() { return type; }
			public void setType(String type) { this.type = type; }
			public int getCode() { return this.code;}
			public void setCode(int code) { this.code = code;}
			public String getName() { return this.name;}
			public void setName(String name) { this.name = name;}
			public static String getNameByTypeAndCode(String type, int code) {
				State[] states = values();
				for (State state: states) {
					if (state.type.equals(type) && state.code == code) {
						return state.name;
					}
				}
				return null;
			}
		}

3.1.6、@SysAdminOperationLog 日志注解

  • 标记用于需要记录日志的方法

  • 知名是那种操作(1添加操作、2修改操作、3删除操作)

     package io.renren.common.annotation;
    
     import org.springframework.core.annotation.AliasFor;
    
     import java.lang.annotation.*;
    
     /**
      * 系统日志注解
      *
      * @author Mark [email protected]
      */
     @Target(ElementType.METHOD)
     @Retention(RetentionPolicy.RUNTIME)
     @Documented
     public @interface SysAdminOperationLog {
     	/**
     	 * 操作类型:1添加,2修改,3删除,4其他
     	 * @return
     	 */
     	@AliasFor("value")
     	int type() default 1;
    
     	@AliasFor("type")
     	int value() default 1;
     }
    

3.2、AOP切面处理

具体的日志切面处理类SysAdminOperationLogAspect.java,步骤如下:

  • 获取切入点方法

  • 获取方法所在的Controller

  • 获取Controller对应的实体类

  • 获取Controller对应的daoBeanName

    • 通过daoBeanName去IOC容器获取dao对象
  • 判断操作类型

    • 添加
      • 执行原方法
      • 获取方法参数
      • 获取ID
      • daoduix.selectById(id)获取实体类对象
    • 删除
      • 获取方法参数
      • 获取ID
      • daoduix.selectById(id)获取删除前实体类对象
      • 执行原方法
    • 修改
      • 获取方法参数
      • 获取ID
      • daoduix.selectById(id)获取修改前实体类对象
      • 执行原方法
      • daoduix.selectById(id)获取修改后实体类对象
  • 封装内容方法

    • getContent(Field[] fields, Object oldObj, Object newObj)
    • 判断newObj对象是否为空
      • 为空(添加或者删除)
      • 遍历属性数组
      • 获取@AttributeDesc
        • 不为空,获取value
        • field.get(oldObj)获取值 val
      • 获取@ForeignKey
        • 不为空,获取daoBeanName,获取attr
        • 通过IOC容器获取dao对象,执行dao.selectByid(val),获取attr对应的对象的值
      • 字符串拼接 value:val
    • 不为空,修改操作
      • 其他操作同上
      • 比对oldObj与newObj 对应属性值
      • 拼接字符串value:修改前->oldVal==>修改后->newVal
  • 记录日志

    • saveLog(String content)
    • 获取HttpServletRequest
    • 通过IPUtils获取IP
    • 通过SecurityUtils获取登录账号和名称
    • 通过logService写入数据库
  • 代码如下(待优化,目前只是实现功能需要进一步优化):

      package io.renren.common.aspect;
    
      import com.baomidou.mybatisplus.annotation.TableId;
      import com.baomidou.mybatisplus.annotation.TableName;
      import com.baomidou.mybatisplus.core.mapper.BaseMapper;
      import io.renren.common.annotation.*;
      import io.renren.common.config.ApplicationContextBean;
      import io.renren.common.utils.Constant;
      import io.renren.common.utils.HttpContextUtils;
      import io.renren.common.utils.IPUtils;
      import io.renren.modules.sys.entity.SysAdminOperationLogEntity;
      import io.renren.modules.sys.service.SysAdminOperationLogService;
      import org.apache.commons.lang.StringUtils;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.annotation.Around;
      import org.aspectj.lang.annotation.Aspect;
      import org.aspectj.lang.annotation.Pointcut;
      import org.aspectj.lang.reflect.MethodSignature;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Component;
    
      import javax.servlet.http.HttpServletRequest;
      import java.lang.reflect.Field;
      import java.lang.reflect.InvocationTargetException;
      import java.lang.reflect.Method;
      import java.util.Date;
    
      /**
       * 系统日志,切面处理类
       *
       * @author Mark [email protected]
       */
      @Aspect
      @Component
      public class SysAdminOperationLogAspect {
      	@Autowired
      	private SysAdminOperationLogService logService;
    
      	@Pointcut("@annotation(io.renren.common.annotation.SysAdminOperationLog)")
      	public void logPointCut() {
    
      	}
    
      	@Around("logPointCut()")
      	public Object around(ProceedingJoinPoint point) throws Throwable {
      		// 获取当前controller 全类名
      		String controllerName = point.getTarget().getClass().getName();
      		// 获取对应的entity全类名
      		String entityName = controllerName.replace("controller", "entity").replace("Controller", "Entity");
      		Class<?> entityClass = Class.forName(entityName);
      		// 获取表名
      		// String tableName = entityClass.getAnnotation(TableName.class).value();
      		String className = entityClass.getDeclaredAnnotation(ClassDesc.class).value();
      		Field[] entityFields = entityClass.getDeclaredFields();
      		// 获取实体类中的主键属性对象
      		Field idField = null;
      		for (Field field : entityFields) {
      			if (field.getDeclaredAnnotation(TableId.class) != null) {
      				idField = field;
      				break;
      			}
      		}
      		idField.setAccessible(true);
    
      		// String dao = controllerName.replace("controller", "dao").replace("Controller", "Dao");
    
      		// 获取对应的dao的bean对象
      		String upperDaoName = controllerName.substring(controllerName.lastIndexOf(".") + 1).replace("Controller", "Dao");
    
      		String daoName = StringUtils.uncapitalize(upperDaoName);
      		BaseMapper mapper = (BaseMapper) ApplicationContextBean.getBean(daoName);
    
    
      		MethodSignature signature = (MethodSignature) point.getSignature();
      		Method method = signature.getMethod();
      		SysAdminOperationLog adminLog = method.getAnnotation(SysAdminOperationLog.class);
      		int type = 1;
      		Object result = null;
    
      		if (adminLog != null) {
      			//注解上的类型值
      			type = adminLog.value();
      		}
    
      		int id = 0;
      		Object argObj;
      		String content = null;
      		String subStr;
      		Object oldObj = null;
      		Object newObj = null;
      		if (type == 1) {
      			// 保存操作
      			result = point.proceed();
      			argObj = point.getArgs()[0];
      			id = (int) idField.get(argObj);
      			oldObj = mapper.selectById(id);
      			content = "添加操作;添加";
    
    
      		} else if (type == 2) {
      			// 修改操作
      			argObj = point.getArgs()[0];
      			id = (int) idField.get(argObj);
      			oldObj = mapper.selectById(id);
      			result = point.proceed();
      			newObj = point.getArgs()[0];
      			content = "修改操作;修改";
      		} else if (type == 3) {
      			// 删除操作
      			argObj = point.getArgs()[0];
      			id = (int) argObj;
      			oldObj = mapper.selectById(id);
      			result = point.proceed();
      			content = "删除操作;删除";
      		} else {
      			// 其他操作
      		}
      		//  content += "操作表:" + tableName + ";记录ID:" + id + ";" + subStr;
      		// 获取操作内容
      		subStr = generateContent(entityFields, oldObj, newObj);
      		content += className+ ";记录ID:" + id + ";" + subStr;
      		// 记录日志
      		saveSysLog(content);
      		return result;
      	}
    
    
      	/**
      	 * 获取操作内容
      	 * @param fields    实体类属性对象数组
      	 * @param oldObj    旧对象
      	 * @param newObj    新对象
      	 * @return          操作内容字符串
      	 */
      	private String generateContent(Field[] fields, Object oldObj, Object newObj) {
      		/**
      		 *  有2种方案
      		 *  方案1、
      		 *      1.1、直接遍历属性数组
      		 *      1.2、判断newObj是否为空
      		 *      1.3、为空添加或者删除
      		 *          1.3.1、在获取注解及后续操作
      		 *      1.4、不为空修改操作
      		 *          1.4.1、获取注解及后续操作
      		 *   方案2、
      		 *      2.1、先判断newObj是否为空
      		 *      2.2、为空添加或者删除操作
      		 *          2.2.1、后续操作
      		 *      2.3、不为空修改操作
      		 *          2.3.1、后续操作
      		 *   3、比较
      		 *      方案1:
      		 *          代码简洁、冗余少
      		 *          逻辑分支多
      		 *      方案2:
      		 *          代码冗余多
      		 *          逻辑清晰、少分支判断
      		 *   4、暂时选用方案2
      		 */
      		StringBuffer content = new StringBuffer();
      		String fieldStr = "";
      		if (newObj == null) {
      			for (Field field : fields) {
      				AttributeDesc desc = field.getDeclaredAnnotation(AttributeDesc.class);
      				if (desc != null) {
      					try {
      						field.setAccessible(true);
      						Object o = field.get(oldObj);
      						if (o != null) {
      							fieldStr = o.toString();
      							ForeignKey foreignKey = field.getDeclaredAnnotation(ForeignKey.class);
      							if (foreignKey != null) {
      								// 获取外键对应的表记录中的指定字段值
      								String daoBeanName = foreignKey.value();
      								if (StringUtils.isBlank(daoBeanName)) {
      									daoBeanName = foreignKey.daoBeanName();
      								}
      								String column = foreignKey.attr();
      								BaseMapper mapper = (BaseMapper) ApplicationContextBean.getBean(daoBeanName);
      								Object foreignObj = mapper.selectById((Integer) o);
      								Method method = foreignObj.getClass().getMethod("get" + StringUtils.capitalize(column));
      								Object foreignVal = method.invoke(foreignObj);
      								fieldStr = foreignVal.toString();
      							}
    
      							CommonState commonState = field.getDeclaredAnnotation(CommonState.class);
      							if (commonState != null) {
      								String type = commonState.value();
      								if (StringUtils.isBlank(type)) {
      									type = oldObj.getClass().getSimpleName();
      								}
      								int code = (int) o;
      								fieldStr = Constant.State.getNameByTypeAndCode(type, code);
      							}
    
      							content.append(desc.value()).append(":").append(fieldStr).append(",");
      						}
      					} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
      						e.printStackTrace();
      					}
    
      				}
      			}
    
      		} else {
      			String oldStr = "";
      			String newStr = "";
      			for (Field field : fields) {
      				AttributeDesc desc = field.getDeclaredAnnotation(AttributeDesc.class);
      				if (desc != null) {
      					try {
      						field.setAccessible(true);
      						Object newVal = field.get(newObj);
      						if (newVal != null) {
      							newStr = newVal.toString();
      							Object oldVal = field.get(oldObj);
      							if (oldVal != null) {
      								oldStr = oldVal.toString();
      							}
      							if (!newStr.equals(oldStr)) {
      								ForeignKey foreignKey = field.getDeclaredAnnotation(ForeignKey.class);
      								if (foreignKey != null) {
      									// 获取外键对应的表记录中的指定字段值
      									String daoBeanName = foreignKey.value();
      									if (StringUtils.isBlank(daoBeanName)) {
      										daoBeanName = foreignKey.daoBeanName();
      									}
      									String column = foreignKey.attr();
      									BaseMapper mapper = (BaseMapper) ApplicationContextBean.getBean(daoBeanName);
      									Object foreignOldObj = mapper.selectById((Integer) oldVal);
      									Object foreignNewObj = mapper.selectById((Integer) newVal);
      									Method method = foreignOldObj.getClass().getMethod("get" + StringUtils.capitalize(column));
      									Object foreignOldVal = method.invoke(foreignOldObj);
      									Object foreignNewVal = method.invoke(foreignNewObj);
      									oldStr = foreignOldVal.toString();
      									newStr = foreignNewVal.toString();
      								}
    
      								CommonState commonState = field.getDeclaredAnnotation(CommonState.class);
      								if (commonState != null) {
      									String type = commonState.value();
      									if (StringUtils.isBlank(type)) {
      										type = oldObj.getClass().getSimpleName();
      									}
      									int oldCode = (int) oldVal;
      									int newCode = (int) newVal;
      									oldStr = Constant.State.getNameByTypeAndCode(type, oldCode);
      									newStr = Constant.State.getNameByTypeAndCode(type, newCode);
      								}
      								content.append(desc.value())
      										.append(":修改前->")
      										.append(oldStr)
      										.append("==>修改后->")
      										.append(newStr)
      										.append(",");
      							}
      						}
      					} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
      						e.printStackTrace();
      					}
    
      				}
      			}
      		}
      		String s = content.toString();
      		return s.substring(0, s.length() - 1);
      	}
    
      	/**
      	 * 日志写入数据库
      	 * @param content   操作内容
      	 */
      	private void saveSysLog(String content) {
      		SysAdminOperationLogEntity logEntity = new SysAdminOperationLogEntity();
      		//获取request
      		HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
      		//设置IP地址
      		logEntity.setOperatingIp(IPUtils.getIpAddr(request));
    
      		//登录账号
      //        String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
      //        logEntity.setUser(username);
      		logEntity.setUser("admin");
    
    
      		// 登录名称
      //        String adminname = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getAdminname();
      //        logEntity.setName(adminname);
      		logEntity.setName("adminname");
      		// 时间
      		logEntity.setOperatingTime(new Date());
    
      		logEntity.setContent(content);
      		//保存系统日志
      		logService.save(logEntity);
      	}
      }
    

4、其他相关代码

  • entity示例代码:

      package io.renren.modules.sys.entity;
    
      import com.baomidou.mybatisplus.annotation.TableId;
      import com.baomidou.mybatisplus.annotation.TableName;
      import io.renren.common.annotation.AttributeDesc;
      import io.renren.common.annotation.ClassDesc;
      import io.renren.common.annotation.CommonState;
      import io.renren.common.annotation.ForeignKey;
      import lombok.Data;
    
      import javax.validation.constraints.NotBlank;
      import javax.validation.constraints.NotNull;
      import java.io.Serializable;
    
      /**
       * 
       * 
       * @author Mark
       * @email [email protected]
       * @date 2020-10-12 16:39:41
       */
      @Data
      @TableName("sys_retail")
      @ClassDesc("门店")
      public class SysRetailEntity implements Serializable {
      	private static final long serialVersionUID = 1L;
    
      	/**
      	 * 
      	 */
      	@TableId
      	private Integer id;
    
      	/**
      	 * 门店照片
      	 */
      	private String img;
    
      	/**
      	 * 门店名称
      	 */
      	@AttributeDesc("门店名称")
      	@NotBlank(message = "门店名称不能为空")
      	private String name;
      	/**
      	 * 门店编号
      	 */
      	private String no;
      	/**
      	 * 门店类型id
      	 */
      	@AttributeDesc("门店类型")
      	@ForeignKey("sysRetailTypeDao")
      	@NotNull(message = "门店类型id不能为空")
      	private Integer retailTypeId;
      	/**
      	 * 门店地址
      	 */
      	@NotBlank(message = "门店地址不能为空")
      	private String addresses;
      	/**
      	 * 门店店主
      	 */
      	@AttributeDesc("门店店主")
      	@ForeignKey(value = "MemberDao", attr = "username")
      	@NotNull(message = "门店店主不能为空")
      	private Integer userId;
      	/**
      	 * 门店状态:1正常,2禁用
      	 */
      	@AttributeDesc("门店状态")
      	@CommonState("retailState")
      	private Integer state;
      	/**
      	 * 店铺地图坐标
      	 */
      	private String coordinates;
    
      	/**
      	 * 是否允许向平台进货:1允许,2不允许
      	 */
      	@AttributeDesc("是否允许向平台进货")
      	@CommonState("permit")
      	private Integer permit;
      }
    
  • controller示例代码

      package io.renren.modules.sys.controller;
    
      import io.renren.common.annotation.SysAdminOperationLog;
      import io.renren.common.exception.RRException;
      import io.renren.common.utils.PageUtils;
      import io.renren.common.utils.R;
      import io.renren.common.validator.ValidatorUtils;
      import io.renren.modules.oss.cloud.OSSFactory;
      import io.renren.modules.oss.entity.SysOssEntity;
      import io.renren.modules.oss.service.SysOssService;
      import io.renren.modules.sys.entity.SysDistrictEntity;
      import io.renren.modules.sys.entity.SysProductInventoriesEntityVo;
      import io.renren.modules.sys.entity.SysRetailEntity;
      import io.renren.modules.sys.entity.SysRetailEntityVo;
      import io.renren.modules.sys.service.SysDistrictService;
      import io.renren.modules.sys.service.SysRetailService;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import org.apache.shiro.authz.annotation.RequiresPermissions;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.*;
      import org.springframework.web.multipart.MultipartFile;
    
      import java.io.IOException;
      import java.util.Date;
      import java.util.List;
      import java.util.Map;
    
    
      /**
       * @author Mark
       * @email [email protected]
       * @date 2020-10-12 16:39:41
       */
      @RestController
      @RequestMapping("sys/sysretail")
      @Api("门面")
      public class SysRetailController {
      	@Autowired
      	private SysRetailService sysRetailService;
    
      	@Autowired
      	private SysOssService sysOssService;
    
      	/**
      	 * 列表
      	 */
      	@GetMapping("/list")
      	@RequiresPermissions("sys:sysretail:list")
      	@ApiOperation("列表")
      	public R list(@RequestParam Map<String, Object> params) {
      		PageUtils page = sysRetailService.queryPage(params);
    
      		return R.ok().put("page", page);
      	}
    
    
      	/**
      	 * 信息
      	 */
      	@GetMapping("/info/{id}")
      	@RequiresPermissions("sys:sysretail:info")
      	@ApiOperation("根据门面ID查询")
      	public R info(@PathVariable("id") Integer id) {
      		SysRetailEntityVo sysRetail = sysRetailService.getById(id);
    
      		return R.ok().put("sysRetail", sysRetail);
      	}
    
    
    
      	/**
      	 * 保存
      	 */
      	@PostMapping("/save")
      	@RequiresPermissions("sys:sysretail:save")
      	@SysAdminOperationLog
      	@ApiOperation("添加")
      	public R save(@RequestBody SysRetailEntity sysRetail) throws IOException {
      		ValidatorUtils.validateEntity(sysRetail);
      		sysRetailService.saveRetail(sysRetail);
      		return R.ok();
      	}
    
      	/**
      	 * 修改
      	 */
      	@PostMapping("/update")
      	@RequiresPermissions("sys:sysretail:update")
      	@ApiOperation("修改")
      	@SysAdminOperationLog(2)
      	public R update(@RequestBody SysRetailEntity sysRetail) throws IOException {
      		sysRetailService.updateById(sysRetail);
      		return R.ok();
      	}
    
      }
    
  • 效果图示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ycb50r4-1604621643547)(./images/log.png)]

后记

  欢迎交流,本人QQ:806797785

项目源代码地址:https://gitee.com/gaogzhen

猜你喜欢

转载自blog.csdn.net/gaogzhen/article/details/109524312
今日推荐