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 sunlightcs@gmail.com */ @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 sunlightcs@gmail.com */ @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 sunlightcs@gmail.com * @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 sunlightcs@gmail.com * @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(); } }
-
效果图示:
后记 :
欢迎交流,本人QQ:806797785
项目源代码地址:https://gitee.com/gaogzhen