阅读本文可能会解决的问题:
① AOP简单了解
② AOP实现日志管理
③ springboot+logback整合
④ logback日志入库+文件记录
⑤ ...
七月份的尾巴,巴拉巴拉,最近很忙每天11点下班已经成了常态,刚看了个开头的《三体》也搁一边无暇顾及,有时候中午赶着出来吃个饭又屁颠回去加班干活。一到深夜就在想这样一种生活状态是不是算病态,因为感觉完全不合理。但是面对代码的时候又很兴奋,一开干就根本停下来,惶惶终日却又自我满足,实在是无奈...
不废话了说主题,最近项目需要一个日志功能,也是大家实在无法忍受没log调试的痛苦遂将此重任交给我,还是springboot还是jpa,下面就说下和logback的整合和我遇到的一些坑。
看回复大家貌似不太清楚应用的场景,简单说下:
AOP方式用来实现日志的持久化(通常记录敏感操作,比如:交易、充值,删除操作等,解决的问题是谁在何时删除、充值了的问题,一般用来记录用户的操作轨迹)
Logback用来本地记录日志文件(通常用来记录系统的运行及调试时的日志,方便查找回溯问题,解决的是系统在何时产生了什么错误、异常的问题),同时Logback也支持持久化,可以将系统产生的Exception保存到数据库方便查看
二者分工明确,共同协调才能达到系统日志的健壮性。
Logback是一定需要的,本文是做了更加深度的配置,使得日志可以分级(error、info等)同时分目录保存到本地形成log文件,并且将指定级别的日志保存到数据库
Aop形式看业务需求,一般一个健壮的系统一定是有这样的日志管理的,将持久化的日志在前台显示出来,可以清晰的看到某人在何时操作了什么,形成一个用户的操作轨迹方便数据分析
环境:
Springboot + jpa + logback
一、什么是AOP
其实老早就在用但是一直没怎么实际接触过,像shiro就是典型的AOP实现。通过注解实现全局拦截处理在实际的应用中很常见。
对于AOP只要知道这几点就好:
1,spring最核心的两个功能是aop和ioc,即面向切面,控制反转。
2,aop的实现是:通过切入点将切面注入业务
3,通俗说:很多业务叠在一起,需要一个针对几乎全部业务统一处理的这样一个过程,aop就是将业务一刀切开,在业务侧面引入统一处理机制。
所以说日志系统非常符合aop这样一种设计方式,另外还有权限系统包括token校验等也同样适用。
二、AOP实现记录日志
下面的代码部分参考了这里,在其基础上增加和优化了一些内容,具体看后面说明
1,首先我们要知道的是通过aop方式进行日志处理我们需要建立自定义注解,因为最终我们是通过在方法和类上加入注解实现面向切面编程的日志操作。
(1) 先定义@LogEvent注解:
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface LogEvent {
ModuleType module() default ModuleType.DEFAULT; // 日志所属的模块
EventType event() default EventType.DEFAULT; // 日志事件类型
String infos() default ""; // 描述信息
}
使用:在类或方法上加入 @LogEvent(module = ModuleType.DEFAULT)
示例:
@LogEvent(module = ModuleType.DEFAULT, event = EventType.ADD, infos = "为用户充值")
注解引申:该注解需要定义内容:日志模块、日志事件、描述信息,这三项在类和方法上非必须,可省略,最终在存库时会参考这三个参数以最为日志内容记录,所以建议别怕麻烦都写上,需要注意的是类上加入此注解是为在方法上加入此注解提供默认参考值,即在方法上此注解未加入参数则会自动获取类上的该注解内容以后续保存。以下为该注解内容
① ModuleType模块类型为枚举型,可以根据实际业务增加业务模块
/**
* 模块类型
*/
public enum ModuleType {
DEFAULT("默认值"), // 默认值
CONTACTS("通讯录"),// 通讯录模块
private ModuleType(String index){
this.module = index;
}
private String module;
public String getModule(){
return this.module;
}
}
② EventType事件类型为枚举型,可以根据实际业务增加事件类型
/**
* 事件类型
*/
public enum EventType {
DEFAULT("默认", "default"), ADD("添加", "add"), FIND("查询", "find"), UPDATE("更新", "update"), DELETE_SINGLE("删除", "delete-single"),
LOGIN("登录","login"),LOGIN_OUT("注销","login_out");
private EventType(String index, String name){
this.name = name;
this.event = index;
}
private String event;
private String name;
public String getEvent(){
return this.event;
}
public String getName() {
return name;
}
}
③ infos描述信息类型为字符串,用以定义方法/类完成的具体业务内容,默认为空。
(2) 定义@LogEnable注解:
import java.lang.annotation.*;
/**
* 日志开关
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogEnable {
/**
* 如果为true,则类下面的LogEvent启作用,否则忽略
* @return
*/
boolean logEnable() default true;
}
使用:在类上加入此注解@LogEnable以开启@LogEvent注解生效,因此,此注解为总开关,决定是否记录日志
示例:加在类上即可,需要在@LogEvent注解之前
(3)定义@LogKey注解
import java.lang.annotation.*;
/**
* 此注解可以注解到成员变量,也可以注解到方法的参数上
*/
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogKey {
String keyName() default ""; // key的名称
boolean isUserId() default false; // 此字段是否是本次操作的userId,这里略
boolean isLog() default true; // 是否加入到日志中
boolean isClass() default false; // 是否类对象
}
使用:在方法的参数或者成员变量上加入此注解@LogKey
示例1(参数为对象):
@RequestMapping(value="/getToken",method= RequestMethod.POST, produces="application/json;charset=UTF-8")
@ResponseBody
@LogEvent(module = ModuleType.TOKEN, event = EventType.getToken, infos = "getToken") // 添加日志标识
public Map<String , String> getToken(@LogKey(keyName = "user", isClass = true) UserInfo user){
...
}
注解引申:该注解需要定义内容:参数名称keyName、参数是否加入日志isLog、参数是否为类对象isClass,这三项可根据自己的方法参数自己改动。原博中说这个注解在类上可用是错误的,请大家注意。还有在此注解中我加入了isClass参数,用以判断输入参数是否为对象,为true则会序列化此对象最终存库。
示例2(参数为非对象)
@LogEvent(module = ModuleType.DEFAULT, event = EventType.ADD, infos = "为用户充值")
public int chargeMoney(@LogKey(keyName = "sum") double sum,
@LogKey(keyName = "remarks") String remarks,
@LogKey(keyName = "updateDate") Date updateDate,
@LogKey(keyName = "uid") String uid,
@LogKey(keyName = "price") double price) {
// TODO Auto-generated method stub
...
}
至此注解部分全部完成了,接下来需要完成aop的切面实现逻辑
2,实现AOP
(1)首先为了方便后续对日志操作,需要一个日志实体(节省文本已省略get/set,自行添加)
/**
* 日志信息类
*/
@Table(name="sys_log")
@Entity
public class LogAdmModel {
@Id
@GeneratedValue
private Long id;
private String userId; // 操作用户
private String userName;
private String admModel; // 模块
private String admEvent; // 操作
private Date createDate = new Date(); //创建时间
@Lob
private String admOptContent; // 操作内容
private String targetMethod; // 目标方法名
private String targetClass; // 目标方法所属类的类名
private String errorMsg; // 错误信息
private String infos; // 备注
}
注意:操作内容admOptContent字段我设置为大文本,因为后续参数增多以及对象序列化内容过大会导致存不下报错,所以该字段加入@Lob注解,后续我实例化存库所以都有指定表明和标注实体类注解。
(2)对应的我们需要完成Service、ServiceImpl、Dao的代码(持久化我使用Jpa)
① LogManager
import com.base.entity.LogAdmModel;
/**
* 日志处理模块
* 1. 可以将日志存入数据
* 2. 可以将日志发送到开中间件,如果redis, mq等等
*/
public interface LogManager {
/**
* 日志处理模块
* @param paramLogAdmBean
*/
void dealLog(LogAdmModel paramLogAdmBean);
/**
* 打印异常信息
* @param e
*/
String printException(Exception e);
}
包括处理日志接口和打印异常接口(打印异常接口非必须,写这个是为了后面我方便操作Exception)
② LogManagerImpl