做项目时,有一个记录操作日志的需求,比如某个用户进行了查询、删除、修改等操作,需要把这个操作所对应的各种信息记录下来,为了实现这个需求,采用Spring AOP的切面可以实现。
我采用的是@AfterReturning和自定义注解来实现的,AOP还有其他几个通知,我就不写了。自定义注解是为了描述这个方法具体是什么操作。
自定义注解:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/** 是否记录日志 true记录 false不记录**/
public boolean record() default true;
/** 要执行的具体操作比如:添加用户 **/
public String operationName() default "";
}
这个注解类,标注在Controller方法上,在给operationName参数定义一个名字,就可以知道这个方法具体是哪个操作了,后续AOP记录时,获取该注解的operationName值,就能获取到对应的方法描述。
注意:eclipse不能导包,需要手动
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.ElementType;
日志POJO类(set get省略),用于保存日志信息,方便把这些信息插入数据库或者打印到日志文件:
private Integer id;//主键
private String userId;//登录用户账号
private String nickName;//登录用户昵称
private String operationName;//模块
private String ip;//ip地址
private Integer port;//端口
private String url;//url路径
private String method;//提交方式
private String cls;//类
private String classMethod;//调用方法
private String para;//传递参数
//@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",locale="zh",timezone="GMT+8") //参数注解,Date对象返回时,格式化日期 yyyy-MM-dd HH:mm:ss a
private String date;//操作时间
private Long time;//耗时
private Integer success;//是否成功 1成功。0失败
重点来啦,AOP切面类:
@Aspect // 表示一个切面bean
@Component // bean容器的组件注解。虽然放在contrller包里,但它不是控制器。如果注入service,但我们又没有放在service包里
@Order(3) // 有多个日志时,ORDER可以定义切面的执行顺序(数字越大,前置越后执行,后置越前执行)
public class WebLogAspect {
//定义日志记录器--获取sl4j包下提供的logger
Logger logger = LoggerFactory.getLogger(this.getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>(); //线程副本类去记录各个线程的开始时间
@Autowired
private LogPojoServiceImpl logPojoServiceImpl;
//在com.zy.controller包下,任意返回值(第一个*)、任意类(第二个*)、任意方法(第三个*)上插入切点
@Pointcut("execution(public * com.zy.controller.*.*(..))")
public void weblog() {
}
//方法的返回值注入给ret
@AfterReturning(returning = "ret", pointcut = "weblog()")
public void doafter(JoinPoint joinPoint,Object ret) {
try {
//获得注解
Log controllerLog = getAnnotationLog(joinPoint);
logger.info("后置返回通知:"); //info ,debug ,warn ,erro四种级别,这里我们注入info级别
startTime.set(System.currentTimeMillis());
//获取servlet请求对象---因为这不是控制器,这里不能注入HttpServletRequest,但springMVC本身提供ServletRequestAttributes可以拿到
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
User userObj= (User) request.getSession().getAttribute("loginUser");//获取登录用户
String userId=userObj.getUserId();//获取账号
String nickName=userObj.getNickName();//获取昵称
String ip=request.getRemoteAddr();// 获取ip
int port=request.getRemotePort();//获取端口
String url=request.getRequestURL().toString(); //想那个url发的请求
String method=request.getMethod();//提交方式
String cls=joinPoint.getSignature().getDeclaringTypeName();//类
String classMethod=joinPoint.getSignature().getName();//方法
String para=Arrays.toString(joinPoint.getArgs());// 方法本传了哪些参数
String date=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//操作时间
long time=System.currentTimeMillis()-startTime.get() ;
logger.info("登录用户昵称:" + nickName);
logger.info("ip:" + ip);
logger.info("端口:" + port);
logger.info("URL:" + url);
logger.info("提交方式:" + method);
logger.info("类:" + cls);
logger.info("调用方法:" + classMethod); //
logger.info("接收参数:" + para); // 方法本传了哪些参数
logger.info("操作时间:" + date);
//logger.info("返回值:" + ret); // 响应的内容---方法的返回值responseEntity
logger.info("耗时:" + time);
if(controllerLog!=null) {
String operationName=controllerLog.operationName();//获取模块名称
logger.info("模块:" + operationName);
//是否需要记录日志
boolean isRecord=controllerLog.record();
LogPojo logPojo=new LogPojo();
if(isRecord) {
logPojo.setNickName(nickName);
logPojo.setUserId(userId);
logPojo.setOperationName(operationName);
logPojo.setIp(ip);
logPojo.setPort(port);
logPojo.setUrl(url);
logPojo.setMethod(method);
logPojo.setCls(cls);
logPojo.setClassMethod(classMethod);
logPojo.setPara(para);
logPojo.setDate(date);
logPojo.setTime(time);
logPojo.setSuccess(1);
logPojoServiceImpl.insertLogPojo(logPojo);//把日志数据保存进数据库
logger.info(isRecord+"该日志需要记录-------------------------------");
}else {
logger.info(isRecord+"该日志不需要记录-------------------------------");
}
}
logger.info("---------------------------------------------------------------------");
} catch (Exception e) {
// 记录本地异常日志
logger.error("==通知异常==");
logger.error("异常信息:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 是否存在注解,如果存在就获取
*/
private static Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
}
这里定义了一个获取注解的方法,就是为了获取Controller 层的具体操作。
然后看Controller上的方法,这样用自定义Log注解标注后:
//查询数据
@GetMapping("/queryUserAll")
@ResponseBody
@Log(operationName="查询用户")
public Result<User> queryUserAll() {
return userManageServiceImpl.queryUserAll();
}
//添加
@PostMapping("/addUser")
@ResponseBody
@Log(operationName="添加用户")
public Result<User> addUser(@RequestBody User user) {
System.out.println(user);
return userManageServiceImpl.addUser(user);
}
//修改
@PostMapping("/updateUser")
@ResponseBody
@Log(operationName="修改用户")
public Result<User> updateUser(@RequestBody User user) {
return userManageServiceImpl.updateUser(user);
}
最后打印出来的结果如下,我把这些日志信息写入了数据库
数据库中保存的数据如下:
为啥我不用前置通知呢?应为在登录时,session还未保存用户数据,所以获取不到登录时的用户名,所以我干脆就直接使用后置返回通知了。 |