SpringAOP 日志管理
前言:日志在系统中是必不可上的部分,系统日志,操作日志,异常日志等等。这些日志功能在系统中是非常常见的,现在有成熟的日志框架使用,像log4j等等。日志信息是非功能性代码,适合采用SpringAOP进行分离,集中管理。不需要在原有的功能上进行修改,符合开闭原则。方便开发人员集中精力进行业务开发。利用SpringAOP实现业务操作日志管理,登陆日志管理,异常日志管理。
一、业务操作日志的实现
功能实现:在Service层切入,通过连接点获取调用的方法,通过自定义的注解,注明调用方法的类型和名称。针对用户增、删、改、查进行信息保存。
1.1 业务日志的表结构
/*
Navicat MySQL Data Transfer
Source Server : yuanjun
Source Server Version : 50519
Source Host : localhost:3306
Source Database : first_db
Target Server Type : MYSQL
Target Server Version : 50519
File Encoding : 65001
Date: 2018-01-05 21:26:20
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for log
-- ----------------------------
DROP TABLE IF EXISTS `log`;
CREATE TABLE `log` (
`logid` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(255) DEFAULT NULL COMMENT 'ip地址',
`operateUserName` varchar(255) DEFAULT NULL COMMENT '操作人员',
`operationName` varchar(255) DEFAULT NULL COMMENT '操作',
`operationType` varchar(255) DEFAULT NULL COMMENT '操作类型',
`operationDate` datetime DEFAULT NULL COMMENT '操作时间',
`operationTime` int(11) DEFAULT NULL COMMENT '操作时长',
`state` int(1) DEFAULT NULL COMMENT '操作状态',
`description` varchar(255) DEFAULT NULL COMMENT '状态描述',
PRIMARY KEY (`logid`)
) ENGINE=InnoDB AUTO_INCREMENT=59 DEFAULT CHARSET=utf8;
1.2 对应的实体bean
import java.util.Date;
/**
*
* @ClassName:Log
* @Description :操作日志
* @author yuanjun
* @date 2018-1-4
*/
public class Log {
private int logid;
private String ip;//操作人的ip
private String operateUserName;//操作人
private String operationName;//操作名
private String operationType;//操作类型
private Date operationDate;//操作时间
private long operationTime;//操作时长
private int state;//操作状态
private String description;//操作描述
...get set方法
}
采用的SSM框架进行的演示,需要进行日志信息的保存。由于log插入不是内容的重点,没有粘贴处对应的代码。读者可自己完成。
1.3 自定义注解,用于描述操作的类型和描述
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog {
/** 要执行的操作类型比如:add操作 **/
public String operationType() default "";
/** 要执行的具体操作比如:添加用户 **/
public String operationName() default "";
}
1.4 切面的具体操作
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.yuanjun.anno.SystemServiceLog;
import com.yuanjun.bean.ExceptionLog;
import com.yuanjun.bean.Log;
import com.yuanjun.log.service.ExceptionLogService;
import com.yuanjun.log.service.LogService;
import com.yuanjun.log.util.IpUtil;
/**
* 日志切面类
* @ClassName:LogAspect
* @Description :TODO
* @author yuanjun
* @date 2018-1-5
*/
public class LogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionAspect.class);
@Autowired
private LogService logService;
@Autowired
private ExceptionLogService exceptionLogService;
//获取开始时间
private long BEGIN_TIME ;
//获取结束时间
private long END_TIME;
//定义本次log实体
private Log log = new Log();
/**
* 前置通知
*/
public void before() {
BEGIN_TIME = new Date().getTime();
System.out.println("开始");
}
/**
* 后置通知
* @param joinPoint
* @throws Exception
*/
public void after(JoinPoint joinPoint) throws Exception{
END_TIME = new Date().getTime();
System.out.println("结束");
}
/**
* 返回值
*/
public void afterReturn(){
if(log.getState()==1||log.getState()==-1){
log.setOperationTime((END_TIME-BEGIN_TIME));
log.setOperationDate(new Date(BEGIN_TIME));
System.out.println(">>>>>>>>>>存入到数据库");
logService.insertLog(log);
}else {
System.out.println(">>>>>>>>不存入到数据库");
logService.insertLog(log);
}
}
public void doAfterThrow(Exception ex){
System.out.println(ex);
System.out.println("例外通知-----------------------------------");
}
/**
*
* @param joinPoint
* @return
*/
public Object aroud(ProceedingJoinPoint joinPoint){
//日志实体对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Object object = null;
try {
Map<String, Object> map = getServiceOperation(joinPoint);
//获取操作类型
String operationType = (String) map.get("operationType");
//获取操作描述
String operationName = (String) map.get("operationName");
//获取操作的IP地址
String ip = IpUtil.getRemortIP(request);
log.setIp(ip);
log.setOperationName(operationName);
log.setOperationType(operationType);
try {
object = joinPoint.proceed();
log.setDescription("执行成功");
log.setState( 1);
} catch (Throwable e) {
// TODO Auto-generated catch block
log.setDescription("执行失败");
log.setState(-1);
//异常日志处理
//handleException(joinPoint, e);
}
} catch (Exception e) {
}
return object;
}
/**
* 通过连接点获取注解的信息,即操作的类型与描述
* @param joinPoint
* @return
* @throws Exception
*/
public static Map<String,Object> getServiceOperation(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();
Map<String,Object> map = new HashMap<String,Object>();
String operationType = "";
String operationName = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
operationType = method.getAnnotation(SystemServiceLog. class).operationType();
operationName = method.getAnnotation(SystemServiceLog. class).operationName();
map.put("operationType", operationType);
map.put("operationName", operationName);
break;
}
}
}
return map;
}
}
获取远程IP地址
import javax.servlet.http.HttpServletRequest;
/**
*
* @ClassName:IpUtil
* @Description : 获取远程ip的工具类
* @author yuanjun
* @date 2018-1-5
*/
public class IpUtil {
/**
* 获取远程访问主机ip地址
*
* 创建时间:2017年2月24日
*
* @author HY
* @param request
* @return
*/
public static String getRemortIP(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;
}
}
LogService 提供向数据库插入log信息方法
1.5 本次采用的xml的配置(注解配置同理)
由Spring来管理切面类
<!--通知spring使用cglib而不是jdk的来生成代理方法 AOP可以拦截到Controller -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<!-- 配置service切面bean -->
<bean class="com.yuanjun.aop.LogAspect" id="myAspect"></bean>
<aop:config>
<!-- 定义service切面 -->
<aop:pointcut expression="execution(* com.yuanjun.service.*.*(..))" id="txpointcut"/>
<!-- 数值越小,优先级越高 -->
<aop:aspect ref="myAspect" order="-998">
<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="txpointcut"/>
<aop:around method="aroud" pointcut-ref="txpointcut"/>
<!-- 后置通知 -->
<aop:after method="after" pointcut-ref="txpointcut"/>
<aop:after-returning method="afterReturn" pointcut-ref="txpointcut"/>
</aop:aspect>
</aop:config>
1.6 页面访问测试结果
这里没有对用户信息保存,实际操作中用户信息还是很好获取的,可在session获取,获取在请求参数中获取
二、异常日志
异常日志在时间中这个是挺需要的,产品上线运行后,一些异常会被记录到log文件,一些不被重视的异常不容易发现,需要到log文件中找。有了这个记录,方便查找,和维护系统
2.1 异常日志的对应的bean与表设计
package com.yuanjun.bean;
import java.util.Date;
/**
*
* @ClassName:ExceptionLog
* @Description :异常信息日志表
* @author yuanjun
* @date 2018-1-5
*/
public class ExceptionLog {
private int id;
private String ip;//请求的ip地址
private String url;//请求的url
private String args;//请求参数
private String className;//发生异常的类名
private String methodName;//执行的方法名
private String exceptionType;//异常类型
private Date exceptionTime;//发生异常时间
private String exceptionMsg;//异常信息
private byte isView;//是否查看
...get Set方法
}
/*
Navicat MySQL Data Transfer
Source Server : yuanjun
Source Server Version : 50519
Source Host : localhost:3306
Source Database : first_db
Target Server Type : MYSQL
Target Server Version : 50519
File Encoding : 65001
Date: 2018-01-05 21:57:01
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for exceptionlog
-- ----------------------------
DROP TABLE IF EXISTS `exceptionlog`;
CREATE TABLE `exceptionlog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
`args` varchar(255) DEFAULT NULL,
`className` varchar(255) DEFAULT NULL,
`methodName` varchar(255) DEFAULT NULL,
`exceptionType` varchar(255) DEFAULT NULL,
`exceptionTime` datetime DEFAULT NULL,
`exceptionMsg` varchar(2000) DEFAULT NULL,
`isView` varchar(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
异常处理的思路:
在aroud中,执行代理方法,捕获object = joinPoint.proceed();中的异常信息,即方法执行过程中产生的异常。将异常在哪个类,调用哪个方法产生什么类型的异常记录下来,存入到数据库中。在之前的业务日志信息处理把异常处理加上即可
/**
* 异常日志处理
* @param joinPoint
* @param ex
*/
public void handleException(JoinPoint joinPoint,Throwable ex){
LOGGER.info(">>>>>>系统异常,记录异常信息到数据库------start------");
ExceptionLog log = new ExceptionLog();
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.
getRequestAttributes()).getRequest();
//获取请求的URL
StringBuffer requestURL = request.getRequestURL();
//获取参 数信息
String queryString = request.getQueryString();
//封装完整请求URL带参数
if(queryString != null){
requestURL .append("?").append(queryString);
}
String ip = IpUtil.getRemortIP(request);
//代理类
String className = joinPoint.getTarget().getClass().getName();
//执行的方法
String methodName = joinPoint.getSignature().getName();
//
Object[] argsList = joinPoint.getArgs();
StringBuffer args = new StringBuffer();
for (int i = 0; i < argsList.length; i++) {
args.append(argsList[i]);
}
String exceptionType = ex.getClass().getSimpleName();
Date exceptionTime = new Date();
String exceptionMsg = ex.getMessage();
byte isView = 0;
log.setIp(ip);
log.setUrl(requestURL.toString());
log.setArgs(args.toString());
log.setClassName(className);
log.setMethodName(methodName);
log.setExceptionType(exceptionType);
log.setExceptionTime(exceptionTime);
log.setExceptionMsg(exceptionMsg);
log.setIsView(isView);
System.out.println(log);
exceptionLogService.saveExceptionLog(log);
LOGGER.info(">>>>>>系统异常,记录异常信息到数据库------end------");
}
三、用户登录日志
3.1 实现分析
针对用户登入的请求进行切面,植入保存用户登录日志信息。也可在处理用户登录逻辑的时候进行的日志处理,为了统一管理和代码分离,选择SpringAOP统一处理。通过拦截Controller中请求来处理。在Springmvc中配置处理Controller拦截时,如果aop的配置与mvc的配置不在同一个xml文件中的时候,会出现请求拦截不到,由于不是一个上下文产生的对象,代理拦截不到,具体的原因了自行百度。
3.2 登录日志的bean与表结构
import java.util.Date;
/**
*
* @ClassName:LoginLog
* @Description :登陆日志
* @author yuanjun
* @date 2018-1-5
*/
public class LoginLog {
private int id;
private String loginName;//登陆名
private String loginIp;//登陆ip
private Date loginTime;//登陆时间
private Date loginOutTime;//退出时间
private String loginStatus;//登陆状态
//get set方法
}
表结构
/*
Navicat MySQL Data Transfer
Source Server : yuanjun
Source Server Version : 50519
Source Host : localhost:3306
Source Database : first_db
Target Server Type : MYSQL
Target Server Version : 50519
File Encoding : 65001
Date: 2018-01-06 10:47:11
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for loginlog
-- ----------------------------
DROP TABLE IF EXISTS `loginlog`;
CREATE TABLE `loginlog` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`loginName` varchar(255) DEFAULT NULL,
`loginIp` varchar(255) DEFAULT NULL,
`loginTime` datetime DEFAULT NULL,
`loginOutTime` datetime DEFAULT NULL,
`loginStatus` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
切面层代码演示
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.yuanjun.bean.ExceptionLog;
import com.yuanjun.bean.LoginLog;
import com.yuanjun.log.service.ExceptionLogService;
import com.yuanjun.log.service.LogService;
import com.yuanjun.log.service.LoginLogService;
import com.yuanjun.log.util.IpUtil;
/**
*
* @ClassName:ControllerAspect
* @Description :controller的切面控制
* @author yuanjun
* @date 2018-1-4
*/
public class ControllerAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionAspect.class);
@Autowired
private LoginLogService loginLogService;
@Autowired
private LogService logService;
@Autowired
private ExceptionLogService exceptionLogService;
//定义本次log实体
private LoginLog log = new LoginLog();
public void before(){
System.out.println("controller前置通知");
}
public void afterReturn(){
log.setLoginTime(new Date());
loginLogService.saveLoginLog(log);
}
/**
* 环绕处理
* @param joinPoint
* @return
*/
public Object aroud(ProceedingJoinPoint joinPoint){
//日志实体对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//用户信息的获取,可从session中获取,如果只针对login方法的,可以从请求中获取
String userName = "张三";
Object object = null;
try {
String ip = IpUtil.getRemortIP(request);
log.setLoginIp(ip);
log.setLoginName(userName);
try {
object = joinPoint.proceed();
//根据登陆逻辑的处理结果,来判断登陆操作是否成功
if("main".equals(object)){
log.setLoginStatus("success");
}else{
log.setLoginStatus("fail");
}
} catch (Throwable e) {
log.setLoginStatus("fail");
handleException(joinPoint, e);
}
} catch (Exception e) {
}
return object;
}
/**
* 处理异常
* @param joinPoint
* @param ex
*/
public void handleException(JoinPoint joinPoint,Throwable ex){
LOGGER.info(">>>>>>系统异常,记录异常信息到数据库------start------");
ExceptionLog log = new ExceptionLog();
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.
getRequestAttributes()).getRequest();
//获取请求的URL
StringBuffer requestURL = request.getRequestURL();
//获取参 数信息
String queryString = request.getQueryString();
//封装完整请求URL带参数
if(queryString != null){
requestURL .append("?").append(queryString);
}
String ip = IpUtil.getRemortIP(request);
//代理类
String className = joinPoint.getTarget().getClass().getName();
//执行的方法
String methodName = joinPoint.getSignature().getName();
//
Object[] argsList = joinPoint.getArgs();
StringBuffer args = new StringBuffer();
for (int i = 0; i < argsList.length; i++) {
args.append(argsList[i]);
}
String exceptionType = ex.getClass().getSimpleName();
Date exceptionTime = new Date();
String exceptionMsg = ex.getMessage();
byte isView = 0;
log.setIp(ip);
log.setUrl(requestURL.toString());
log.setArgs(args.toString());
log.setClassName(className);
log.setMethodName(methodName);
log.setExceptionType(exceptionType);
log.setExceptionTime(exceptionTime);
log.setExceptionMsg(exceptionMsg);
log.setIsView(isView);
System.out.println(log);
exceptionLogService.saveExceptionLog(log);
LOGGER.info(">>>>>>系统异常,记录异常信息到数据库------end------");
}
}
Spring中xml配置切面bean
<!-- 配置控制层异常处理 -->
<bean class="com.yuanjun.aop.ExceptionAspect"/>
<!-- 配置control切面bean -->
<bean class="com.yuanjun.aop.ControllerAspect" id="controllerAspect"></bean>
<aop:config>
<!-- 定义controller切面 -->
<aop:pointcut expression="execution(* com.yuanjun.control.LoginControl.*(..))" id="controlpointcut"/>
<!-- 数值越小,优先级越高 -->
<aop:aspect ref="controllerAspect" order="-999">
<!-- 后置通知 -->
<aop:before method="before" pointcut-ref="controlpointcut"/>
<aop:around method="aroud" pointcut-ref="controlpointcut"/>
<aop:after-returning method="afterReturn" pointcut-ref="controlpointcut"/>
</aop:aspect>
</aop:config>
由于演示采用的是SSM框架,springMVC的配置与aop的配置不在同一个xml中,出现AOP在Controller不起作用,解决办法,在springmvc中的xml文件添加,扫描的包为切面实现类所在的包
<!-- 保证在同一个容器中 -->
<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.yuanjun.aop"/>
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.yuanjun.bean.User;
import com.yuanjun.service.UserService;
@Controller
public class LoginControl {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(String userName,String password){
//简单的展示了登陆逻辑,根据自己项目的实际处理,这里登陆处理成功的标识,与
//AOP的处理保持一致,主要是为了获取登陆日志中是否登陆成功的字段信息
if("yuanjun".equals(userName)&&"123456".equals(password)){
return "main";
}
return "error";
}
}
通过SpringAOP拦截Serivce与controller完成登陆日志,用户操作日志与异常日志等功能。将烦锁的日志进行一个统一管理。不需要到处进行异常处理。通过这个实例Demo加深连接SpringAOP是如何使用以及好处。
源码下载