说明
系统日志不论是在日常的管理还是维护中都会起到很大的作用,但是在日志的记录中通常会存在很多的问题
- 日志记录的不规范性
- 日志记录的重复性
- 日志记录的难分类
目前日志主要记录的有三方面
- 请求的入参,出参
- 关于业务上的操作
- 异常日常日志的打印
解决方案
1.记录请求的出参入参
记录出参入参这是日志记录最好操作的一部分,而这里会存在一定的重复性,因为每个请求都需要记录,这是重复操作,完全可以使用Spring AOP进行入参和出参的记录
import org.apache.log4j.Logger;
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.springframework.core.annotation.Order;
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.util.Arrays;
/**
* Web层日志切面
*/
@Aspect //这里使用@Aspect注解方式设置AOP
@Order(5) //值越小,越先加载
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
//这里@Pointcut设置切点可以设置为Controller层的地址
@Pointcut("execution(public * com.training..*.*Controller(..))")
public void webLog(){}
//@Before指在切点方法之前执行,也就是在Controller层方法执行之前执行,这里可以通过JoinPoint获取一些有关方法的信息,在这里也可以修改参数的值
//@Before()括号里设置的是切点方法的名称
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
}
}
2.记录操作日志
在系统中可能会有很多的增删改查或是会涉及到一些业务操作,这时候我们需要记录一下操作的入口,在此还可以记录下操作的类型,或是业务的名称.不过在就操作日志前需要构建日志的基础部件
日志对象
import java.util.Date;
/**
* 操作日志
*/
public class OperationLog extends Base {
private static final long serialVersionUID = 1L;
/**
* 日志类型
*/
private String logtype;
/**
* 日志名称
*/
private String logname;
/**
* 用户id
*/
private Integer userid;
/**
* 类名称
*/
private String classname;
/**
* 方法名称
*/
private String method;
/**
* 创建时间
*/
private Date createtime;
/**
* 是否成功
*/
private String succeed;
/**
* 备注
*/
private String message;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLogtype() {
return logtype;
}
public void setLogtype(String logtype) {
this.logtype = logtype;
}
public String getLogname() {
return logname;
}
public void setLogname(String logname) {
this.logname = logname;
}
public Integer getUserid() {
return userid;
}
public void setUserid(Integer userid) {
this.userid = userid;
}
public String getClassname() {
return classname;
}
public void setClassname(String classname) {
this.classname = classname;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public String getSucceed() {
return succeed;
}
public void setSucceed(String succeed) {
this.succeed = succeed;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
日志对象创建工厂
import com.stylefeng.guns.common.constant.state.LogSucceed;
import com.stylefeng.guns.common.constant.state.LogType;
import com.stylefeng.guns.common.persistence.model.LoginLog;
import com.stylefeng.guns.common.persistence.model.OperationLog;
import java.util.Date;
/**
* 日志对象创建工厂
*/
public class LogFactory {
/**
* 创建操作日志
*/
public static OperationLog createOperationLog(LogType logType, Integer userId, String bussinessName, String clazzName, String methodName, String msg, LogSucceed succeed) {
OperationLog operationLog = new OperationLog();
operationLog.setLogtype(logType.getMessage());
operationLog.setLogname(bussinessName);
operationLog.setUserid(userId);
operationLog.setClassname(clazzName);
operationLog.setMethod(methodName);
operationLog.setCreatetime(new Date());
operationLog.setSucceed(succeed.getMessage());
operationLog.setMessage(msg);
return operationLog;
}
/**
* 创建登录日志
*/
public static LoginLog createLoginLog(LogType logType, Integer userId, String msg,String ip) {
LoginLog loginLog = new LoginLog();
loginLog.setLogname(logType.getMessage());
loginLog.setUserid(userId);
loginLog.setCreatetime(new Date());
loginLog.setSucceed(LogSucceed.SUCCESS.getMessage());
loginLog.setIp(ip);
loginLog.setMessage(msg);
return loginLog;
}
}
日志任务创建工厂
日志任务创建工厂的作用是将日志记录存储到数据库中
import com.stylefeng.guns.common.constant.state.LogSucceed;
import com.stylefeng.guns.common.constant.state.LogType;
import com.stylefeng.guns.common.persistence.dao.LoginLogMapper;
import com.stylefeng.guns.common.persistence.dao.OperationLogMapper;
import com.stylefeng.guns.common.persistence.model.LoginLog;
import com.stylefeng.guns.common.persistence.model.OperationLog;
import com.stylefeng.guns.core.log.LogManager;
import com.stylefeng.guns.core.util.SpringContextHolder;
import com.stylefeng.guns.core.util.ToolUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.TimerTask;
/**
* 日志操作任务创建工厂
*/
public class LogTaskFactory {
private static Logger logger = LoggerFactory.getLogger(LogManager.class);
//LoginLogMapper记录登录登出日志
private static LoginLogMapper loginLogMapper = SpringContextHolder.getBean(LoginLogMapper.class);
//OperationLogMapper记录操作日志
private static OperationLogMapper operationLogMapper = SpringContextHolder.getBean(OperationLogMapper.class);
public static TimerTask loginLog(final Integer userId, final String ip) {
return new TimerTask() {
@Override
public void run() {
try {
LoginLog loginLog = LogFactory.createLoginLog(LogType.LOGIN, userId, null, ip);
loginLogMapper.insert(loginLog);
} catch (Exception e) {
logger.error("创建登录日志异常!", e);
}
}
};
}
public static TimerTask loginLog(final String username, final String msg, final String ip) {
return new TimerTask() {
@Override
public void run() {
LoginLog loginLog = LogFactory.createLoginLog(
LogType.LOGIN_FAIL, null, "账号:" + username + "," + msg, ip);
try {
loginLogMapper.insert(loginLog);
} catch (Exception e) {
logger.error("创建登录失败异常!", e);
}
}
};
}
public static TimerTask exitLog(final Integer userId, final String ip) {
return new TimerTask() {
@Override
public void run() {
LoginLog loginLog = LogFactory.createLoginLog(LogType.EXIT, userId, null,ip);
try {
loginLogMapper.insert(loginLog);
} catch (Exception e) {
logger.error("创建退出日志异常!", e);
}
}
};
}
public static TimerTask bussinessLog(final Integer userId, final String bussinessName, final String clazzName, final String methodName, final String msg) {
return new TimerTask() {
@Override
public void run() {
OperationLog operationLog = LogFactory.createOperationLog(
LogType.BUSSINESS, userId, bussinessName, clazzName, methodName, msg, LogSucceed.SUCCESS);
try {
operationLogMapper.insert(operationLog);
} catch (Exception e) {
logger.error("创建业务日志异常!", e);
}
}
};
}
public static TimerTask exceptionLog(final Integer userId, final Exception exception) {
return new TimerTask() {
@Override
public void run() {
String msg = ToolUtil.getExceptionMsg(exception);
OperationLog operationLog = LogFactory.createOperationLog(
LogType.EXCEPTION, userId, "", null, null, msg, LogSucceed.FAIL);
try {
operationLogMapper.insert(operationLog);
} catch (Exception e) {
logger.error("创建异常日志异常!", e);
}
}
};
}
}
记录操作日志
这一步是最关键的一环
原理:通过自定义的注解@BussinessLog(可以任意命名),里面定义了业务的名称,被修改的实体的唯一标识,字典(用于查找key的中文名称和字段的中文名称),然后通过AOP,拦截所有添加了@BussinessLog注解的方法,解析其注解里面的属性,然后记录到对应的操作日志表中,完成操作日志的记录
@BussinessLog
import java.lang.annotation.*;
/**
* 标记需要做业务日志的方法
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BussinessLog {
/**
* 业务的名称,例如:"修改菜单"
*/
String value() default "";
/**
* 被修改的实体的唯一标识,例如:菜单实体的唯一标识为"id"
*/
String key() default "id";
/**
* 字典(用于查找key的中文名称和字段的中文名称)
*/
String dict() default "SystemDict";
}
@BussinessLog注解拦截AOP
import com.stylefeng.guns.common.annotion.log.BussinessLog;
import com.stylefeng.guns.common.constant.dictmap.base.AbstractDictMap;
import com.stylefeng.guns.common.constant.dictmap.factory.DictMapFactory;
import com.stylefeng.guns.core.log.LogManager;
import com.stylefeng.guns.core.log.LogObjectHolder;
import com.stylefeng.guns.core.log.factory.LogTaskFactory;
import com.stylefeng.guns.core.shiro.ShiroKit;
import com.stylefeng.guns.core.shiro.ShiroUser;
import com.stylefeng.guns.core.support.HttpKit;
import com.stylefeng.guns.core.util.Contrast;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 日志记录
*/
@Aspect
@Component
public class LogAop {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Pointcut(value = "@annotation(com.stylefeng.guns.common.annotion.log.BussinessLog)")
public void cutService() {
}
@Around("cutService()")
public Object recordSysLog(ProceedingJoinPoint point) throws Throwable {
//先执行业务
Object result = point.proceed();
try {
handle(point);
} catch (Exception e) {
log.error("日志记录出错!", e);
}
return result;
}
private void handle(ProceedingJoinPoint point) throws Exception {
//获取拦截的方法名
Signature sig = point.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("该注解只能用于方法");
}
msig = (MethodSignature) sig;
Object target = point.getTarget();
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
String methodName = currentMethod.getName();
//如果当前用户未登录,不做日志
ShiroUser user = ShiroKit.getUser();
if (null == user) {
return;
}
//获取拦截方法的参数
String className = point.getTarget().getClass().getName();
Object[] params = point.getArgs();
//获取操作名称
BussinessLog annotation = currentMethod.getAnnotation(BussinessLog.class);
String bussinessName = annotation.value();
String key = annotation.key();
String dictClass = annotation.dict();
StringBuilder sb = new StringBuilder();
for (Object param : params) {
sb.append(param);
sb.append(" & ");
}
//如果涉及到修改,比对变化
String msg;
if (bussinessName.indexOf("修改") != -1 || bussinessName.indexOf("编辑") != -1) {
Object obj1 = LogObjectHolder.me().get();
Map<String, String> obj2 = HttpKit.getRequestParameters();
msg = Contrast.contrastObj(dictClass, key, obj1, obj2);
} else {
Map<String, String> parameters = HttpKit.getRequestParameters();
AbstractDictMap dictMap = DictMapFactory.createDictMap(dictClass);
msg = Contrast.parseMutiKey(dictMap,key,parameters);
}
LogManager.me().executeLog(LogTaskFactory.bussinessLog(user.getId(), bussinessName, className, methodName, msg));
}
}
@BussinessLog使用实例
/**
* 新增字典
@param dictValues 格式例如 "1:启用;2:禁用;3:冻结"
*/
@BussinessLog(value = "添加字典记录", key = "dictName,dictValues", dict = com.stylefeng.guns.common.constant.Dict.DictMap)
@RequestMapping(value = "/add")
@Permission(Const.ADMIN_NAME)
@ResponseBody
public Object add(String dictName, String dictValues) {
if (ToolUtil.isOneEmpty(dictName, dictValues)) {
throw new BussinessException(BizExceptionEnum.REQUEST_NULL);
}
dictService.addDict(dictName, dictValues);
return SUCCESS_TIP;
}
3.记录异常日志
记录异常日志其实也是一个重复式的过程,这也可以通过统一的处理来记录异常抛出的日志
import com.stylefeng.guns.common.constant.tips.ErrorTip;
import com.stylefeng.guns.common.exception.BizExceptionEnum;
import com.stylefeng.guns.common.exception.BussinessException;
import com.stylefeng.guns.common.exception.InvalidKaptchaException;
import com.stylefeng.guns.core.log.LogManager;
import com.stylefeng.guns.core.log.factory.LogTaskFactory;
import com.stylefeng.guns.core.shiro.ShiroKit;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.UnknownSessionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.UndeclaredThrowableException;
import static com.stylefeng.guns.core.support.HttpKit.getIp;
import static com.stylefeng.guns.core.support.HttpKit.getRequest;
/**
* 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 拦截业务异常
*/
@ExceptionHandler(BussinessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorTip notFount(BussinessException e) {
LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e));
getRequest().setAttribute("tip", e.getMessage());
log.error("业务异常:", e);
return new ErrorTip(e.getCode(), e.getMessage());
}
/**
* 用户未登录
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String unAuth(AuthenticationException e) {
log.error("用户未登陆:", e);
return "/login.html";
}
/**
* 账号被冻结
*/
@ExceptionHandler(DisabledAccountException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String accountLocked(DisabledAccountException e, Model model) {
String username = getRequest().getParameter("username");
LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号被冻结", getIp()));
model.addAttribute("tips", "账号被冻结");
return "/login.html";
}
/**
* 账号密码错误
*/
@ExceptionHandler(CredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public String credentials(CredentialsException e, Model model) {
String username = getRequest().getParameter("username");
LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号密码错误", getIp()));
model.addAttribute("tips", "账号密码错误");
return "/login.html";
}
/**
* 验证码错误
*/
@ExceptionHandler(InvalidKaptchaException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String credentials(InvalidKaptchaException e, Model model) {
String username = getRequest().getParameter("username");
LogManager.me().executeLog(LogTaskFactory.loginLog(username, "验证码错误", getIp()));
model.addAttribute("tips", "验证码错误");
return "/login.html";
}
/**
* 无权访问该资源
*/
@ExceptionHandler(UndeclaredThrowableException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public ErrorTip credentials(UndeclaredThrowableException e) {
getRequest().setAttribute("tip", "权限异常");
log.error("权限异常!", e);
return new ErrorTip(BizExceptionEnum.NO_PERMITION);
}
/**
* 拦截未知的运行时异常
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorTip notFount(RuntimeException e) {
LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e));
getRequest().setAttribute("tip", "服务器未知运行时异常");
log.error("运行时异常:", e);
return new ErrorTip(BizExceptionEnum.SERVER_ERROR);
}
/**
* session失效的异常拦截
*/
@ExceptionHandler(InvalidSessionException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String sessionTimeout(InvalidSessionException e, Model model, HttpServletRequest request, HttpServletResponse response) {
model.addAttribute("tips", "session超时");
assertAjax(request, response);
return "/login.html";
}
/**
* session异常
*/
@ExceptionHandler(UnknownSessionException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String sessionTimeout(UnknownSessionException e, Model model, HttpServletRequest request, HttpServletResponse response) {
model.addAttribute("tips", "session超时");
assertAjax(request, response);
return "/login.html";
}
private void assertAjax(HttpServletRequest request, HttpServletResponse response) {
if (request.getHeader("x-requested-with") != null
&& request.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) {
//如果是ajax请求响应头会有,x-requested-with
response.setHeader("sessionstatus", "timeout");//在响应头设置session状态
}
}
}