SpringAop实现操作日志记录
前言
大家好,这里是经典鸡翅,今天给大家带来一篇基于SpringAop实现的操作日志记录的解决的方案。大家可能会说,切,操作日志记录这么简单的东西,老生常谈了。不!
网上的操作日志一般就是记录操作人,操作的描述,ip等。好一点的增加了修改的数据和执行时间。那么!我这篇有什么不同呢!今天这种不仅可以记录上方所说的一切,还增加记录了操作前的数据,错误的信息,堆栈信息等。正文开始~~~~~
思路介绍
记录操作日志的操作前数据是需要思考的重点。我们以修改场景来作为探讨。当我们要完全记录数据的流向的时候,我们必然要记录修改前的数据,而前台进行提交的时候,只有修改的数据,那么如何找到修改前的数据呢。有三个大的要素,我们需要知道修改前数据的表名,表的字段主键,表主键的值。这样通过这三个属性,我们可以很容易的拼出select*from表名where主键字段=主键值。我们就获得了修改前的数据,转换为json之后就可以存入到数据库中了。如何获取三个属性就是重中之重了。我们采取的方案是通过提交的映射实体,在实体上打上注解,根据Java的反射取到值。再进一步拼装获得对象数据。那么AOP是在哪里用的呢,我们需要在记录操作日志的方法上,打上注解,再通过切面获取到切点,一切的数据都通过反射来进行获得。
定义操作日志注解
既然是基于spinrg的aop实现切面。那么必然是需要一个自定义注解的。用来作为切点。我们定义的注解,可以带一些必要的属性,例如操作的描述,操作的类型。操作的类型需要说一下,我们分为新增、修改、删除、查询。那么只有修改和删除的时候,我们需要查询一下修改前的数据。其他两种是不需要的,这个也可以用来作为判断。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceOperateLog{
Stringoperation()default"";
StringoperateType()default"";
}
定义用于找到表和表主键的注解
表和表主键的注解打在实体上,内部有两个属性tableName和idName。这两个属性的值获得后,可以进行拼接select*from表名where主键字段。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceSelectTable{
StringtableName()default"";
StringidName()default"";
}
定义获取主键值的注解
根据上面所说的三个元素,我们还缺最后一个元素主键值的获取,用于告诉我们,我们应该从提交的请求的那个字段,拿到其中的值。
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interfaceSelectPrimaryKey{
}
注解的总结
有了上面的三个注解,注解的准备工作已经进行完毕。我们通过反射取到数据,可以获得一切。接下来开始实现切面,对于注解的值进行拼接处理,最终存入到我们的数据库操作日志表中。
切面的实现
对于切面来说,我们需要实现切点、数据库的插入、反射的数据获取。我们先分开进行解释,最后给出全面的实现代码。方便大家的理解和学习。
切面的定义
基于spring的aspect进行声明这是一个切面。
@Aspect
@Component
publicclassOperateLogAspect{
}
切点的定义
切点就是对所有的打上OperateLog的注解的请求进行拦截和加强。我们使用annotation进行拦截。
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
privatevoidoperateLogPointCut(){
}
获取请求ip的共用方法
privateStringgetIp(HttpServletRequestrequest){
Stringip=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.getHeader("HTTP_CLIENT_IP");
}
if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=request.getHeader("HTTP_X_FORWARDED_FOR");
}
if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=request.getRemoteAddr();
}
returnip;
}
数据库的日志插入操作
我们将插入数据库的日志操作进行单独的抽取。
privatevoidinsertIntoLogTable(OperateLogInfooperateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
Stringsql="insertintologvalues(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}
环绕通知的实现
日志的实体类实现
@TableName("operate_log")
@Data
publicclassOperateLogInfo{
//主键id
@TableId
privateStringid;
//操作人id
privateStringuserId;
//操作人名称
privateStringuserName;
//操作内容
privateStringoperation;
//操作方法名称
privateStringmethod;
//操作后的数据
privateStringmodifiedData;
//操作前数据
privateStringpreModifiedData;
//操作是否成功
privateStringresult;
//报错信息
privateStringerrorMessage;
//报错堆栈信息
privateStringerrorStackTrace;
//开始执行时间
privateDateexecuteTime;
//执行持续时间
privateLongduration;
//ip
privateStringip;
//操作类型
privateStringoperateType;
}
准备工作全部完成。接下来的重点是对环绕通知的实现。思路分为数据处理、异常捕获、finally执行数据库插入操作。环绕通知的重点类就是ProceedingJoinPoint,我们通过它的getSignature方法可以获取到打在方法上注解的值。例如下方。
MethodSignaturesignature=(MethodSignature)pjp.getSignature(); OperateLogdeclaredAnnotation=signature.getMethod().getDeclaredAnnotation(OperateLog.class); operateLogInfo.setOperation(declaredAnnotation.operation()); operateLogInfo.setModule(declaredAnnotation.module()); operateLogInfo.setOperateType(declaredAnnotation.operateType()); //获取执行的方法 Stringmethod=signature.getDeclaringType().getName()+"."+signature.getName(); operateLogInfo.setMethod(method); StringoperateType=declaredAnnotation.operateType();
获取请求的数据,也是通过这个类来实现,这里有一点是需要注意的,就是我们要约定参数的传递必须是第一个参数。这样才能保证我们取到的数据是提交的数据。
if(pjp.getArgs().length>0){
Objectargs=pjp.getArgs()[0];
operateLogInfo.setModifiedData(newGson().toJson(args));
}
接下来的一步就是对修改前的数据进行拼接。之前我们提到过如果是修改和删除,我们才会进行数据的拼接获取,主要是通过类来判断书否存在注解,如果存在注解,那么就要判断注解上的值是否是控制或者,非空才能正确的进行拼接。取field的值的时候,要注意私有的变量需要通过setAccessible(true)才可以进行访问。
if(GlobalStaticParas.OPERATE_MOD.equals(operateType)||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
StringtableName="";
StringidName="";
StringselectPrimaryKey="";
if(pjp.getArgs().length>0){
Objectargs=pjp.getArgs()[0];
//获取操作前的数据
booleanselectTableFlag=args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName=args.getClass().getAnnotation(SelectTable.class).tableName();
idName=args.getClass().getAnnotation(SelectTable.class).idName();
}else{
thrownewRuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
}
Field[]fields=args.getClass().getDeclaredFields();
Field[]fieldsCopy=fields;
booleanisFindField=false;
intfieldLength=fields.length;
for(inti=0;i>maps=jdbcTemplate.queryForList(sql,selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(newGson().toJson(maps));
}
}catch(Exceptione){
e.printStackTrace();
thrownewRuntimeException("查询操作前数据出错!");
}
}else{
thrownewRuntimeException("表名、主键名或主键值存在空值情况,请核实!");
}
}else{
operateLogInfo.setPreModifiedData("");
}
切面的完整实现代码
@Aspect
@Component
publicclassOperateLogAspect{
@Autowired
privateJdbcTemplatejdbcTemplate;
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
privatevoidoperateLogPointCut(){
}
@Around("operateLogPointCut()")
publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{
ObjectresponseObj=null;
OperateLogInfooperateLogInfo=newOperateLogInfo();
Stringflag="success";
try{
HttpServletRequestrequest=SpringContextUtil.getHttpServletRequest();
DomainUserDetailscurrentUser=SecurityUtils.getCurrentUser();
if(currentUser!=null){
operateLogInfo.setUserId(currentUser.getId());
operateLogInfo.setUserName(currentUser.getUsername());
}
MethodSignaturesignature=(MethodSignature)pjp.getSignature();
OperateLogdeclaredAnnotation=signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//获取执行的方法
Stringmethod=signature.getDeclaringType().getName()+"."+signature.getName();
operateLogInfo.setMethod(method);
StringoperateType=declaredAnnotation.operateType();
if(pjp.getArgs().length>0){
Objectargs=pjp.getArgs()[0];
operateLogInfo.setModifiedData(newGson().toJson(args));
}
if(GlobalStaticParas.OPERATE_MOD.equals(operateType)||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
StringtableName="";
StringidName="";
StringselectPrimaryKey="";
if(pjp.getArgs().length>0){
Objectargs=pjp.getArgs()[0];
//获取操作前的数据
booleanselectTableFlag=args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName=args.getClass().getAnnotation(SelectTable.class).tableName();
idName=args.getClass().getAnnotation(SelectTable.class).idName();
}else{
thrownewRuntimeException("操作日志类型为修改或删除,实体类必须指定表面和主键注解!");
}
Field[]fields=args.getClass().getDeclaredFields();
Field[]fieldsCopy=fields;
booleanisFindField=false;
intfieldLength=fields.length;
for(inti=0;i>maps=jdbcTemplate.queryForList(sql,selectPrimaryKey);
if(maps!=null){
operateLogInfo.setPreModifiedData(newGson().toJson(maps));
}
}catch(Exceptione){
e.printStackTrace();
thrownewRuntimeException("查询操作前数据出错!");
}
}else{
thrownewRuntimeException("表名、主键名或主键值存在空值情况,请核实!");
}
}else{
operateLogInfo.setPreModifiedData("");
}
//操作时间
DatebeforeDate=newDate();
LongstartTime=beforeDate.getTime();
operateLogInfo.setExecuteTime(beforeDate);
responseObj=pjp.proceed();
DateafterDate=newDate();
LongendTime=afterDate.getTime();
Longduration=endTime-startTime;
operateLogInfo.setDuration(duration);
operateLogInfo.setIp(getIp(request));
operateLogInfo.setResult(flag);
}catch(RuntimeExceptione){
thrownewRuntimeException(e);
}catch(Exceptione){
flag="fail";
operateLogInfo.setResult(flag);
operateLogInfo.setErrorMessage(e.getMessage());
operateLogInfo.setErrorStackTrace(e.getStackTrace().toString());
e.printStackTrace();
}finally{
insertIntoLogTable(operateLogInfo);
}
returnresponseObj;
}
privatevoidinsertIntoLogTable(OperateLogInfooperateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
Stringsql="insertintoenergy_logvalues(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}
privateStringgetIp(HttpServletRequestrequest){
Stringip=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.getHeader("HTTP_CLIENT_IP");
}
if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=request.getHeader("HTTP_X_FORWARDED_FOR");
}
if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){
ip=request.getRemoteAddr();
}
returnip;
}
}
示例的使用方式
针对于示例来说我们要在controller上面打上操作日志的注解。
@PostMapping("/updateInfo")
@OperateLog(operation="修改信息",operateType=GlobalStaticParas.OPERATE_MOD)
publicvoidupdateInfo(@RequestBodyInfoinfo){
service.updateInfo(info);
}
针对于Info的实体类,我们则要对其中的字段和表名进行标识。
@Data
@SelectTable(tableName="info",idName="id")
publicclassInfo{
@SelectPrimaryKey
privateStringid;
privateStringname;
}
总结
文章写到这,也就结束了,文中难免有不足,欢迎大家批评指正
以上就是SpringAop实现操作日志记录的详细内容,更多关于SpringAop操作日志记录的资料请关注毛票票其它相关文章!