大家好,我是一安,今天介绍一下项目开发中如何更好地实现系统日志记录
简介
审计日志,简单说就是系统需要记录谁,在什么时间,对什么数据,做了什么样的更改!这个日志数据是极其珍贵的,后面如果因业务操作上出了问题,可以很方便进行操作回查
Spring AOP的一些术语
-
切面(Aspect):在Spring AOP中,切面可以使用通用类或者在普通类中以
@Aspect
注解来实现 -
连接点(Joinpoint):在Spring AOP中一个连接点代表一个方法的执行
-
java.lang.Object[] getArgs()
:获取连接点方法运行时的入参列表 -
Signature getSignature()
:获取连接点的方法签名对象 -
java.lang.Object getTarget()
:获取连接点所在的目标对象 -
java.lang.Object getThis()
:获取代理对象本身 -
ProceedingJoinPoint:继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法
-
java.lang.Object proceed() throws java.lang.Throwable
:通过反射执行目标对象的连接点处的方法 -
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable
:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参 -
通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括”around”、”before”和”after”等通知。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链
-
前置通知(@Before):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常) -
返回后通知(@AfterReturning):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回 -
抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知 -
后通知(@After):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出) -
环绕通知(@Around):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型,环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行 -
切入点(Pointcut):定义出一个或一组方法,当执行这些方法时可产生通知,Spring缺省使用AspectJ切入点语法。
代码示例
自定义注解
1.日志扫描注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EPAroundLog {
//调用的方法操作view modify save remove
String desc() default "";
}
2.参数描述注解
/**
* Swagger扩展注解
* 用于application/json请求
* 并使用诸如Map或JSONObject等非具体实体类接收参数时,对参数进行进一步描述
*/
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiGlobalModel {
/**
* 字段集合容器
*
*/
Class<?> component();
/**
* 分隔符
*
*/
String separator() default ",";
/**
* 实际用到的字段
* 可以是字符串数组,也可以是一个字符串 多个字段以分隔符隔开: "id,name"
* 注意这里对应的是component里的属性名,但swagger显示的字段名实际是属性注解上的name
*
*/
String[] value() default {};
}
3.参数字典定义
小编这里只列举了一部分,实际开发自己可以扩充字典
/**
* @description: 字段集合容器
**/
@ApiModel("JSON参数Vo")
@Data
public class JsonParamVo implements Serializable {
/******************************/
@ApiModelProperty(value = "查询条件", name = "qc")
private String qc;
@ApiModelProperty(value ="页数", name = "page")
private Integer page ;
@ApiModelProperty(value ="条目数", name = "pageSize")
private Integer pageSize;
private HttpServletRequest request;
private HttpServletRequest response;
private MultipartFile file;
}
编写一个代理类
@Aspect
@Component
public class EPLogAspect {
private final static Logger logger = LoggerFactory.getLogger(EPLogAspect.class);
@Around("@annotation(com.vpdn.auth.annos.EPAroundLog)")
public Object outPutLog(ProceedingJoinPoint pjd) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//获取开始时间戳
Long startTime = System.currentTimeMillis();
//获取请求URL
String requestURL = request.getRequestURL().toString();
//获取操作描述信息
MethodSignature signature = (MethodSignature) pjd.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
EPAroundLog annotation = method.getAnnotation(EPAroundLog.class);//获得该注解
String desc = annotation.desc();//获得自定义注解上面的值
ApiGlobalModel apiGlobalModel = method.getAnnotation(ApiGlobalModel.class);//获得该注解
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String start = sdf.format(new Date());
//执行方法
Object result = null;
//执行方法是否异常
try {
result = pjd.proceed();
} catch (Exception e) {
logger.info("执行方法异常,{}", e);
}
JSONObject ret = (JSONObject) JSONObject.toJSON(result);
if (null == ret) ret = new JSONObject();
//获取请求结束时间
Date stopDate = new Date();
String stop = sdf.format(stopDate);
//处理完请求,返回内容
JSONObject jsonObject = new JSONObject();
//获取用户请求方法的参数并序列化为JSON格式字符串
switch (desc) {
case "view":
List params = getArgs(pjd.getArgs(), apiGlobalModel);
jsonObject.put("查询参数", params);
break;
case "save":
List param = getArgs(pjd.getArgs(), apiGlobalModel);
jsonObject.put("添加参数", param);
break;
case "modify":
List para = getArgs(pjd.getArgs(), apiGlobalModel);
jsonObject.put("修改参数", para);
break;
case "remove":
List par = getArgs(pjd.getArgs(), apiGlobalModel);
jsonObject.put("删除参数", par);
break;
}
//从request中获取授权id
String sessionId = request.getHeader("usid");
if (sessionId == null) {
sessionId = request.getParameter("dusid");
}
//调用公共方法获取当前登录用户
String uspid = "";
Map<String, String> currentHomeDepAndUsernameAndId = CommonTools.getCurrentHomeDepAndUsernameAndId(request);
if (currentHomeDepAndUsernameAndId != null && currentHomeDepAndUsernameAndId.size() > 0) {
//获取当前登录用户名
uspid = currentHomeDepAndUsernameAndId.get("username");
}
//操作子系统类型ftst|时间|操作码(操作类型)|操作请求时间|操作请求url|响应返回时间|详细描述|操作响应码|运行耗时(ms)|操作者|授权ID
logger.info("0102|" + startTime + "|" + desc + "|" + start + "|" + requestURL + "|" + stop + "|" + jsonObject + "|" + ret.get("msg") + "|" +
(stopDate.getTime() - startTime) + "|" + uspid + "|" + sessionId);
return result;
}
public List<Object> getArgs(Object[] args, ApiGlobalModel apiGlobalModel) throws IllegalAccessException {
List<Object> paramList = new ArrayList<>();
//apiGlobalModel注解的参数实体类
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
//HttpServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException:
continue;
}
//获取方法的所有参数values
String[] apiVals = null;
if (apiGlobalModel != null) {
apiVals = apiGlobalModel.value();
}
int index = 0;
//带@ApiGlobalModel 注解的参数翻译
if (apiVals != null && apiVals.length > 0) {
JSONObject argsJson = JSONObject.parseObject(JSON.toJSONString(args[i]));
String[] values = apiVals[0].split(",");
for (Map.Entry<String, Object> entry : argsJson.entrySet()) {
//获取参数类的属性
try {
Field field = apiGlobalModel.component().getDeclaredField(values[index]);
if (field.getName().equals(values[index])) {
index++;
ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
JSONObject addjson = new JSONObject();
String paramDescKey = apiModelProperty.value(); //含义
String name = apiModelProperty.name(); //获取参数key即json参数的key
addjson.put(paramDescKey, argsJson.get(name));
paramList.add(addjson);
}
} catch (Exception e) {
logger.info("获取参数实体类属性失败,{}", e);
}
}
} else {
//带注解的实体类型参数翻译
Class tClass = args[i].getClass();
if (tClass != null) {
Field[] declaredFields = tClass.getDeclaredFields();
JSONObject argsObj = new JSONObject();
for (int d = 0; d < declaredFields.length; d++) {
Field field = declaredFields[d];
field.setAccessible(true);
// 获取字段名称
// 获取指定对象的当前字段的值
Object fieldval = field.get(args[i]);
ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
if (apiModelProperty != null) {
String paramDescKey = apiModelProperty.value(); //含义
argsObj.put(paramDescKey, fieldval);
}
}
paramList.add(argsObj);
} else {
//不翻译的
Object argsObj = JSON.toJSON(args[i]);
paramList.add(argsObj);
}
}
}
}
return paramList;
}
}
编写测试类
@ApiOperation(value = "企业信息分页查询", notes = "qc参数可根据企业名称或域名查询企业",httpMethod="POST",response = Result.class)
@ApiImplicitParam(name = "usid",value = "usid" , dataType = "String", required = true,paramType="header")
@ApiGlobalModel(component = JsonParamVo.class, value = "qc,page,pageSize")
@PostMapping(value = "/view",produces = "application/json;charset=UTF-8")
@MustLogin
@EPAroundLog(desc = "view")
public Result view(@RequestBody JSONObject jsonParam, HttpServletRequest request, HttpServletResponse response) {
String qc = jsonParam.getString("qc");
Integer page = jsonParam.getInteger("page") == null ? 1 : jsonParam.getInteger("page");
Integer pageSize = jsonParam.getInteger("pageSize")== null ? 10 : jsonParam.getInteger("pageSize");
//需要对登录用户进行权限数据域判断,即本接口只有当前登录用户管理的企业域名查询权限
Map<String,String> currentMap = CommonTools.getCurrentHomeDepAndUsernameAndId(request);
return entService.view(currentMap,qc,page,pageSize,request) ;
}
验证
利用logback实现日志记录写入文件,最后通过实时采集日志入库ES
0102|1661581336483|view|2022-08-27 14:22:16.483|http://192.168.5.128:8302/onlineUser/view|2022-08-27 14:22:16.498|{"查询参数":[{}]}|成功|15|admin|20fc76a9394c416aa2cf034b8d4e9c21
0102|1661581336479|view|2022-08-27 14:22:16.479|http://192.168.5.128:8302/ent/view|2022-08-27 14:22:16.535|{"查询参数":[{"查询条件":"gre"},{"页数":1},{"条目数":10}]}|成功|56|admin|20fc76a9394c416aa2cf034b8d4e9c21
小结
整个程序的实现过程,主要使用了Spring AOP特性,对特定方法进行前、后拦截,从而实现业务方的需求。
号外!号外!
如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!
Spring Boot 实现跨域的 5 种方式,总有一种适合你
原文始发于微信公众号(一安未来):实际项目开发中如何完善系统日志记录
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44896.html