一、背景
隨著項目的長期運行和迭代,積累的功能日益繁多,但并非所有功能都能得到用戶的頻繁使用或實際上根本無人問津。
為了提高系統性能和代碼質量,我們往往需要對那些不常用的功能進行下線處理。
那么,該下線哪些功能呢?
此時,我們就需要對接口的調用情況進行統計和分析了!
二、實戰
以下內容為主要代碼,完整代碼請參考:https://gitee.com/regexpei/daily-learning-test
以下使用 自定義注解 + AOP 的方式,對接口調用進行記錄。
1. 創建項目,添加依賴
<dependencies> <!-- 提供自動配置、日志、YAML等核心功能 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- 提供面向切面編程支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 用于構建Web,包括RESTful和基于Servlet的Web應用,包含了Spring MVC、Tomcat等 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 通過注解減少樣板代碼的Java庫,自動生成getter、setter等方法 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Swagger的注解庫,允許開發者為API添加文檔和元數據 --> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.2</version> </dependency> <!-- 用于Java對象的JSON序列化/反序列化的庫,Fastjson的繼任者 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.41</version> </dependency> <!-- 為Spring Boot應用提供了測試所需的依賴項,包括JUnit等,但僅限于測試階段 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <!-- 排除已包含的SLF4J API版本,避免版本沖突 --> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <!-- Java工具包,提供了許多實用的工具類和方法 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency>
</dependencies>
2. 自定義注解和實體類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface ApiOprLogAnno {@ApiModelProperty(value = "接口類型")String apiType() default "";@ApiModelProperty(value = "接口說明")String apiDetail() default "";@ApiModelProperty(value = "是否保存請求參數")boolean isSaveRequest() default false;@ApiModelProperty(value = "是否保存響應結果")boolean isSaveResponse() default false;}
@Setter
@Getter
public class ApiOprLog {@ApiModelProperty(name = "主鍵")private String id;@ApiModelProperty(name = "源IP")private String sourceIp;@ApiModelProperty(name = "用戶名")private String username;@ApiModelProperty(name = "方法")private String method;@ApiModelProperty(name = "請求參數")private String reqParams;@ApiModelProperty(name = "響應結果")private String resResult;@ApiModelProperty(name = "異常信息")private String exMessage;@ApiModelProperty(name = "異常詳細")private String exJson;@ApiModelProperty(name = "接口模塊")private String apiModule;@ApiModelProperty(name = "接口類型")private String apiType;@ApiModelProperty(name = "接口說明")private String apiDetail;@ApiModelProperty(name = "創建時間")private Date createTime;@ApiModelProperty(name = "更新時間")private Date updateTime;
}
3. 創建切面類
@Slf4j
@Aspect
@Component
public class ApiOprAspect {@Value("${spring.application.name}")private String moduleName;/*** 從請求中獲取 IP** @return IP*/private static String getIpFromRequest() {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);return IpUtil.getRealIp(request);}return Constants.UNKNOWN;}@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")public void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {String id = IdUtil.fastSimpleUUID();Object result;try {// 執行方法前操作executeBefore(proceedingJoinPoint, id);result = proceedingJoinPoint.proceed();// 執行方法后操作executeAfter(proceedingJoinPoint, id, result);} catch (Throwable ex) {// 執行方法異常后操作executeAfterEx(ex, id);throw ex;}return result;}private void executeBefore(ProceedingJoinPoint proceedingJoinPoint, String id) {// 獲取目標方法的簽名信息MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();// 從方法簽名中獲取 ApiOprLogAnno 注解的信息ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);// 封裝 ApiOprLog 對象ApiOprLog apiOprLog = packaging(id, getIpFromRequest(), signature.toString(), apiOprLogAnno);if (apiOprLogAnno.isSaveRequest()) {// 保存請求參數// 獲取方法簽名的參數名數組String[] parameterNames = signature.getParameterNames();// 獲取連接點傳遞的實參數組 Object[] args = proceedingJoinPoint.getArgs();Map<String, Object> paramMap = new HashMap<>(parameterNames.length);for (int i = 0; i < parameterNames.length; i++) {if (!RequestAttributes.REFERENCE_REQUEST.equals(parameterNames[i])) {paramMap.put(parameterNames[i], args[i]);}}apiOprLog.setReqParams(JSON.toJSONString(paramMap));}// 入庫操作log.debug("executeBefore apiOprLog: {}", JSON.toJSONString(apiOprLog));}private void executeAfter(ProceedingJoinPoint proceedingJoinPoint, String id, Object result) {// 獲取目標方法的簽名信息MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();// 從方法簽名中獲取 ApiOprLogAnno 注解的信息ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);if (!apiOprLogAnno.isSaveResponse()) {return;}ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setResResult(JSON.toJSONString(result));apiOprLog.setUpdateTime(DateTime.now());// 入庫操作log.debug("executeAfter apiOprLog: {}", JSON.toJSONString(apiOprLog));}private void executeAfterEx(Throwable ex, String id) {ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setExMessage(ex.toString());apiOprLog.setExJson(ExceptionUtil.stacktraceToString(ex));apiOprLog.setUpdateTime(DateTime.now());// 入庫操作log.debug("executeAfterEx apiOprLog: {}", JSON.toJSONString(apiOprLog));}/*** 封裝 ApiOprLog** @param id 主鍵* @param sourceIp IP* @param method 方法* @param apiOprLogAnno 注解* @return 接口操作日志對象*/private ApiOprLog packaging(String id,String sourceIp,String method,ApiOprLogAnno apiOprLogAnno) {ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setSourceIp(sourceIp);apiOprLog.setUsername("Regexp");apiOprLog.setMethod(method);apiOprLog.setApiModule(moduleName);apiOprLog.setApiType(apiOprLogAnno.apiType());apiOprLog.setApiDetail(apiOprLogAnno.apiDetail());apiOprLog.setCreateTime(DateTime.now());return apiOprLog;}
}
4. 進行測試
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/get")@ApiOprLogAnno(apiType = "查詢", apiDetail = "查詢單個用戶", isSaveResponse = true)public Person get() {return new Person("Regexp", 18);}@PostMapping("/save")@ApiOprLogAnno(apiType = "保存", apiDetail = "保存單個用戶", isSaveRequest = true)public String save(@RequestBody Person person) {log.debug("save person: {}", JSON.toJSONString(person));return "ok";}@GetMapping("/getEx")@ApiOprLogAnno(apiType = "查詢", apiDetail = "查詢單個用戶(異常情況)")public Person getEx() {throw new IllegalArgumentException();}
}
三、問題記錄
1. 引用不是注解類型
描述
啟動項目時,報錯如下:
Caused by: java.lang.IllegalArgumentException: error Type referred to is not an annotation type: cn$regexp$dailylearningtest$anno$ApiOprLog
分析
從報錯信息來看,顯示為:錯誤的類型,引用的不是一個注解類型。
Ctrl + Shift + F 全局搜索 ApiOprLog,看看哪些地方有用到 ApiOprLog。
經過搜索,發現在@annotation
中引用了 ApiOprLog(注解重命名后,這里忘記改了),但 ApiOprLog 并不是注解類型,所以導致啟動項目時,Spring找到了這個類但這個類卻不是注解,就報了這個錯。
@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLog)")
public void pointcut(){}
將 ApiOprLog 修改為正確的注解名稱即可。
@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")
public void pointcut(){}
2. 依賴沖突
描述
SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found
binding in
[jar:file:/D:/OpenSource/maven-repository/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in
[jar:file:/D:/OpenSource/maven-repository/org/slf4j/slf4j-reload4j/1.7.36/slf4j-reload4j-1.7.36.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an
explanation. SLF4J: Actual binding is of type
[ch.qos.logback.classic.util.ContextSelectorStaticBinder]
分析
從以上信息來看,應該是發生了依賴沖突導致的。
在控制臺輸入 mvn dependency:tree
查看項目中所有使用的依賴以及依賴中引用的依賴,查找哪些依賴使用了 slf4j,在其中一個依賴中使用exclusions
進行排除即可,如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion></exclusions></dependency>