项目需求
**需求1: **web项目一般而言都需要日志记录,比较常用的是实用log4j来记录项目的异常日志,将日志单独存储于文件当中,这样有利于我们快速进行bug 排解。
**需求2: **异常的记录一般就是将异常的堆栈保存在文件中,这样文件大小会急剧上升,有效异常信息也不能被立即定位,有没有一种方式可以可以让我们重写异常记录,并保存在异常日志文件当中呢。
**需求3: **在异常日志之上,我们一般还需要对系统中各角色的各个重要操作进行一些日志记录,以方便我们找到操作人,以及责任人。
福利彩蛋
针对1中的需求,我们大家熟悉的是实用log4j进行日志记录。配置简单,实现快速。
针对2,3中的需求我们一般采用的是基于拦截器实现(Aop思想的一种实现方式)在方法操作之前进行一定的处理,获取操作人、操作方法名、操作参数,异常捕获与记录,这样实现也是完全可以的。
今天记录的是基于自定义注解和面向切面(AOP)进行统一操作日志以及异常日志记录的实现。
项目代码
项目中代码如下所示:
1.首先定义两个注解:分别为SystemControllerLog(用于拦截Controller层操作注解,起切点表达式作用,明确切面应该从哪里注入),SystemServiceLog(用于拦截Service层操作注解,起切点表达式作用,明确切面应该从哪里注入)
这两个注解在切面中定义切点表达式的时候会用到。
SystemControllerLog.java
package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;
/**
* Created by mark on 16/11/25.
* @usage 自定义注解,拦截Controller
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemControllerLog {
String description() default "";
}
SystemServiceLog
package com.fxmms.common.log.logannotation;
import java.lang.annotation.*;
/**
* Created by mark on 16/11/25.
* @usage 自定义注解 拦截service
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog {
String description() default "";
}
2.接下来定义切面类,这里面主要定义了几个通知,在调用被代理对象目标方法前后,或目标方法抛出异常之后使用。
package com.fxmms.common.log.logaspect;
import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.common.log.logannotation.SystemServiceLog;
import com.fxmms.common.security.ScottSecurityUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 切点类
* @author tiangai
* @since 2014-08-05 Pm 20:35
* @version 1.0
*/
@Aspect
@Component
public class SystemLogAspect {
//注入Service用于把日志保存数据库 nodo service 层实现
//本地异常日志记录对象
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
/**
* Service层切点 使用到了我们定义的 SystemServiceLog 作为切点表达式。
* 而且我们可以看出此表达式基于 annotation。
*
*/
@Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemServiceLog)")
public void serviceAspect() {
}
/**
* Controller层切点 使用到了我们定义的 SystemControllerLog 作为切点表达式。
* 而且我们可以看出此表达式是基于 annotation 的。
*/
@Pointcut("@annotation(com.fxmms.common.log.logannotation.SystemControllerLog)")
public void controllerAspect() {
}
/**
* 前置通知 用于拦截Controller层记录用户的操作
*
* @param joinPoint 连接点
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//请求的IP
String ip = request.getRemoteAddr();
System.out.println(ip+"sdsdsdsdsd");
try {
//控制台输出
System.out.println("=====前置通知开始=====");
Object object = joinPoint.getTarget();
System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("方法描述:" + getControllerMethodDescription(joinPoint));
System.out.println("请求人:" + ScottSecurityUtil.getLoginName());
System.out.println("请求IP:" + ip);
//构造数据库日志对象
//保存数据库
System.out.println("=====前置通知结束=====");
} catch (Exception e) {
//记录本地异常日志
logger.error("==前置通知异常==");
logger.error("异常信息:{}", e.getMessage());
}
}
/**
* 异常通知 用于拦截service层记录异常日志
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "serviceAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取请求ip
String ip = request.getRemoteAddr();
//获取用户请求方法的参数并组织成字符串
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
params += joinPoint.getArgs()[i]+ ",";
}
}
try {
//控制台输出
System.out.println("=====异常通知开始=====");
System.out.println("异常代码:" + e.getClass().getName());
System.out.println("异常信息:" + e.getMessage());
System.out.println("异常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));
System.out.println("方法描述:" + getServiceMthodDescription(joinPoint));
System.out.println("请求人:" + ScottSecurityUtil.getLoginName());
System.out.println("请求IP:" + ip);
System.out.println("请求参数:" + params);
//构造数据库日志对象
//保存数据库
System.out.println("=====异常通知结束=====");
} catch (Exception ex) {
//记录本地异常日志
logger.error("==异常通知异常==");
logger.error("异常信息:{}", ex);
}
//录本地异常日志
logger.error("异常方法:{}异常代码:{}异常信息:{}参数:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);
}
/**
* 获取注解中对方法的描述信息 用于service层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static String getServiceMthodDescription(JoinPoint joinPoint)
throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SystemServiceLog.class).description();
break;
}
}
}
return description;
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SystemControllerLog.class).description();
break;
}
}
}
return description;
}
}
上面的切面类中定义了公共切点 serviceAspect、serviceAspect,并实现了Controller层的前置通知,Service业务逻辑层的异常通知。
其中预留了保存日志到数据库的代码段,我们可以根据业务自行进行填充。
3.创建好切点、切面类之后,如何让他起作用呢,我们需要在配置文件中进行配置了。我将web项目中关于不同层的配置文件进行的切割,数据访问层配置文件是data-access.xml、业务逻辑层是service-application.xml、控制层是defalut-servlet.xml
首先看defalut-servlet.xml中的配置文件:
注意:以上配置有两个重要的点:
1.项,
2.
proxy-target-class="true"默认是false,更改为true时使用的是cglib动态代理。这样只能实现对Controller层的日志记录。
service-application.xml配置AOP,实现对Service层的日志记录.
这样Service也是可以实现操作、异常日志记录了。
4.在代码中使用自定义注解,相当于在目标方法上设置了一个切点,通过切点注入切面。
Controller层上运用SystemControllerLog注解:
TestNullPointExceptionController.java(验证Controller层中异常,Controller中调用Service层代码)
package com.fxmms.www.controller.admin;
import com.fxmms.common.log.logannotation.SystemControllerLog;
import com.fxmms.www.service.TestExceptionLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Created by mark on 16/11/25.
*/
@Controller
public class TestNullPointExceptionController {
private static Log logger = LogFactory.getLog(TestNullPointExceptionController.class);
//自动注入一个Service层类对象
@Autowired
TestExceptionLog testExceptionLog;
@ResponseBody
@RequestMapping("/admin/testexcption")
@SystemControllerLog(description = "testException")//使用 SystemControllerLog注解,此为切点
public String testException(String str){
return testExceptionLog.equalStr(str);
}
}
** TestExceptionLog.java**
package com.fxmms.www.service;
import com.fxmms.common.log.logannotation.SystemServiceLog;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;
/**
* Created by mark on 16/11/25.
*/
@Service
public class TestExceptionLog {
private static Log logger = LogFactory.getLog(TestExceptionLog.class);
@SystemServiceLog(description = "equalstr")
public String equalStr(String str) {
str = null;
if (str.equals("sd")) {
return "sd";
}else {
return "sd";
}
}
}
我在其中手动设置str = null,用于模拟前台输入。
程序在运行时会报运行时异常。
最终启动项目项目console日志输出如下图所示:
这样就完成了自定义注解&Aop&自定义异常&操作日志的记录,而且自定义的注解与切面可以进行重用,操作日志与异常日志可以进行数据库记录,后期甚至可以做一个关于异常分析的系统,我们可以直接从日志后台系统中查看异常出现的频率,以及定位异常的发声位置,明确操作人等。
完。
福利彩蛋
职位:腾讯OMG 广告后台高级开发工程师;
Base:深圳;
场景:海量数据,To B,To C,场景极具挑战性。
基础要求:
熟悉常用数据结构与算法;
熟悉常用网络协议,熟悉网络编程;
熟悉操作系统,有线上排查问题经验;
熟悉MySQL,oracle;
熟悉JAVA,GoLang,c++其中一种语言均可;
可内推,欢迎各位优秀开发道友私信[微笑]
期待关注我的开发小哥哥,小姐姐们私信我,机会很好,平台对标抖音,广告生态平台,类似Facebook 广告平台,希望你们用简历砸我~
联系方式 微信 13609184526
博客搬家:大坤的个人博客
欢迎评论哦~