通常为了监控系统,我们希望将请求的入参和出参记录到数据库中,已备后查。除了在每个方法里面加日志处理代码,手动保存到数据库,还有其他的办法吗?
今天就给大伙介绍一个注解 @Log2DB
什么?没这个注解?是的,这个注解是自定义的,那用这个注解能干什么呢?怎么写一个自己的注解呢?今天我们一起来探讨下。
首先,我们随便找个注解,看下它的结构
@RestController
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
是个interface,但是又加了一个@符号,那好吧,我们也搞一个这个。
@Target({ElementType.METHOD, ElementType.TYPE})//注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上
@Retention(RetentionPolicy.RUNTIME)//注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段SOURCE、类加载阶段CLASS,或运行阶段RUNTIME
@Documented//注解信息会被添加到Java文档中
public @interface Log2DB {
String name() default "接口";
boolean enabled() default true;//是否启用,默认是
}
注解是加进去了,怎么用呢?
@Api("testController相关操作")
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@Log2DB(name = "获取用户")
@GetMapping("get")
@ApiOperation("获取用户1")
public String getUser(@RequestParam("userId")String userId){
log.info("get is called");
return "get is called";
}
@Log2DB(name = "获取用户")
@PostMapping("post")
@ApiOperation("保存用户1")
public String postUser(@RequestBody Map request){
log.info("post is called");
return "post is called";
}
}
这就加上去了,但不是说好了保存到数据库吗?我们用面向切面把
@Log2DB
这个注解拦截下来,定义一个aspect
@Aspect
@Configuration
@Slf4j
public class Log2DBAspect {
//拦截哪个包下的类
//拦截哪个注解com.example.lesson20.annotation.Log2DB
@Pointcut("execution(public * *(..)) && @annotation(com.example.lesson20.annotation.Log2DB)")
public void logs(){
log.info("@Pointcut");
}
@Around("logs()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("@Around");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Log2DB logAnnotation = signature.getMethod().getAnnotation(Log2DB.class);
//如果不需要记录到数据库,继续业务调用并返回
if(!logAnnotation.enabled()){
return joinPoint.proceed();
}
//定义需要从request和response中获取的信息 begin
String name = "",//请求的服务名称
url="",//请求服务地址
method="",//请求方法 get post put
clientIp="",//客户端ip
browser="",//浏览器
clazz_method="",//请求的类和方法名
headers="", //head头信息
request="";//请求body入参
Object response=null;//返回参数
Date bgn,end;//请求开始结束时间
Boolean success = true;//是否成功
long cost = 0;//耗时 毫秒
//定义需要从request和response中获取的信息 end
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes==null){
return joinPoint.proceed();
}
HttpServletRequest httpServletRequest = attributes.getRequest();
url = httpServletRequest.getRequestURL().toString();
method = httpServletRequest.getMethod();
clientIp =getClientIp(httpServletRequest);
clazz_method =joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName();
name = logAnnotation.name();
//获取url后面的参数串
StringBuffer params = new StringBuffer();
for (String item : httpServletRequest.getParameterMap().keySet()) {
params.append(item).append("=").append(httpServletRequest.getParameter(item)).append("&");
}
String tmp = params.toString();
if(StringUtils.hasText(tmp)){
tmp = tmp.substring(0,tmp.length()-1);
url = url+"?"+tmp;
}
//获取header头信息
List<Map<String,String>> headerList = new ArrayList<>();
Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = httpServletRequest.getHeader(key);
Map<String,String> map = new HashMap<String,String>();
map.put(key,value);
headerList.add(map);
if("user-agent".equals(key.toLowerCase())){
browser = value;
}
}
headers = new ObjectMapper().writeValueAsString(headerList);
request = new ObjectMapper().writeValueAsString(joinPoint.getArgs());
//记录起始时间
bgn = new Date();
/** 执行目标方法 */
try{
response= joinPoint.proceed();
log.info("response={}",response==null?"": new ObjectMapper().writeValueAsString(response));
}
catch(Exception e){
success=false;
response = e.getMessage();
log.error("errorMessage: {}", e.getMessage());
e.printStackTrace();
throw e;
}
finally{
end = new Date();
/** 记录操作时间 */
cost = (end.getTime() - bgn.getTime());
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
log.info("name={}",name);
log.info("url={}",url);
log.info("method={}",method);
log.info("ip={}",clientIp);
log.info("browser={}",browser);
log.info("class_method={}",clazz_method);
log.info("header={}",headers);
log.info("request={}",request);
log.info("response={}",response);
log.info("success={}",success);
log.info("bgn={}",format.format(bgn));
log.info("end={}",format.format(end));
log.info("cost={}", cost);
//TODO: 保存到数据库 这里大家伙自行sql保存到数据库,我就不参合了
//不过我建议这个地方可以考虑先放到mq 消息队列中,然后慢慢消费,不要占用mysql的资源。
// 或者日志就存储到mongodb中也不失为一个好办法
}
return response;
}
@Before("logs()")
public void doBefore(JoinPoint point){
log.info("@Before");
}
@After("logs()")
public void doAfter(){
log.info("@After");
}
//在doAfter之后执行,主要用户记录程序执行后的返回值
@AfterReturning(returning="object",pointcut="logs()")
public void doAfterReturning(Object object){
log.info("@AfterReturning");
}
//获取客户端ip
private String getClientIp(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;
}
}
类有点长,我就不给大家一一解释了,里面注释都很详细,大家直接
copy paste
就可以用了,最后我们来验证下是不是这些参数都拿到了,postman请求一次,看看后台打印日志
@Around
@Before
post is called
@AfterReturning
@After
response="post is called"
name=保存用户1
url=http://localhost:8080/test/post?userId=abc
method=POST
ip=0:0:0:0:0:0:0:1
browser=PostmanRuntime/7.26.8
class_method=com.example.lesson20.controller.TestController.postUser
header=[{"content-type":"application/json"},{"user-agent":"PostmanRuntime/7.26.8"},{"accept":"*/*"},{"cache-control":"no-cache"},{"postman-token":"b3f49862-8b4e-4c7a-b817-3ff4f4e19336"},{"host":"localhost:8080"},{"accept-encoding":"gzip, deflate, br"},{"connection":"keep-alive"},{"content-length":"47"}]
request=[{"username":"abc","password":123}]
response=post is called
success=true
bgn=2021-07-03 10:00:25:183
end=2021-07-03 10:00:25:214
cost=31
嗯,是不错。但是方法的中文名字在Log2DB
和ApiOperation
中都要写一次,有点麻烦。下一期我们一起来解决这个问题。