1. 业务场景概述
对于一个软件系统来说,日志的记录很有必要。当系统运行出现问题时,开发人员可以通过记录的日志信息去快速定位问题。其中,业务操作日志主要记录和跟踪用户对业务数据执行的操作,操作日志对可读性 的要求较高,所以需要重点关注操作日志的记录格式。
2. 实现思路分析
2.1 嵌入业务逻辑代码记录
优点
:可以自定义操作日志的格式与信息,可以详细记录操作细节;缺点
:操作日志与业务逻辑代码耦合性太高,重复代码太多【如果后续需要修改,简直就是灾难性的存在】
2.2 采用AOP实现
为了代码后续的可拓展性和可维护性,可以采用AOP 思想,利用注解 和切面类将记录操作日志的相关代码独立出来。
优点
:业务逻辑与操作日志解耦;缺点
:日志粒度比较粗 ,而且日志内容是静态,不能将具体的操作细节体现出来,只能实现如:xxx删除了用户,xxx新增了用户等。
2.3 采用AOP + SpEL实现
为了使日志格式更加灵活,可以利用 AOP + SpEL 可以实现更细粒度的日志记录,体现更多的细节,最终实现如:xxx删除了用户ID = xxx的记录。
3. 技术介绍
3.1 AOP
3.1.1 什么是AOP
AOP (Aspect Oriented Programming,面向切面编程),是一种编程思想。主要的作用:不改变原有代码的情况下添加新功能。这样有利于减少系统的重复代码,降低模块之间的耦合度,有利于未来的拓展和维护。
在编码过程中,如果遇到代码重复的问题,一般来说有两种方式:
纵向抽取
:将重复代码抽象成公共类/公共方法;横向抽取
:利用AOP 思想,横向切割代码,将重复代码抽取出来形成一个独立模块。这种方式适用于重复代码耦合在业务逻辑代码中,有逻辑顺序的情况。
3.1.2 AOP中的核心概念
Advice(通知)
:增强 的那一部分逻辑代码,用于定义共性功能 的方法 ;通知的类型 :- 前置 通知(
@Before
):增强部分代码在原代码之前; - 后置 通知(
@After
):增强部分代码在原代码之后; - 环绕 通知(
@Around
):增强部分代码在原代码前,也有在原代码后; - 异常 通知(
@AfterThrowing
):原代码抛出异常后才会执行; - 返回 通知(
@AfterReturning
):原代码成功执行后执行。
- 前置 通知(
上述所有的通知类型写到一个切面中,执行顺序如下图所示:
Joinpoint(连接点)
:可能执行通知 的方法;Aspect(切面)
:用于建立通知与切入点之间的绑定关系;PointCut(切点)
:执行通知的方法。
3.2 SpEL
3.2.1 什么是SpEL
Spring Expression Language
,由Spring提供的表达式语言,用于实现动态 ,可配置 的行为,支持常见的表达式操作,如:算术运算,逻辑运算,条件运算等。可以通过xml或者注解的配置方式进行使用。
Java
// SpEL支持条件运算
@Value("#{2 > 1 ? 'a' : 'b'}") // "a"
private String ternary;
3.2.2 解析表达式
Java
CompanyDTO companyDTO = new CompanyDTO();
companyDTO.setCompanyName("elTest");
// 解析表达式
ExpressionParser expressionParser = new SpelExpressionParser();
// 计算表达式字符串的值
Expression expression = expressionParser.parseExpression("companyName");
// 根据上下文自动对类型进行转换
StandardEvaluationContext context = new StandardEvaluationContext(companyDTO);
String result = (String) expression.getValue(context);
log.info("结果:{}",result);
4. 核心代码实现
4.1 注解@OperationLogAnnotation定义
Java
@Target({ ElementType.PARAMETER, ElementType.METHOD }) // 注解放置的目标位置,PARAMETER: 可用在参数上 METHOD:可用在方法级别上
@Retention(RetentionPolicy.RUNTIME) // 指明修饰的注解的生存周期 RUNTIME:运行级别保留
@Documented
public @interface OperationLogAnnotation {
/**
* 日志内容,支持SpEL表达式
* @return
*/
String content() default "";
}
4.2 切面类OperationLogAspect
Java
@Component
@Aspect
@Slf4j
public class OperationLogAspect {
private static final ExpressionEvaluator<String> EVALUATOR = new ExpressionEvaluator<>();
@Autowired
private LogRepository operationLogRepository;
@Autowired
private SysUserRepository sysUserRepository;
@Autowired
private JwtTokenUtil jwtTokenUtil;
/**
* 被执行方法上添加OperationLogAnnotation注解的才会执行这个方法
* @param joinPoint
* @param jsonResult
*/
@AfterReturning(pointcut = "@annotation(com.ocrweb.annotation.OperationLogAnnotation)", returning = "jsonResult")
public void doAfterReturning (JoinPoint joinPoint, Object jsonResult) {
handleOperationLog(joinPoint, jsonResult);
}
/**
* 操作日志处理逻辑,包括:格式处理,持久化
* @param joinPoint
* @param jsonResult 返回结果
*/
protected void handleOperationLog(final JoinPoint joinPoint, Object jsonResult) {
OperationLogAnnotation operationLogAnnotation = ((MethodSignature) joinPoint.getSignature()).getMethod().
getAnnotation(OperationLogAnnotation.class);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取方法名
String methodName = joinPoint.getSignature().getName().toLowerCase();
// 根据方法名判定方法类型
String methodType = FunctionTypeEnum.getMessageByCode(methodName);
// 获取类名
String className = joinPoint.getTarget().getClass().getName().toLowerCase();
// 根据类名判断所属模块
String moduleType = ModuleTypeEnum.getMessageByCode(className);
// 获取方法签名
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
// 参数名称列表
String[] parameterNames = methodSignature.getParameterNames();
// 获取accessToken参数下标
int accessTokenIndex = ArrayUtils.indexOf(parameterNames, "accessToken");
log.info("accessTokenIndex:{}",accessTokenIndex);
Object[] args = joinPoint.getArgs();
if (accessTokenIndex < 0){
return;
}
// 根据参数accessToken获取操作人相关信息
String accessToken = String.valueOf(args[accessTokenIndex]);
log.info("accessToken:{}",accessToken);
Integer userId = jwtTokenUtil.getUserIdFromToken(accessToken);
SysUser sysUser = sysUserRepository.findByUserId(userId);
// content的默认值为返回结果
String content = new String();
// 获取operationLogAnnotation注解的的content内容
if (StringUtils.isNotBlank(operationLogAnnotation.content())) {
// 解析EL表达式
content = this.evalExpression(joinPoint, operationLogAnnotation.content() );
log.info("expression:{}", content);
if (StringUtils.isNotBlank(methodType) && !methodType.equals(FunctionTypeEnum.SAVE.message())){
content = methodType + " " + moduleType + content;
} else {
content = moduleType + " " + content;
}
}
JsonResult result = (JsonResult) jsonResult;
if (result.getCode() == 500){
content = result.toString();
}
// 操作日志保存到数据库内
OperationLog operationLog = new OperationLog();
if (StringUtils.isNotBlank(moduleType) && StringUtils.isNotBlank(methodType)){
operationLog.setModuleType(moduleType);
operationLog.setOperationType(methodType);
operationLog.setContent(content);
operationLog.setClientIp(IpAddrUtil.getLocalIp(request));
operationLog.setServerIp(request.getRemoteAddr());
operationLog.setLoginName(sysUser.getLoginName());
operationLog.setUserName(sysUser.getUserName());
operationLog.setUserId(sysUser.getUserId());
operationLogRepository.save(operationLog);
}
}
/**
* 解析EL表达式
* @param point 切入点
* @param expression 需要解析的EL表达式
* @return 解析出的值
*/
private String evalExpression(JoinPoint point, String expression) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = EVALUATOR.createEvaluationContext(target, target.getClass(), method, args);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return EVALUATOR.condition(expression, elementKey, context, String.class);
}
}
4.3 解析EL表达式相关类
ExpressionEvaluator
类
Java
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
Method targetMethod = getTargetMethod(targetClass, method);
ExpressionRootObject root = new ExpressionRootObject(object, args);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
if (targetMethod == null) {
targetMethod = method;
}
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
}
ExpressionRootObject
类
Java
public class ExpressionRootObject {
private final Object object;
private final Object[] args;
public ExpressionRootObject(Object object, Object[] args) {
this.object = object;
this.args = args;
}
public Object getObject() {
return object;
}
public Object[] getArgs() {
return args;
}
}
4.4 Controller层调用
Java
@OperationLogAnnotation(content = " #companyDTO.companyId == null ? '添加 名称:' + #companyDTO.companyName: '编辑 ID:' + #companyDTO.companyId")
@PostMapping("/saveCompany")
public JsonResult saveCompany(@RequestBody CompanyDTO companyDTO, @RequestHeader String accessToken, HttpServletRequest request){
// ...
}