背景
业务组有一些给开发用的后门接口,为了做到调用溯源,业务组最近需要记录所有接口的访问记录,暂时只需要记录接口的响应结果,如果调用失败,则记录异常信息。由于后门接口较多以及只是业务组内部轻度使用,因此使用了切面的方式实现。
方案
@EnableAspectJAutoProxy
@Aspect
@Component
@Slf4j
public class ResponseLogAspect {
@Resource
private CommonConstants commonConstants;
@Pointcut("@annotation(*.log.ResponseLog)")
public void logPointcut() {
}
/**
* 执行成功打印
*/
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void log(JoinPoint joinPoint, Object result) {
try {
// 降级开关
if (!commonConstants.getBoolean("interface.response.log.switch", true)) {
return;
}
ResponseLog annotation = findAnnotation(joinPoint, ResponseLog.class);
String metric = metric(annotation.value(), joinPoint);
log.info("interface success: {}, result: {}", metric, result);
} catch (Exception e) {
log.error("log error", e);
QMonitor.recordOne("interface_response_log_fail");
}
}
/**
* 执行失败打印
*/
@AfterThrowing(pointcut = "logPointcut()", throwing = "error")
public void logError(JoinPoint joinPoint, Throwable error) {
try {
// 降级开关
if (!commonConstants.getBoolean("interface.response.log.switch", true)) {
return;
}
ResponseLog annotation = findAnnotation(joinPoint, ResponseLog.class);
String metric = metric(annotation.value(), joinPoint);
log.error("interface fail: {}, error: {}", metric, error.getMessage());
} catch (Exception e) {
log.error("log error", e);
QMonitor.recordOne("interface_response_log_fail");
}
}
/**
* 监控指标
* @param specificName 具体指标名
* @param point 切点
* @return 指标名称
*/
private String metric(String specificName, JoinPoint point) {
if (StringUtils.isBlank(specificName)) {
String clz = point.getTarget().getClass().getSimpleName();
String mtd = point.getSignature().getName();
return clz + "_" + mtd;
} else {
return specificName;
}
}
/**
* 注解查询
* @param point 切点
* @param annotationType 注解类型
* @return 注解信息
*/
private <A extends Annotation> A findAnnotation(JoinPoint point, Class<A> annotationType) {
MethodSignature signature = (MethodSignature) point.getSignature();
return AnnotationUtils.findAnnotation(signature.getMethod(), annotationType);
}
}
接下来只需要在后门接口上增加对应的注解即可:
@RequestMapping(value = "save", method = RequestMethod.POST)
@ResponseBody
@ResponseLog("/voucher/save")
public APIResponse<Boolean> save(HttpServletRequest request, @RequestBody VoucherCommit voucherCommit) {
// 代金券保存接口
}
加餐
@Target({ElementType.METHOD})
:指定该注解可以应用于方法。如果不加这个注解,则表示默认该注解可以应用到类与方法上,但是加上后就表示这个注解只能作用于方法,否则会报错。- springboot项目由于存在spring-boot-autoconfigure依赖,会默认开启aop代理,所以注解@EnableAspectJAutoProxy可以不用加,但是由于可以在配置文件中修改默认开启的逻辑,所以建议加上避免失效。
- @Pointcut注解中的参数:@within和@annotation。
@annotation
注解用于匹配那些具有指定注解的方法,@within
注解用于匹配那些具有指定注解的类中的所有方法,即使这些方法本身没有显式地标注注解。
// 切点:匹配带有@OnlyIntranetAccess注解的类
@Pointcut("@within(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)")
public void onlyIntranetAccessOnClass() {}
// 切点:匹配带有@OnlyIntranetAccess注解的方法
@Pointcut("@annotation(org.openmmlab.platform.common.annotation.OnlyIntranetAccess)")
public void onlyIntranetAccessOnMethed() {}