一、@interface 关键字
我们想定义一个自己的注解 需要使用 @interface 关键字来定义。
如定义一个叫 MyAnnotation 的注解:
public @interface MyAnnotation {}
二、元注解
光加上 @interface 关键字 还不够,我们还需要了解5大元注解
@Retention
@Target
@Documented
@Inherited(JDK8引入)
@Retention:定义注解的保留策略
@Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS) //默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,
@Retention(RetentionPolicy.RUNTIME) //注解会在class字节码文件中存在,在运行时可以通过反射获取到
-------------------------------------------------------------------------------------------
@Target:指定被修饰的Annotation可以放置的位置(被修饰的目标)
@Target(ElementType.TYPE) //接口、类
@Target(ElementType.FIELD) //属性
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR) //构造函数
@Target(ElementType.LOCAL_VARIABLE) //局部变量
@Target(ElementType.ANNOTATION_TYPE) //注解
@Target(ElementType.PACKAGE) //包
注:可以指定多个位置,例如:
@Target({ElementType.METHOD, ElementType.TYPE}),也就是此注解可以在方法和类上面使用
-------------------------------------------------------------------------------------------
@Inherited:指定被修饰的Annotation将具有继承性
-------------------------------------------------------------------------------------------
@Documented:指定被修饰的该Annotation可以被javadoc工具提取成文档.
三、什么是AOP
Spring AOP(面向切面编程)是Spring框架的一个重要模块,用于通过在运行时动态地织入到现有的类中,实现横切关注点的功能。横切关注点是那些会影响多个模块的功能,如日志、事务管理、安全性等。
Spring AOP提供了一种方便的方式来实现横切关注点的功能,而无需修改目标对象的源代码。他通过代理机制,在目标对象的方法执行前、执行后或发生异常时插入额外的逻辑,从而实现注入性能监控、事务管理、日志记录等功能。这样可以避免在每个需要这些功能的方法中都编写重复的代码,提高了代码的重用性和可维护性。
3.1 核心概念
切点(Pointcut):定义了在那些连接点(Join Point)上应用通知(Advice)。切点是一个表达式,用于匹配连接点。
通知(Advice):定义了在切点处执行的逻辑,比如在方法执行前后执行的代码。
连接点(Join Point):在应用程序执行过程中的某个特定点,必须方法执行时、异常抛出等,连接点是代码的具体执行位置,它代表了横切关注点可以插入的地方。
切面(Aspect):切面是切点和通知的组合,他定义了在哪里以及何时应用通知。
3.2 通知类型
通知有五种:
前置通知@Before:目标方法调用前被调用后置通知使用
最终通知@After:目标方法执行完之后执行
后置通知@AfterReturning:通知方法在目标方法返回后调用
异常通知@AfterThrowing:通知方法在目标方法抛出异常后调用
环绕通知@Around:通知包裹目标方法,在目标方法执行之前、执行之后添加自定义的行为
执行顺序:@Around => @Before => 接口中的逻辑代码 => @After => @AfterReturning
除 @Around 外,每个方法里都可以加或者不加参数 JoinPoint
@Around 参数必须为 ProceedingJoinPoint
光说肯定不如来个案例来的清晰,下面是具体的例子来介绍如何基于AOP和自定义注解实现日志记录
四、实战一
4.1 引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
4.2 定义注解
aop/annotation/ZkpLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ZkpLog {
}
4.3 定义切面
aop/ZkpLog
@Component
@Aspect
@Slf4j
public class ZkpLogAspect {
//切点,表示在注解切入
@Pointcut("@annotation(com.zkp.shortlink.project.annotation.ZkpLog)")
public void zkpLogAspect() {}
//表示在方法执行前执行
@Before("zkpLogAspect()")
public void beforeZkpLog(JoinPoint joinPoint){
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String methodName = joinPoint.getSignature().getName();
System.out.println("========================================= Method " + methodName + "() begin=========================================");
// 执行时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d= new Date();
String time = sdf.format(d);
System.out.println("Time : " + time);
// 打印请求 URL
System.out.println("URL : " + request.getRequestURL());
// 打印 请求方法
System.out.println("HTTP Method: " + request.getMethod());
// 打印controller 的全路径以及执行方法
System.out.println("Class Method : " + joinPoint.getSignature().getDeclaringTypeName() + "." + methodName);
// 打印请求的 IP
System.out.println("IP : " + request.getRemoteHost());
// 打印请求入参
System.out.println("Request Args : " + JSON.toJSONString(joinPoint.getArgs()));
System.out.println("=======================================================================================================");
}
//表示在方法执行后执行
@After("zkpLogAspect()")
public void afterZkpLog(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("========================================= Method " + methodName + "() end =========================================");
}
}
4.4 使用
@ZkpLog
@PostMapping("/api/short-link/v1/create")
public Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam){
ShortLinkCreateRespDTO result = shortLinkService.createShortLink(requestParam);
return Results.success(result);
}
4.5 效果
到现在就基本实现了日志记录的功能啦!上面那个枚举是没参数的,下面再来一个有参数的来加深一下大家对自定义注解的认识
五、实战二
5.1 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLogAnnotation {
String type() default "";
}
5.2 定义切面
@Aspect
@Component
@Slf4j
public class AttendanceOperationLogAop {
private final static String TYPE_ADD = "新增";
private final static String TYPE_APPROVAL = "审批";
//切点
@Pointcut("@annotation(com.zkp.attendance.domain.aop.annotation.OperationLogAnnotation)")
public void loggableMethods() {}
/**
* @param joinPoint 切点
* @Description 记录审批操作日志(通过,拒绝,退回)
* @author zhengkangpan
* @date 2024/5/9 11:24
**/
@AfterReturning(value = "loggableMethods()")
public void approvalOperationLog(JoinPoint joinPoint) {
//获取方法签名
String type = getType(joinPoint);
if (!StrUtil.isEmpty(type) && (TYPE_APPROVAL).equals(type)) {
Object[] args = joinPoint.getArgs();
//获取第一个和第二个参数
String status = (String) args[0];
AttendanceApplicationDetailsVo attendanceApplicationDetailsVo = (AttendanceApplicationDetailsVo) args[1];
}
//具体逻辑,例如记录日志到数据库
xxxxxxx
}
@NotNull
private String getType(JoinPoint joinPoint) {
//获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//通过方法签名获取方法对象
Method method = signature.getMethod();
//通过方法对象 获取方法对象注解
OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
//获取类型(新增,编辑,提交)
return annotation.type();
}
5.3 使用
@OperationLogAnnotation(type = "审批")
public void approval(String type, AttendanceApplicationDetailsVo detailsVo) {
//方法具体逻辑
}
尽管已经实现了基本功能,但还是觉得不是很美观,比如@OperationLogAnnotation(type = "审批"),"审批"即为静态常量,而且也不支持多个type,后面就想到了可以用枚举改造
六、实战三(枚举版)
6.1 创建枚举
/**
* @Description 审计日志操作事件
* @author zhengkangpan
* @date 2024/8/6 15:13
* @return
**/
@Getter
@AllArgsConstructor
public enum AopOperationEventTypeEnum {
OUT_USER_REGISTER("外部用户注册"),
OUT_USER_LOGIN("外部用户登录"),
CHANGE_PASSWORD("修改密码"),
FORGET_PASSWORD("忘记密码"),
OUT_USER_EXAMINE("外部用户审核"),
ADD_OUT_USER("新增外部用户"),
EDIT_OUT_USER("编辑外部用户"),
DELETE_OUT_USER("删除外部用户"),
ADD_CERTIFICATE("新增证书"),
EDIT_CERTIFICATE("编辑证书"),
DELETE_CERTIFICATE("删除证书"),
CHANGE_SAFE_CONFIG("修改安全配置");
private String description;
}
6.2 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLogAnnotation {
AopOperationEventTypeEnum[] value();
}
6.3 定义切面
/**
* @Description Aop记录审计日志
* @Author zhengkangpan
* @Date 2024/8/6 15:49
*/
@Aspect
@Component
@Slf4j
//@Order(1)
public class AuditLogAop {
@Autowired
AuditLogService auditLogService;
@Autowired
AuditLogRepository auditLogRepository;
@Autowired
UserDDDService userDDDService;
@Autowired
OutUserBackRepository userBackRepository;
@Autowired
CertificateRepository certificateRepository;
/**
* @Description 切点表达式
* @author zhengkangpan
* @date 2024/8/6 15:51
**/
@Pointcut("@annotation(com.haiyisec.cms.auditLog.domain.aop.annotation.OperationLogAnnotation)")
public void loggableMethods() {
}
@AfterReturning(value = "loggableMethods()", returning = "result")
public void saveAuditLog(JoinPoint joinPoint, Object result) {
// 操作成功
handleAuditLog(joinPoint, EventResultEnum.SUCCESS.getCode(), result, null);
}
// @AfterThrowing(value = "loggableMethods()", throwing = "ex")
// public void handleFailure(JoinPoint joinPoint, Exception ex) {
// // 操作失败
// handleAuditLog(joinPoint, EventResultEnum.FAIL.getCode(), ex);
// }
/**
* @Description 获取Type
* @author zhengkangpan
* @date 2024/5/9 16:09
* @param joinPoint 切点
**/
private AopOperationEventTypeEnum[] getType(JoinPoint joinPoint) {
//获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//通过方法签名获取方法对象
Method method = signature.getMethod();
//通过方法对象 获取方法对象注解
OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
//获取类型(保存,提交,审批)
return annotation.value();
}
private void handleAuditLog(JoinPoint joinPoint, String eventResult, Object result, Exception ex) {
AopOperationEventTypeEnum[] types = getType(joinPoint);
//提取方法参数中的信息,需要的话再从args拿出来强转即可
Object[] args = joinPoint.getArgs();
//操作时间
Date operationTime = new Date();
//操作者
String operatorName = "";
//操作对象
String operationObject = null;
//操作内容
String operationContent = null;
//操作事件
String description = null;
//操作结果
String operationResult = eventResult;
for (AopOperationEventTypeEnum type : types) {
switch (type) {
case OUT_USER_LOGIN:
String errorMsg = ((CommResponseVo<?>) result).getErrorMsg();
CommRequestVo<Map<String, Object>> loginRequest = (CommRequestVo<Map<String, Object>>) args[0];
String identifier = (String) loginRequest.getData().get("identifier");
String address = (String) loginRequest.getData().get("address");
//构造日志实体
operatorName = identifier;
operationObject = identifier;
description = AopOperationEventTypeEnum.OUT_USER_LOGIN.getDescription();
if (StrUtil.isEmpty(errorMsg)) {
//登录成功
operationContent = "外部用户登录,源IP地址为【" + address + "】";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
} else {
//登录失败
operationContent = "外部用户登录,源IP地址为【" + address + "】,认证失败,原因为【" + errorMsg + "】";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
}
case OUT_USER_REGISTER:
String registerErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
if (StrUtil.isEmpty(registerErrorMsg)) {
//获取方法的第二个参数
CommRequestVo<Map<String, Object>> registerRequest = (CommRequestVo<Map<String, Object>>) args[1];
String email = (String) registerRequest.getData().get("email");
List<String> phoneList = (List<String>) registerRequest.getData().get("phoneList");
String realName = (String) registerRequest.getData().get("realName");
String businessName = (String) registerRequest.getData().get("businessName");
operatorName = email + "_" + realName;
description = AopOperationEventTypeEnum.OUT_USER_REGISTER.getDescription();
operationObject = email;
operationContent = "外部用户注册信息,最终为姓名【" + realName + "】、公司名称【" + businessName + "】、邮箱号码【" + email + "】、手机号码【" + phoneList.get(1) + "】";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
}else{
//不记录失败日志
break;
}
case CHANGE_PASSWORD:
String changePasswordErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
if (StrUtil.isEmpty(changePasswordErrorMsg)) {
//获取方法的第一个参数
CommRequestVo<Map<String, Object>> changeRequest = (CommRequestVo<Map<String, Object>>) args[0];
if ((String) changeRequest.getData().get("userId") == null) {
break;
} else {
OutUserBackPo backPo = outUserBackRepository.getOne((String) changeRequest.getData().get("userId"));
if (backPo == null) {
throw new AppException(null, "该用户不存在");
}
operatorName = backPo.getEmail() + "_" + backPo.getRealName();
description = AopOperationEventTypeEnum.CHANGE_PASSWORD.getDescription();
operationObject = backPo.getEmail() + "_" + backPo.getRealName();
operationContent = "外部用户修改了密码";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
}
}else{
//不记录失败日志
break;
}
case FORGET_PASSWORD:
String forgetPasswordErrorMsg = ((CommResponseVo<?>) result).getErrorMsg();
if (StrUtil.isEmpty(forgetPasswordErrorMsg)) {
//获取方法的第一个参数
CommRequestVo<Map<String, Object>> forgetRequest = (CommRequestVo<Map<String, Object>>) args[0];
if ((String) forgetRequest.getData().get("userId") != null) {
break;
} else {
OutUserBackPo backPo = outUserBackRepository.findByEmail((String) forgetRequest.getData().get("email"));
if (backPo == null) {
throw new AppException(null, "该用户不存在");
}
operatorName = backPo.getRealName();
description = AopOperationEventTypeEnum.FORGET_PASSWORD.getDescription();
operationObject = backPo.getEmail();
operationContent = "通过邮箱找回密码并改密";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
}
}else{
//不记录失败日志
break;
}
case OUT_USER_EXAMINE:
//获取方法的第三个参数
OutUserExamineVo queryVo = (OutUserExamineVo) args[2];
operatorName = getOperatorName();
operationObject = queryVo.getEmail();
String examine = queryVo.getExamineStatus().equals(ExamineStatusEnum.PASS.getCode()) ? ExamineStatusEnum.PASS.getDescription() : ExamineStatusEnum.REFUSE.getDescription();
operationContent = "审核了【" + queryVo.getRealName() + "】的注册信息,审核结果为【" + examine + "】";
description = AopOperationEventTypeEnum.OUT_USER_EXAMINE.getDescription();
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case ADD_OUT_USER:
//获取方法的第一个参数
OutUserVo addOutUserVo = (OutUserVo) args[0];
List<String> addPhoneList = addOutUserVo.getPhoneList();
operatorName = getOperatorName();
operationContent = "新增外部用户【" + addOutUserVo.getRealName() + "】,最终为姓名【" + addOutUserVo.getRealName() + "】、用户类型【" + PersonTypeEnum.getMsgByCode(addOutUserVo.getPersonType()) + "】、关联公司名称【" + addOutUserVo.getAssociateBusinessName() + "】、邮箱号码【" + addOutUserVo.getEmail() + "】、手机号码【" + addPhoneList.get(0) + " " + addPhoneList.get(1) + "】";
description = AopOperationEventTypeEnum.ADD_OUT_USER.getDescription();
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case EDIT_OUT_USER:
//获取方法的第一个参数
OutUserVo editOutUserVo = (OutUserVo) args[0];
operatorName = getOperatorName();
operationObject = "外部用户【" + editOutUserVo.getOldName() + "】";
operationContent = "编辑外部用户名称【" + editOutUserVo.getOldName() + "】,最终为姓名【" + editOutUserVo.getRealName() + "】、用户类型【" + PersonTypeEnum.getMsgByCode(editOutUserVo.getPersonType()) + "】、关联公司名称【" + editOutUserVo.getAssociateBusinessName() + "】、邮箱号码【" + editOutUserVo.getEmail() + "】、手机号码【" + editOutUserVo.getPhoneList().get(0) + " " + editOutUserVo.getPhoneList().get(1) + "】";
description = AopOperationEventTypeEnum.EDIT_OUT_USER.getDescription();
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case DELETE_OUT_USER:
AuditDeleteReturnVo auditDeleteReturnVo1 = (AuditDeleteReturnVo) result;
OutUserBackPo userBackPo = auditDeleteReturnVo1.getOutUserBackPo();
operatorName = getOperatorName();
description = AopOperationEventTypeEnum.DELETE_OUT_USER.getDescription();
operationObject = "外部用户【" + userBackPo.getEmail() + "_" + userBackPo.getRealName() + "】";
operationContent = "删除外部用户【" + userBackPo.getRealName() + "】";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case DELETE_CERTIFICATE:
AuditDeleteReturnVo auditDeleteReturnVo2 = (AuditDeleteReturnVo) result;
CertificatePo certificatePo = auditDeleteReturnVo2.getCertificatePo();
operatorName = getOperatorName();
description = AopOperationEventTypeEnum.DELETE_CERTIFICATE.getDescription();
operationObject = certificatePo.getCertificateNumber();
operationContent = "删除了证书";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case CHANGE_SAFE_CONFIG:
//获取方法的第一个参数
SafeConfigVo safeVo = (SafeConfigVo) args[0];
operatorName = getOperatorName();
description = AopOperationEventTypeEnum.CHANGE_SAFE_CONFIG.getDescription();
operationContent = "修改安全配置,最终安全配置10分钟内最多尝试错误密码【" + safeVo.getMaxTryPassword() + "】次,密码有效期【" + safeVo.getPasswordValidity() + "】天,禁止使用历史密码个数【" + safeVo.getForbiddenPasswordsQuantity() + "】个,同个ip一天内最多注册次数【" + safeVo.getDailyIpMaxRegister() + "】次";
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
case ADD_CERTIFICATE:
//获取参数,判断是编辑还是新增
Boolean isEdit = (Boolean) args[2];
if (!isEdit) {
//获取返回结果
CertificatePo certificatePoAdd = (CertificatePo) result;
operatorName = getOperatorName();
description = AopOperationEventTypeEnum.ADD_CERTIFICATE.getDescription();
operationObject = certificatePoAdd.getCertificateNumber();
OutUserBackPo outUserBackPo = userBackRepository.getOne(certificatePoAdd.getUserId());
operationContent = String.format("新增了证书,最终证书编号【%s】、证书类型【%s】、产品类型【%s】、姓名【%s】、公司名称【%s】、有效期至【%s】", certificatePoAdd.getCertificateNumber(), CertificateTypeEnum.getMsgByCode(certificatePoAdd.getCertificateType()), ProductType.getMsgByCode(certificatePoAdd.getProductType()), certificatePoAdd.getShowUserName(), outUserBackPo.getAssociateBusinessName(), DateUtil.format(certificatePoAdd.getGraduationTime(), "yyyy-MM-dd"));
} else {
//获取返回结果
CertificatePo certificatePoEdit = (CertificatePo) result;
operatorName = getOperatorName();
description = AopOperationEventTypeEnum.EDIT_CERTIFICATE.getDescription();
operationObject = certificatePoEdit.getCertificateNumber();
OutUserBackPo backPo = userBackRepository.getOne(certificatePoEdit.getUserId());
operationContent = String.format("编辑了证书,最终证书编号【%s】、证书类型【%s】、产品类型【%s】、姓名【%s】、公司名称【%s】、有效期至【%s】", certificatePoEdit.getCertificateNumber(), CertificateTypeEnum.getMsgByCode(certificatePoEdit.getCertificateType()), ProductType.getMsgByCode(certificatePoEdit.getProductType()), certificatePoEdit.getShowUserName(), backPo.getAssociateBusinessName(), DateUtil.format(certificatePoEdit.getGraduationTime(), "yyyy-MM-dd"));
}
saveAuditLog(operationTime, operatorName, operationObject, operationContent, description, operationResult);
break;
default:
log.error("无对应的操作事件!");
break;
}
}
}
/**
* @Description 获取登录子用户
* @author zhengkangpan
* @date 2024/9/9 14:21
* @return String
**/
private String getOperatorName() {
String operatorName = "";
if (UserUtil.getLoginUser() != null) {
operatorName = UserUtil.getLoginUser().getName();
} else {
SysUserVo sysUserVo = userDDDService.getById(UserUtil.getCurUserId());
if (sysUserVo == null) {
throw new AppException("ERR_CRM80001", "无该对象操作权限或对象不存在!");
}
operatorName = sysUserVo.getLoginName();
}
return operatorName;
}
}
6.4 使用
@OperationLogAnnotation(AopOperationEventTypeEnum.EDIT_OUT_USER)
public void editExternalUser(OutUserVo outUserVo, CheckResult cr) {
//方法逻辑
}
@Transactional(rollbackFor = Exception.class)
@OperationLogAnnotation({AopOperationEventTypeEnum.CHANGE_PASSWORD, AopOperationEventTypeEnum.FORGET_PASSWORD})
public CommResponseVo<Map<String, Object>> changePassword(CommRequestVo<Map<String, Object>> requestVo) {
//方法逻辑
}
相关文章