前言
平时在java项目开发过程中,涉及到记录操作日志的场景很多,有时候大家习惯把操作日志的生成代码写到业务代码中,这样造成了日志和业务代码的耦合性比较高、可维护性也不强,易读性更差,更多的时候是使用AOP切面编程进行操作日志记录,当前网上也能百度到一些使用样例,但是使用场景比较简单,或者示例代码不够完整,本文主要针对使用aop做操作日志记录做一个总结分享;
主要功能:
1)基于spring的aop面向切面编程,与业务代码解耦;
2)使用自定义注解用于标注要产生操作日志的方法;
3)可在自定义标签中设置要数据的入参属性值;
如下:
@SysLog("用户添加.concat(#user.role.roleName)")
4)使用spring event对日志生成和存储进一步解耦,在微服务环境,日志存储功能和生成可以位于不同的服务上;
5)参数名及参数值的解析;
Spring AOP:
方便大家理解,这里简单回顾一下AOP的关键注解;
1)切面(Aspect)
:在Spring AOP中,切面可以使用通用类或者在普通类中以@Aspect 注解(@AspectJ风格)来实现
2)连接点(Joinpoint)
:在Spring AOP中一个连接点代表一个方法的执行
3)通知(Advice)
:在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括"around"、"before”和"after"等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
4)切入点(Pointcut)
:定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。
通知类型:
前置通知(@Before)
:在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)
返回后通知(@AfterReturning)
:在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
抛出异常后通知(@AfterThrowing)
:方法抛出异常退出时执行的通知
后通知(@After)
:当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)
环绕通知(@Around)
:包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行
代码示例:
代码结构
1)定义自定义注解
package com.liuy.esports1.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author dell
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
String value() default "";
}
2)定义切面类
关键切面类:
AspectJ
使用org.aspectj.lang.JoinPoint
接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint
表示连接点对象,该类是JoinPoint
的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint
访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1) JoinPoint
java.lang.Object[] getArgs()
:获取连接点方法运行时的入参列表;
Signature getSignature()
:获取连接点的方法签名对象;
java.lang.Object getTarget()
:获取连接点所在的目标对象;
java.lang.Object getThis()
:获取代理对象本身;
2) ProceedingJoinPoint
ProceedingJoinPoint
继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:
java.lang.Object proceed() throws java.lang.Throwable
:通过反射执行目标对象的连接点处的方法;
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable
:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
package com.liuy.esports1.aspect;
import com.alibaba.fastjson.JSONObject;
import com.liuy.esports1.annotation.SysLog;
import com.liuy.esports1.event.SysLogEvent;
import com.liuy.esports1.utils.IPUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
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.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Log4j2
@Aspect
@Component
@RequiredArgsConstructor
public class SysLoggerAspect {
private final ApplicationContext applicationContext;
@Pointcut("@annotation(com.liuy.esports1.annotation.SysLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//异步保存日志
saveLog(point, time);
return result;
}
void saveLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEvent sysLog = new SysLogEvent();
SysLog syslog = method.getAnnotation(SysLog.class);
if (syslog != null) {
// 注解上的描述
sysLog.setOperation(syslog.value());
}
String value = syslog.value();
if (value.contains(".concat")) {
String propertys = value.substring(value.indexOf("(") + 1, value.lastIndexOf(")"));
String paramValue = getOneParam(propertys, joinPoint);
value = value.substring(0, value.indexOf(".")) + "[" + paramValue + "]";
sysLog.setOperation(value);
}
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的参数
Object[] args = joinPoint.getArgs();
try {
sysLog.setParams(Arrays.toString(args));
} catch (Exception e) {
}
// 获取request
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
sysLog.setTime((int) time);
// 系统当前时间
Date date = new Date();
sysLog.setGmtCreate(date);
// 保存系统日志
System.out.println("------------result--------------------------------");
System.out.println("------------result--------------------------------");
System.out.println("------------result--------------------------------");
System.out.println("=============" + sysLog);
applicationContext.publishEvent(sysLog);
}
//匹配目标参数
public String getOneParam(String propertys, ProceedingJoinPoint proceedingJoinPoint) {
String[] propertyss = propertys.split("\\.");
JSONObject paramJson = getAllParam(proceedingJoinPoint);
String targetValue = "";
for (int i = 0; i < propertyss.length; i++) {
String property = propertyss[i];
try {
if (i != propertyss.length - 1) {
if (paramJson.containsKey(property)) {
paramJson = paramJson.getJSONObject(property);
}
} else {
if (paramJson.containsKey(property)) {
targetValue = (String) paramJson.get(property);
}
}
} catch (Exception e) {
log.error("error:{}",e);
}
}
return targetValue;
}
//通过proceedingJoinPoint获取切入方法的参数名及参数值
public JSONObject getAllParam(ProceedingJoinPoint proceedingJoinPoint) {
Map<String, Object> map = new HashMap<>();
Object[] values = proceedingJoinPoint.getArgs();
String[] names = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();
for (int i = 0; i < names.length; i++) {
map.put(names[i], values[i]);
}
return (JSONObject) JSONObject.toJSON(map);
}
}
3)Controller类
package com.liuy.esports1.controller;
import com.liuy.esports1.annotation.SysLog;
import com.liuy.esports1.entity.User;
import org.springframework.web.bind.annotation.*;
/**
* @author dell
*/
@RestController
@RequestMapping("/log")
public class TestLogController {
@SysLog("用户查询")
@GetMapping("/test")
public String test(String name){
name = name + "在工作!";
return name;
}
@SysLog("用户添加.concat(#user.role.roleName)")
@PostMapping("/test")
public String add(@RequestBody User user){
String name = user.getName() + "在工作!";
return name;
}
}
4)实体类
package com.liuy.esports1.entity;
import lombok.Data;
@Data
public class Role {
private Integer roleId;
private String roleName;
}
package com.liuy.esports1.entity;
import lombok.Data;
@Data
public class User {
private Integer id;
private String name;
private String age;
private Role role;
}
5)spring event观察者事件
package com.liuy.esports1.event;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysLogEvent {
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 用户名称
*/
private String username;
/**
* 注解上的描述
*/
private String operation;
/**
* 执行时间
*/
private Integer time;
/**
* 执行方法
*/
private String method;
/**
* 参数
*/
private String params;
/**
* ip地址
*/
private String ip;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date gmtCreate;
}
package com.liuy.esports1.event;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Log4j2
@Configuration
public class SysLogListener {
@SneakyThrows
@EventListener(SysLogEvent.class)
public void onApplicationEvent(SysLogEvent event) {
String username = event.getUsername();
log.info("-----------------------log insert to db---------");
log.info("----data:{}",event.toString());
//数据库存储、远程接口调用
}
}
6)工具类
package com.liuy.esports1.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
/**
* 获取IP地址
*
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}