架构师之路(二)
今天,我们继续来讲一个平时司空见惯,但其实很有讲究的内容,那就是:接口打日志。
打日志可不是随便打打的。日志打得不好,不仅不便于问题排查,甚至会拖垮机器,造成线上故障。
我的第一任老板当时就和我们这些初出茅庐的开发说:看一个人技术好不好,你就看他打日志就行。
这么多年工作过来,真是越来越深刻地体会到这句话的正确性。
今天我们就来聊聊“打日志”。怎么聊呢?直接聊很枯燥,我们就结合具体的代码例子来看吧。
package com.czh.controller;
import com.czh.aop.annotation.Anonymous;
import com.czh.aop.annotation.SystemLog;
import com.czh.design.login.LoginContext;
import com.czh.enums.LoginEnum;
import com.czh.service.LoginService;
import com.czh.utils.ip.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassNAME LoginController
* @Description TODO
* @Author czh
* @Date 2024/5/16 09:32
* @Version 1.0
*/
@RestController
@Anonymous
@Slf4j
public class LoginController {
@Autowired
private LoginContext loginContext;
/**
* 后台登录
* @param userName
* @param password
*/
@PostMapping("login")
public void login(HttpServletRequest request,String userName, String password){
log.info("=================start================");
log.info("入口功能:后台登录");
log.info("IP:" + IpUtils.getIpAddr(request));
log.info("入口参数=" + userName, password);
loginContext.login(LoginEnum.ADMIN,userName,password);
log.info("=================end================");
}
/**
* app登录
* @param token
*
*/
@PostMapping("appLogin")
public void appLogin(HttpServletRequest request,String token){
log.info("=================start================");
log.info("入口功能:后台登录");
log.info("IP:" + IpUtils.getIpAddr(request));
log.info("入口参数=" + token);
loginContext.login(LoginEnum.APP,token);
log.info("=================end================");
}
/**
* 支付宝登录
* @param token
*
*/
@PostMapping("zfbLogin")
public void zfbLogin(String token){
loginContext.login(LoginEnum.ZFB,token);
}
}
可以看到日志打印是没有问题的
你觉得这样打印日志ok吗?
问题一:每个接口都要写这种重复的代码
问题二:日志代码影响业务代码的可读性
看了有问题的代码,下一步我们要做的就是改造它!这种日志不相关业务系统我们就可以用到spring Aop的加强机制
直接上代码:
首先定义一个自定义日志注解
package com.czh.aop.annotation;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
*
* @author czh
*
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public String content() default "";
}
写入切面
package com.czh.aop.aspectj;
import com.alibaba.fastjson.JSON;
import com.czh.aop.annotation.SystemLog;
import com.czh.utils.ip.IpUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.ThreadContext;
import org.apache.tomcat.util.net.IPv6Utils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* 自定义操作日志记录注解
*
* @author czh
*/
@Component
@Aspect
public class SystemLogAspect {
private static final Logger log = LoggerFactory.getLogger(SystemLogAspect.class);
/**
* 计算操作消耗时间
*/
private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
/**
* 设置操作日志切入点,这里介绍两种方式:
* 1、基于注解切入(也就是打了自定义注解的方法才会切入)
*
* @Pointcut("@annotation(com.czh.aop.annotation.SystemLog)") 2、基于包扫描切入
* @Pointcut("@annotation(com.czh.aop.annotation.SystemLog)")
*/
@Pointcut("@annotation(com.czh.aop.annotation.SystemLog)")//在注解的位置切入代码
//@Pointcut("execution(public * org.wujiangbo.controller..*.*(..))")//从controller切入
public void operLogPoinCut() {
}
/**
* 处理请求前执行
*/
@Before("operLogPoinCut()")
public void boBefore(JoinPoint joinPoint) {
//为了记录方法的执行时间
TIME_THREADLOCAL.set(System.currentTimeMillis());
ThreadContext.put("traceId", UUID.randomUUID().toString());
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
SystemLog myLog = method.getAnnotation(SystemLog.class);
ThreadContext.put("traceId", UUID.randomUUID().toString());
// 将入参转换成json
String params = argsArrayToString(joinPoint.getArgs());
log.info("=================start================");
log.info("入口模块:" + myLog.title());
log.info("入口功能:" + myLog.content());
log.info("IP:" + IpUtils.getIpAddr(request));
log.info("入口参数=" + params);
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp);
exp.printStackTrace();
}
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(value = "operLogPoinCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
{
try {
// 设置消耗时间
log.info("消耗时间(ms)" + String.valueOf(System.currentTimeMillis() - TIME_THREADLOCAL.get()));
log.info("=================end============");
} finally {
TIME_THREADLOCAL.remove();
}
}
/**
* @param: 参数paramsArray
* @return: String
* @description:拼接参数
* @author: czh
* @date: 2023/12/24 21:16
**/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (o != null) {
try {
Object jsonObj = JSON.toJSON(o);
params += jsonObj.toString() + " ";
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return params.trim();
}
}
最新的代码使用在需要的方法上面加上注解即可
package com.czh.controller;
import com.czh.aop.annotation.Anonymous;
import com.czh.aop.annotation.SystemLog;
import com.czh.design.login.LoginContext;
import com.czh.enums.LoginEnum;
import com.czh.service.LoginService;
import com.czh.utils.ip.IpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @ClassNAME LoginController
* @Description TODO
* @Author czh
* @Date 2024/5/16 09:32
* @Version 1.0
*/
@RestController
@Anonymous
@Slf4j
public class LoginController {
@Autowired
private LoginContext loginContext;
/**
* 后台登录
* @param userName
* @param password
*/
@PostMapping("login")
public void login(HttpServletRequest request,String userName, String password){
log.info("=================start================");
log.info("入口功能:后台登录");
log.info("IP:" + IpUtils.getIpAddr(request));
log.info("入口参数=" + userName, password);
loginContext.login(LoginEnum.ADMIN,userName,password);
log.info("=================end================");
}
/**
* app登录
* @param token
*
*/
@PostMapping("appLogin")
public void appLogin(HttpServletRequest request,String token){
log.info("=================start================");
log.info("入口功能:后台登录");
log.info("IP:" + IpUtils.getIpAddr(request));
log.info("入口参数=" + token);
loginContext.login(LoginEnum.APP,token);
log.info("=================end================");
}
/**
* 支付宝登录
* @param token
*
*/
@PostMapping("zfbLogin")
@SystemLog(title = "支付宝登录",content = "支付宝登录")
public void zfbLogin(String token){
loginContext.login(LoginEnum.ZFB,token);
}
}
好了到此结束了如果对你有用请点一下赞!
评论区