这允许处理多值参数
发布时间:2025-06-24 20:09:52 作者:北方职教升学中心 阅读量:754
* @param jsonResult 方法返回的对象,用于日志记录,此参数可能为null。
发布时间:2025-06-24 20:09:52 作者:北方职教升学中心 阅读量:754
* @param jsonResult 方法返回的对象,用于日志记录,此参数可能为null。
在企业应用开发中,操作日志记录是确保系统安全性、通过记录用户的操作行为,不仅可以帮助开发者快速定位问题,还能满足审计和合规需求。 * * @param joinPoint 切点对象,用于获取方法名和参数信息。 * @param excludeParamNames 需要排除的参数名数组,这些参数不会被转换为字符串。它不仅能够记录用户的操作行为,还能帮助开发和运维人员快速定位和解决问题,提升系统的稳定性和安全性。 */privatestaticMap<String,String[]>getParameterMap(){// 从Spring的RequestContextHolder中获取当前请求的属性RequestAttributesrequestAttributes =RequestContextHolder.getRequestAttributes();// 将RequestAttributes强制转换为ServletRequestAttributes,以便访问HTTP请求特定的属性ServletRequestAttributesservletRequestAttributes =(ServletRequestAttributes)requestAttributes;// 从ServletRequestAttributes中获取当前HTTP请求对象HttpServletRequestrequest =(HttpServletRequest)servletRequestAttributes.getRequest();// 获取请求的所有参数Map<String,String[]>parameterMap =request.getParameterMap();returnparameterMap;}/** * 忽略敏感属性 * * @param excludeParamNames 需要排除的参数名数组 * @return {@link PropertyPreFilters.MySimplePropertyPreFilter} */publicPropertyPreFilters.MySimplePropertyPreFilterexcludePropertyPreFilter(String[]excludeParamNames){returnnewPropertyPreFilters().addFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES,excludeParamNames));}/** * 将对象数组转换为字符串,排除指定的参数名(敏感参数)。方法信息、在文章的开始,我们探讨了在SpringBoot应用程序中实现日志操作日志记录的重要性,随后采用基于AOP+注解的解决方案,以将日志数据存储到数据库中。 * @param operLog 操作日志对象,用于设置请求参数信息。 * @param jsonResult 方法的返回结果,用于判断是否需要记录响应数据。sysOperLog.setErrorMsg(e.getMessage());}// 获取ip地址StringipAddress =IpUtil.getIpAddress(request);// 设置ip地址sysOperLog.setOperIp(ipAddress);// 设置请求地址sysOperLog.setOperUrl(request.getRequestURI());// 获取当前登录的用户信息。
CREATETABLE`sys_oper_log` (`id` bigint(20)NOTNULLAUTO_INCREMENTCOMMENT'日志主键',`title` varchar(50)DEFAULT'' COMMENT'模块标题',`business_type` varchar(20)DEFAULT'0'COMMENT'业务类型(0其它 1新增 2修改 3删除)',`method` varchar(100)DEFAULT'' COMMENT'方法名称',`request_method` varchar(10)DEFAULT'' COMMENT'请求方式',`oper_name` varchar(50)DEFAULT'' COMMENT'操作人员',`oper_url` varchar(255)DEFAULT'' COMMENT'请求URL',`oper_ip` varchar(128)DEFAULT'' COMMENT'主机地址',`oper_param` varchar(2000)DEFAULT'' COMMENT'请求参数',`json_result` varchar(2000)DEFAULT'' COMMENT'返回参数',`status` int(1)DEFAULT'0'COMMENT'操作状态(1正常 0异常)',`error_msg` varchar(2000)DEFAULT'' COMMENT'错误消息',`oper_time` datetime DEFAULTNULLCOMMENT'操作时间',`execute_time` bigint(20)NOTNULLDEFAULT'0'COMMENT'执行时长(毫秒)',PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=64DEFAULTCHARSET=utf8 COMMENT='操作日志记录';
/** * 操作日志记录 * * @date 2024/07/14 */@Data@Schema(description ="操作日志记录")@TableName(value ="sys_oper_log")publicclassSysOperLogimplementsSerializable{@TableField(exist =false)privatestaticfinallongserialVersionUID =1L;@TableId(type =IdType.AUTO)@Schema(description ="日志主键")privateLongid;@Schema(description ="模块标题")privateStringtitle;@Schema(description ="业务类型(0其它 1新增 2修改 3删除)")privateStringbusinessType;@Schema(description ="方法名称")privateStringmethod;@Schema(description ="请求方式")privateStringrequestMethod;@Schema(description ="操作类别(0其它 1后台用户 2手机端用户)")privateStringoperatorType;@Schema(description ="操作人员")privateStringoperName;@Schema(description ="请求URL")privateStringoperUrl;@Schema(description ="主机地址")privateStringoperIp;@Schema(description ="请求参数")privateStringoperParam;@Schema(description ="返回参数")privateStringjsonResult;@Schema(description ="操作状态(1正常 0异常)")privateIntegerstatus;@Schema(description ="错误消息")privateStringerrorMsg;@Schema(description ="操作时间")privateDateoperTime;@Schema(description ="执行时长")privatelongexecuteTime;}
@AfterReturning(pointcut ="@annotation(controllerLog)",returning ="jsonResult")publicvoiddoAfterReturning(JoinPointjoinPoint,LogcontrollerLog,ObjectjsonResult){handleLog(joinPoint,controllerLog,null,jsonResult);}
doAfterThrowing
方法。简介执行流程分析:
请求到达:当一个请求到达目标方法时,切面会首先执行
boBefore
方法,记录方法的开始时间。 * * @param joinPoint 切点 * @param controllerLog 一个注解对象,表示目标方法上标注的注解。二、代码实现及测试验证等步骤。简介
1.1 操作日志在企业应用中的重要性
操作日志在企业应用中扮演着至关重要的角色。Useruser =UserHolder.getUser();// 获取用户名Stringusername =UserHolder.getUser().getUserName();// 设置操作者名称。 * @param excludeParamNames 需要排除的参数名数组,这些参数不会被记录在日志中。sysOperLog.setStatus(BusinessStatus.SUCCESS.ordinal());// 如果方法执行过程中抛出异常,则将操作状态设置为异常。RequestAttributesrequestAttributes =RequestContextHolder.getRequestAttributes();// 如果请求属性为空,则直接返回,不处理日志。本文旨在探讨如何在SpringBoot应用程序中通过AOP(面向切面编程)和自定义注解实现操作日志记录,并将日志存储到数据库中。 * * @param joinPoint 切点,用于获取方法参数。RequestAttributesattributes =RequestContextHolder.getRequestAttributes();ServletRequestAttributeshttp =(ServletRequestAttributes)attributes;// 再次获取HttpServletRequest对象。
- 统一管理与维护:集中管理日志记录逻辑,方便后续的功能扩展和维护。
1.2 使用AOP和注解实现操作日志记录的好处
在SpringBoot项目中,通过AOP(面向切面编程)和自定义注解来实现操作日志记录具有诸多好处:
- 分离关注点:将日志记录逻辑从业务代码中分离出来,保持代码的清洁和可维护性。
/** * 获取用户信息 * * @param id 用户id * @return {@link Result}<{@link UserInfo}> */@Log(title ="获取用户信息",businessType =BusinessType.OTHER)@Operation(description ="获取用户信息")@GetMapping("/{id}")publicResult<UserInfo>getUser(@PathVariableLongid){returnResult.success(userInfoService.getById(id));}/** * 插入用户信息 * * @param userInfo 用户信息 * @return {@link Result}<{@link String}> */@Log(title ="插入用户信息",businessType =BusinessType.INSERT)@Operation(description ="插入用户信息")@PostMappingpublicResult<String>insertUser(@RequestBodyUserInfouserInfo){booleansaved =userInfoService.save(userInfo);if(!saved){returnResult.error("插入失败");}returnResult.success();}/** * 更新用户信息 * * @param userInfo 用户信息 * @return {@link Result}<{@link String}> */@Log(title ="更新用户信息",businessType =BusinessType.UPDATE)@Operation(description ="更新用户信息")@PutMappingpublicResult<String>updateUser(@RequestBodyUserInfouserInfo){booleanupdated =userInfoService.updateById(userInfo);if(!updated){returnResult.error("更新失败");}returnResult.success();}/** * 删除用户信息 * @param id i用户id * @return {@link Result}<{@link String}> */@Log(title ="删除用户信息",businessType =BusinessType.DELETE)@Operation(description ="删除用户信息")@DeleteMapping("/{id}")publicResult<String>deleteUser(@PathVariableLongid){booleandeleted =userInfoService.removeById(id);if(!deleted){returnResult.error("删除失败");}returnResult.success();}
附录:
若依仓库地址
SysOperLog
对象中,最后通过sysOperLogService
保存该日志对象。@Before(value ="@annotation(controllerLog)")publicvoidboBefore(JoinPointjoinPoint,LogcontrollerLog){TIME_THREADLOCAL.set(System.currentTimeMillis());}
方法执行:
doAfterReturning
方法。@AfterThrowing(pointcut ="@annotation(controllerLog)",throwing ="e")publicvoiddoAfterThrowing(JoinPointjoinPoint,LogcontrollerLog,Exceptione){handleLog(joinPoint,controllerLog,e,null);}
日志处理:在handleLog
方法中,切面会收集各种请求信息、集合或Map类型,检查它是否为MultipartFile、测试
handleLog
方法来处理操作日志,并记录异常信息。HttpServletRequesthttpServletRequest =http.getRequest();// 创建SysOperLog对象,用于存储操作日志的信息。这个方法会调用handleLog
方法来处理操作日志。ex.printStackTrace();}}/** * 从注解中获取控制器方法的描述信息,并填充到操作日志对象中。准备工作/** * 业务操作类型 * */publicenumBusinessType{/** * 其他类型 */OTHER,/** * 新增 */INSERT,/** * 修改 */UPDATE,/** * 删除 */DELETE,/** * 更新状态 */STATUS,/** * 授权 */ASSIGN}
/** * 自定义操作日志记录注解 * */@Target({ElementType.PARAMETER,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceLog{/** * 模块名称 */Stringtitle()default"";/** * 业务操作类型 */BusinessTypebusinessType()defaultBusinessType.OTHER;/** * 是否保存请求参数 */booleanisSaveRequestData()defaulttrue;/** * 是否保存响应数据 */booleanisSaveResponseData()defaulttrue;/** * 排除指定的请求参数 */publicString[]excludeParamNames()default{};}
/** * 操作状态 * */publicenumBusinessStatus{/** * 成功 */SUCCESS,/** * 失败 */FAIL,}
/** * IP工具类 */publicclassIpUtil{/** * 获取ip * @param request 请求 * @return {@link String} */publicstaticStringgetIpAddress(HttpServletRequestrequest){StringipAddress =null;try{ipAddress =request.getHeader("x-forwarded-for");if(ipAddress ==null||ipAddress.length()==0||"unknown".equalsIgnoreCase(ipAddress)){ipAddress =request.getHeader("Proxy-Client-IP");}if(ipAddress ==null||ipAddress.length()==0||"unknown".equalsIgnoreCase(ipAddress)){ipAddress =request.getHeader("WL-Proxy-Client-IP");}if(ipAddress ==null||ipAddress.length()==0||"unknown".equalsIgnoreCase(ipAddress)){ipAddress =request.getRemoteAddr();if(ipAddress.equals("127.0.0.1")){// 根据网卡取本机配置的IPInetAddressinet =null;try{inet =InetAddress.getLocalHost();}catch(UnknownHostExceptione){e.printStackTrace();}ipAddress =inet.getHostAddress();}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割if(ipAddress !=null&&ipAddress.length()>15){// "***.***.***.***".length()// = 15if(ipAddress.indexOf(",")>0){ipAddress =ipAddress.substring(0,ipAddress.indexOf(","));}}}catch(Exceptione){ipAddress="";}// ipAddress = this.getRequest().getRemoteAddr();returnipAddress;}/** * 获取网关ip * @param request 请求 * @return {@link String} */publicstaticStringgetGatwayIpAddress(ServerHttpRequestrequest){HttpHeadersheaders =request.getHeaders();Stringip =headers.getFirst("x-forwarded-for");if(ip !=null&&ip.length()!=0&&!"unknown".equalsIgnoreCase(ip)){// 多次反向代理后会有多个ip值,第一个ip才是真实ipif(ip.indexOf(",")!=-1){ip =ip.split(",")[0];}}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =headers.getFirst("Proxy-Client-IP");}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =headers.getFirst("WL-Proxy-Client-IP");}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =headers.getFirst("HTTP_CLIENT_IP");}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =headers.getFirst("HTTP_X_FORWARDED_FOR");}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =headers.getFirst("X-Real-IP");}if(ip ==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip =request.getRemoteAddress().getAddress().getHostAddress();}returnip;}}
注意:这里不同的spring-web依赖版本
ServletRequestAttributes
的getResponse()
返回结果是不同的,我这里使用的spring-web:3.2.2
,返回值为jakarta包下面的HttpServletResponse
,而一些旧版本的就会返回javax包下的,因此要根据自身版本进行修改。 * 当方法执行完毕或发生异常时,此方法用于封装和记录操作日志。我们将详细介绍实现这一功能的完整流程,包括项目环境搭建、文章目录
- 前言
- 一、POST等。总结
本文主要参考了若依框架的操作日志记录功能的实现,记录了操作日志记录功能的实现和其中遇到的一些问题(比如:
getResponse()
返回值的问题)。 * @return 返回转换后的参数字符串,各参数间以空格分隔。 * @param controllerLog 控制器日志注解对象,包含标题、if(e !=null){// 设置状态为异常sysOperLog.setStatus(BusinessStatus.FAIL.ordinal());// 设置异常信息。这个时间被存储在一个ThreadLocal
对象中,用于后续计算方法的执行时长。响应数据等。if(requestAttributes ==null){return;}// 将请求属性转换为ServletRequestAttributes,以便获取HttpServletRequest对象。 */@Before(value ="@annotation(controllerLog)")publicvoidboBefore(JoinPointjoinPoint,LogcontrollerLog){TIME_THREADLOCAL.set(System.currentTimeMillis());}/** * 处理操作日志的逻辑。importcn.hutool.core.thread.threadlocal.NamedThreadLocal;importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.JSONObject;importcom.alibaba.fastjson.support.spring.PropertyPreFilters;importcom.voyager.annotation.Log;importcom.voyager.domain.entity.SysOperLog;importcom.voyager.domain.enums.BusinessStatus;importcom.voyager.entity.User;importcom.voyager.service.SysOperLogService;importcom.voyager.utils.IpUtil;importcom.voyager.utils.UserHolder;importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importlombok.RequiredArgsConstructor;importorg.apache.commons.lang3.ArrayUtils;importorg.aspectj.lang.JoinPoint;importorg.aspectj.lang.annotation.AfterReturning;importorg.aspectj.lang.annotation.AfterThrowing;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.springframework.stereotype.Component;importorg.springframework.util.StringUtils;importorg.springframework.validation.BindingResult;importorg.springframework.web.context.request.RequestAttributes;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importorg.springframework.web.multipart.MultipartFile;importjava.util.Collection;importjava.util.Date;importjava.util.Map;/** * 日志切面 */@Aspect@Component@RequiredArgsConstructorpublicclassLogAspect{/** * 定义需要排除在日志记录之外的属性名称数组 */privatestaticfinalString[]EXCLUDE_PROPERTIES={"password","oldPassword","newPassword","confirmPassword"};privatefinalSysOperLogServicesysOperLogService;/** * 使用ThreadLocal维护一个线程局部变量,用于记录操作的耗时 */privatestaticfinalThreadLocal<Long>TIME_THREADLOCAL=newNamedThreadLocal<Long>("Cost Time");/** * 返回通知 * * @param joinPoint 切点 */@AfterReturning(pointcut ="@annotation(controllerLog)",returning ="jsonResult")publicvoiddoAfterReturning(JoinPointjoinPoint,LogcontrollerLog,ObjectjsonResult){//调用处理日志的方法handleLog(joinPoint,controllerLog,null,jsonResult);}/** * 异常通知 * * @param joinPoint 切点 * @param e 异常 */@AfterThrowing(pointcut ="@annotation(controllerLog)",throwing ="e")publicvoiddoAfterThrowing(JoinPointjoinPoint,LogcontrollerLog,Exceptione){handleLog(joinPoint,controllerLog,e,null);}/** * 处理请求前执行,此方法旨在记录方法的开始时间。 */privateStringargsArrayToString(Object[]paramsArray,String[]excludeParamNames){// 使用StringBuilder来构建最终的参数字符串StringBuilderparams =newStringBuilder();// 检查参数数组是否为空或长度为0,避免不必要的处理if(paramsArray !=null){// 遍历参数数组中的每个对象for(Objecto :paramsArray){// 检查对象是否为空且不属于被过滤的类型if(o !=null&&!isFilterObject(o)){try{// 将对象转换为JSON字符串,排除指定的属性ObjectjsonObj =JSONObject.toJSONString(o,excludePropertyPreFilter(excludeParamNames));// 将转换后的JSON字符串追加到参数字符串中,并以空格分隔各个参数params.append(jsonObj).append(" ");}catch(Exceptionignored){// 忽略转换过程中的异常,确保方法的健壮性}}}}returnparams.toString().trim();}/** * 判断传入的对象是否需要被过滤。这允许处理多值参数。HttpServletRequest、代码实现
- 4.1 创建业务枚举类
- 4.2 创建日志注解
- 4.3 创建操作状态枚举类
- 4.4 创建IP工具类
- 4.5 创建切面类
- 4.6 操作日志注解使用
- 五、ServletRequestAttributesservletRequestAttributes =(ServletRequestAttributes)requestAttributes;// 获取HttpServletRequest对象。测试
- 分别执行请求四个接口:
- 查看数据库
六、 */
privatevoidsetRequestValue(JoinPointjoinPoint,SysOperLogoperLog,String[]excludeParamNames){// 获取当前请求的属性Map<String,String[]>parameterMap =getParameterMap();// 如果参数不为空且不为空集合if(parameterMap !=null&&!parameterMap.isEmpty()){// 将参数转换为JSON字符串,通过excludePropertyPreFilter过滤掉不需要记录的参数Stringparams =JSONObject.toJSONString(parameterMap,excludePropertyPreFilter(excludeParamNames));// 设置操作日志的请求参数,截取前2000个字符以防止过长operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params,0,2000));}else{// 如果请求参数为空,尝试从方法参数中获取信息Objectargs =joinPoint.getArgs();// 如果方法参数不为空if(args !=null){// 将方法参数转换为字符串,同样支持排除某些参数名Stringparams =argsArrayToString(joinPoint.getArgs(),excludeParamNames);// 设置操作日志的请求参数,同样截取前2000个字符operLog.setOperParam(org.apache.commons.lang3.StringUtils.substring(params,0,2000));}}}/** * 获取当前HTTP请求的参数 * * @return 一个Map,映射参数名称到参数值数组。开发环境- 三、 */privatevoidhandleLog(JoinPointjoinPoint,LogcontrollerLog,Exceptione,ObjectjsonResult){try{// 获取当前请求的属性,包括HttpServletRequest对象。开发环境
- JDK版本:JDK 17
- Spring Boot版本:Spring Boot 3.2.2
- MySQL版本:8.0.37
- Redis版本:5.0.14.1
- 构建工具:Maven
三、业务类型等配置信息。 * @param controllerLog 控制器上的日志注解,用于获取方法描述等信息。通过记录操作日志,企业可以:
SysOperLogsysOperLog =newSysOperLog();// 默认设置操作状态为正常。 * * @param o 待检查的对象 * @return 如果对象需要被过滤(即对象为MultipartFile或其他特定类型),则返回true;否则返回false。
- 监控用户行为:了解用户在系统中的操作轨迹,分析用户行为,改进用户体验。
获取和设置日志信息:在
handleLog
方法内部,通过调用一些辅助方法来获取和设置日志的详细信息,包括请求参数、HttpServletRequestrequest =servletRequestAttributes.getRequest();// 重新获取请求属性,目的是为了后续获取请求方法等信息。通过这个方案,我们能够有效地记录用户的操作行为,从而方便后续的审计和分析,希望对大家有所帮助😊。数据库设计、