Java——AOP實(shí)現(xiàn)監(jiān)控日志功能(java項(xiàng)目日志監(jiān)控平臺(tái))
java知識(shí)點(diǎn)整理正在進(jìn)行中,關(guān)注我,持續(xù)給您帶來(lái)簡(jiǎn)單,實(shí)用的Java編程技巧。
最近在做一個(gè)接口項(xiàng)目,需要一個(gè)能夠記錄接口運(yùn)行情況的監(jiān)控日志。想在日志中記錄:
- 接口的輸入?yún)?shù),返回結(jié)果
- 調(diào)用接口的IP地址,調(diào)用的是那個(gè)接口
- 接口運(yùn)行時(shí)的異常信息
- 接口的響應(yīng)時(shí)間
結(jié)合具體的使用環(huán)境,還需要:
- 希望記錄日志能以統(tǒng)一的方式運(yùn)行,記錄日志的代碼不寫(xiě)在具體的業(yè)務(wù)邏輯中
- 可以方便的設(shè)置是否記錄日志
- 在設(shè)置時(shí)可以靈活地確定記錄在那個(gè)日志文件中
針對(duì)以上要求,結(jié)合前陣子做過(guò)的一個(gè)自定義注解記錄接口運(yùn)行時(shí)間的例子,發(fā)現(xiàn)這個(gè)需求可以認(rèn)為是之前例子的升級(jí)版。整體思路梳理了下:
- 使用AOP來(lái)截取調(diào)用接口的相關(guān)信息,包括請(qǐng)求的IP,請(qǐng)求的是那個(gè)接口,調(diào)用的參數(shù)和返回結(jié)果,還有異常信息
- 使用自定義注解來(lái)確定是否記錄日志
- 使用注解的參數(shù)來(lái)確定日志記錄到那個(gè)文件中
這樣做就可以實(shí)現(xiàn):
- 業(yè)務(wù)代碼與日志代碼的解耦
- 監(jiān)控日志業(yè)務(wù)的靈活運(yùn)用,可以方便的決定那個(gè)業(yè)務(wù)進(jìn)行監(jiān)控,同時(shí)可以靈活的調(diào)整日志記錄在那個(gè)文件中
好,說(shuō)干就干,代碼開(kāi)擼。
1. 引入依賴
<!-- AOP依賴 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency><!-- 獲取運(yùn)行時(shí)長(zhǎng) --><dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version></dependency><!-- 轉(zhuǎn)義JSON --><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version></dependency>
2. 編寫(xiě)自定義注解MonitorLog
package com.bbzd.mws.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 用于監(jiān)控日志的注解 * @author mill * @date 2022/10/14 - 11:54 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface MonitorLog { String value();}
3. AOP的實(shí)現(xiàn)類
package com.bbzd.mws.aop;import com.alibaba.fastjson.JSON;import com.bbzd.mws.annotation.MonitorLog;import com.google.common.base.Stopwatch;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;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 javax.servlet.http.HttpServletResponse;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;/** * 業(yè)務(wù)監(jiān)控日志 * 記錄內(nèi)容:請(qǐng)求IP,請(qǐng)求URI,業(yè)務(wù)類名,方法名,輸入?yún)?shù),返回值,異常信息 * @date 2022/10/12 - 10:12 */@Component@Aspectpublic class RequestParameterAOP { //以注解MonitorLog標(biāo)記的方法為切入點(diǎn) @Pointcut("@annotation(com.bbzd.mws.annotation.MonitorLog)") public void methodArgs(){} @Around("methodArgs()") public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable{ StringBuffer stringBuffer=new StringBuffer(); Object result=null; Stopwatch stopwatch = Stopwatch.createStarted(); HttpServletRequest httpServletRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest(); String ipAddr=getRemoteHost(httpServletRequest); String requestUrl=httpServletRequest.getRequestURI(); stringBuffer.append("n請(qǐng)求源IP[" ipAddr "];"); stringBuffer.append("請(qǐng)求URL[" requestUrl "];"); Signature signature=joinPoint.getSignature(); MethodSignature methodSignature=(MethodSignature)signature; // 類名 String[] sourceName = signature.getDeclaringTypeName().split("."); String fullName=signature.getDeclaringTypeName(); String className = sourceName[sourceName.length - 1]; // 方法名 String methodName = signature.getName(); stringBuffer.append("n" className "." methodName ";"); // 參數(shù)名數(shù)組 String[] parameterNames = methodSignature.getParameterNames(); Class[] parameterTypes=methodSignature.getParameterTypes(); // 構(gòu)造參數(shù)組集合 List<Object> argList = new ArrayList<>(); for (Object arg : joinPoint.getArgs()) { // request/response無(wú)法使用toJSON if (arg instanceof HttpServletRequest) { argList.add("request"); } else if (arg instanceof HttpServletResponse) { argList.add("response"); } else { argList.add(JSON.toJSON(arg)); } } stringBuffer.append("n請(qǐng)求參數(shù):" JSON.toJSON(parameterNames) "->" JSON.toJSON(argList)); try{ result=joinPoint.proceed(); }catch(exception e){ stringBuffer.append("n異常:" e.getMessage()); //log.info("獲取參數(shù)失?。簕}",e.getMessage()); } stopwatch.stop(); long timeConsuming = stopwatch.elapsed(TimeUnit.MILLISECONDS); if(result!=null){ stringBuffer.append("n請(qǐng)求結(jié)果:" JSON.toJSON(result)); }else{ stringBuffer.append("n請(qǐng)求結(jié)果:無(wú)"); } stringBuffer.append("n請(qǐng)求耗時(shí):" timeConsuming "毫秒"); Logger logger=getLogger(fullName,methodName,parameterTypes); logger.info(stringBuffer.toString()); return result; } /** * 從請(qǐng)求中獲取請(qǐng)求源IP * @param request * @return 請(qǐng)求源IP */ private String getRemoteHost(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 ip.contains("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip; } /** * 根據(jù)MonitorLog注解中的值,返回Logger * @param className MonitorLog所在方法對(duì)應(yīng)的類名 * @param methodName MonitorLog所在方法對(duì)應(yīng)的方法名 * @param paramTypes MonitorLog所在方法對(duì)應(yīng)的參數(shù)名 * @return */ private Logger getLogger( String className, String methodName, Class[] paramTypes){ String logName="com.bbzd.mws.aop"; try{ Class clazz=Class.forName(className); logName=clazz.getDeclaredMethod(methodName, paramTypes).getAnnotation(MonitorLog.class).value(); }catch(Exception e){ e.printStackTrace(); } Logger logger= LoggerFactory.getLogger(logName); return logger; }}
4. 業(yè)務(wù)邏輯方法
@Override //com.bbzd.mws.aop是logger的名稱,需要在日志文件中進(jìn)行對(duì)應(yīng)的配置@MonitorLog(value = "com.bbzd.mws.aop")public User getUserName(@Valid @RequestBody @WebParam(name="UserVo") UserVo vo) throws ConstraintViolationException { User user = new User(vo.getName(), vo.getAge()); try{ //模擬異常情況,測(cè)試異常信息的記錄 //int i=1/0; }catch(NullPointerException exception){ exception.printStackTrace(); } return user;}
5. 日志配置文件
<!-- name屬性值需要與注解的value屬性值一樣 --><logger name="com.bbzd.mws.aop" level="DEBUG" additivity="false"> <appender-ref ref="console"/> <appender-ref ref="monitor"/></logger>
6. 日志記錄內(nèi)容示例
[11:11:59.266][INFO][com.bbzd.mws.aop][http-nio-8889-exec-1] 請(qǐng)求源IP[127.0.0.1];請(qǐng)求URL[/mws/ws/user];UserServiceImpl.getUserName;請(qǐng)求參數(shù):["vo"]->[{"name":"powerful","age":10}]請(qǐng)求結(jié)果:{"name":"powerful","age":10}請(qǐng)求耗時(shí):56毫秒
總結(jié)
- POM文件是代碼片段
- 配置文件是logback的代碼片段
- 其它文件是完整的代碼
- 關(guān)于logback日志框架及l(fā)ogback配置文件的使用方法,后面會(huì)整理一篇文章詳細(xì)介紹下,想了解的小伙伴可以關(guān)注我。