因项目需要记录整个系统的操作记录,考虑到系统操作日志的数据量,单表很容易达到瓶颈,导致查询效率低下,顾使用分表方案,减小数据库的负担,缩短查询时间。目前对于分表的解决方案有很多,本博文主要讲解博主自行实现的日志管理的解决方案,如有遗漏或错误的请各位大佬多多包涵
鉴于总是有人私信要demo,这里将以前搭的一个简易的项目贴出来:https://gitee.com/jiangliuhong/syslog.git
具体表设计随项目情况而变化
表创建SQL语句
CREATE TABLE `sys_user_log` (
`log_id` varchar(32) NOT NULL COMMENT '日志表id,uuid',
`user_id` varchar(32) DEFAULT NULL COMMENT '用户id,记录操作用户',
`module_name` varchar(225) NOT NULL COMMENT '模块名称',
`operate` varchar(225) NOT NULL COMMENT '操作名称',
`time` datetime NOT NULL COMMENT '操作时间',
`class_name` varchar(225) NOT NULL COMMENT '类名称',
`method_name` varchar(225) NOT NULL COMMENT '方法名称',
`params` longtext COMMENT '传入参数',
`ip` varchar(225) NOT NULL COMMENT '操作ip',
PRIMARY KEY (`log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
因考虑到项目情况,顾为每月创建一个日志表
创建service、dao方法
DbMapper.xml
create table #{name,jdbcType=VARCHAR} like sys_user_log
DbMapper.java
public interface SysDbMapper {
/**
* 创建数据表
* @param name 表名
*/
void createUserLogTable(String name);
}
DbService.java
public interface SysDbService {
/**
* 创建数据表
*
* @param preName 名称前缀
* @param isSufTime 是否显示 时间后缀
*/
void createTable(String preName, Boolean isSufTime);
}
DbServiceImpl.java
@Service("dbService")
public class DbServiceimpl implements DbService {
@Autowired
private DbMapper dbMapper;
@Override
public void createTable(String preName, Boolean sufTime) {
if (sufTime) {
dbMapper.createTable(preName + "_" + getSufName());
} else {
dbMapper.createTable(preName);
}
}
/**
* 获取名称后缀
* 格式: 20170301
*
* @return
*/
private String getSufName() {
Date d = new Date();
String s = DateUtil.date2String(d, false);
return s.replace("-", "");
}
}
BaseQuartz.java
/**
* 报警定时任务接口
*
*/
public interface BaseQuartz {
/**
* 定时任务执行方法
*/
public void work();
}
DbQuartz.java
public class DbQuartz implements BaseQuartz {
private static final Logger log = LoggerFactory.getLogger(DbQuartz.class);
/**
* 默认表名
*/
private static String tableName = "sys_user_log";
@Override
public void work() {
try {
//创建表
DbService dbService = (SysDbService) SpringUtil.getBean("sysDbService");
dbService.createTable(tableName, true);
log.info("创建表" + tableName + "成功");
//更新系统用户日志表表缓存
SysCacheUtil.flushSysUserLogTableName();
} catch (Exception e) {
log.error("创建表" + tableName + "失败");
work();
}
}
}
quartz配置文件:spring-quartz.xml
work
0 0 0 1 * ?
到止为止日志表自动创建逻辑完成
因考虑查询速度,采用根据传入的时间进行联合查询,规定只能查询连续3个月的日志数据,即前台传入 20170301,20170525 则联合表sys_user_log20170301,sys_user_log20170401,sys_user_log20170501三表进行联合查询。
主要代码包括,日志实体类,日志查询类,日志表相关的dao、service 类
SysUserLog.java
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 系统用户日志
*/
public class SysUserLog implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
/**日志id*/
private String logId;
/**用户id*/
private String userId;
/**模块名称*/
private String moduleName;
/**操作*/
private String operate;
/**时间*/
private Date time;
/**类名*/
private String className;
/**方法名*/
private String methodName;
/**传入参数*/
private String params;
/**操作ip*/
private String ip;
public String getLogId() {
return logId;
}
public void setLogId(String logId) {
this.logId = logId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getModuleName() {
return moduleName;
}
public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
public String getOperate() {
return operate;
}
public void setOperate(String operate) {
this.operate = operate;
}
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
//扩展字段
/**用户账号*/
private String username;
/**组织名称*/
private String organizationName;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getOrganizationName() {
return organizationName;
}
public void setOrganizationName(String organizationName) {
this.organizationName = organizationName;
}
SysUserLogMapper.xml
log_id, user_id, module_name, operate, time, class_name, method_name, params, ip
insert into ${tableName}
(log_id, user_id, module_name, operate, time, class_name, method_name, params, ip)
values
(
#{log.logId,jdbcType=VARCHAR},
#{log.userId,jdbcType=VARCHAR},
#{log.moduleName,jdbcType=VARCHAR},
#{log.operate,jdbcType=VARCHAR},
#{log.time,jdbcType=TIMESTAMP},
#{log.className,jdbcType=VARCHAR},
#{log.methodName,jdbcType=VARCHAR},
#{log.params,jdbcType=VARCHAR},
#{log.ip,jdbcType=VARCHAR}
)
insert into sys_user_log
(log_id, user_id, module_name, operate, time, class_name, method_name, params, ip)
values
(
#{logId,jdbcType=VARCHAR},
#{userId,jdbcType=VARCHAR},
#{moduleName,jdbcType=VARCHAR},
#{operate,jdbcType=VARCHAR},
#{time,jdbcType=TIMESTAMP},
#{className,jdbcType=VARCHAR},
#{methodName,jdbcType=VARCHAR},
#{params,jdbcType=VARCHAR},
#{ip,jdbcType=VARCHAR}
)
SysUserLogMapper.java
public interface SysUserLogMapper {
/**
* 新增日志记录
* @param sysUserLog
* @return
*/
int insert(SysUserLog sysUserLog);
/**
* 添加日志到指定数据库
* @param tableName
* @param sysUserLog
* @return
*/
int insertToTable(@Param("tableName") String tableName,@Param("log") SysUserLog sysUserLog);
/**
* 查询所有
* @return
*/
List selectAll(SysUserLogQuery query);
List selectAllByTables(@Param("table") String table,@Param("query") SysUserLogQuery query);
}
SysUserLogService.java
PageInfo 为Mybatis分页插件“PageHelp”中的分页辅助类
public interface SysUserLogService {
/**
* 添加系统用户日志
*
* @param sysUserLog 日志信息
*/
void addUserLog(SysUserLog sysUserLog);
/**
* 分页获取日志列表
*
* @param query 查询参数
* @param start 页数
* @param length 每页个数
* @return
*/
PageInfo queryPage(SysUserLogQuery query, Integer start, Integer length) throws Exception;
SysUserLogServiceImpl.java
SysCacheUtil:项目中集成了EhCahe缓存,而后根据项目的缓存规则封装的缓存工具类。在该日志查询、存储方案中将根据数据库中的日志表进行操作,顾将日志数据表名存入缓存。查询系统数据库中日志数据表表名的sql语句如下:
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE=‘BASE TABLE’ AND TABLE_SCHEMA = ‘数据库名称’ AND TABLE_NAME LIKE CONCAT (‘sys_user_log’,’%’);
/**
* 用户日志管理业务实现
*
* @author jiangliuhong
* @CREATEDATE 2017年2月13日
*/
@Service("sysUserLogService")
public class SysUserLogServiceImpl implements SysUserLogService {
@Autowired
private SysUserLogMapper userLogMapper;
@Autowired
private DbService dbService;
/**
* sys_user_log
*/
private static String preTableName = "sys_user_log";
@Override
public void addUserLog(SysUserLog sysUserLog) {
try {
userLogMapper.insertToTable(getUserLogTableName(), sysUserLog);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public PageInfo queryPage(SysUserLogQuery query, Integer start, Integer length) throws Exception {
// 判断开始结束日期是否为空
if (query.getStartTime() == null || query.getEndTime() == null) {
throw new CustomerException("开始、结束时间不能为空");
}
// 从缓存中读取系统数据库中的日志表表名
List userLogTableName = (List) SysCacheUtil.getSysUserLogTableName();
// 检查系统用户日志表名缓存是否存在,不存在则查询
if (userLogTableName == null) {
userLogTableName = dbService.queryUserLogTableName();
}
String logTable = "";
if (userLogTableName == null ? true : userLogTableName.size() > 0) {
// 标记是否比较开始时间
Boolean isStart = true;
for (int i = 0; i < userLogTableName.size(); i++) {
if (isStart) {
// 如果表创建时间与开始时间时间同年同月 则使循环体往下执行
if (DateUtil.equals(changeToDate(userLogTableName.get(i)), query.getStartTime())
|| DateUtil.countSecond(changeToDate(userLogTableName.get(i)), query.getStartTime()) >= 0) {
logTable = " ( select * from " + preTableName;
isStart = false;
} else {
continue;
}
}
logTable = logTable + " union all " + " select * from " + userLogTableName.get(i);
// 如果表创建时间与结束时间同年同月 则跳出循环
if (DateUtil.equals(changeToDate(userLogTableName.get(i)), query.getEndTime())) {
break;
}
}
if (!isStart) {
logTable = logTable + " ) ";
} else {
logTable = preTableName;
}
} else {
logTable = preTableName;
}
// 分页计算
start = start / length + 1;
PageHelper.startPage(start, length);
// List userLogs = userLogMapper.selectAll(query);
List userLogs = userLogMapper.selectAllByTables(logTable, query);
PageInfo pageInfo = new PageInfo(userLogs);
return pageInfo;
}
/**
* 获取最新的日志表表名
*
* @return
*/
private String getUserLogTableName() {
//取用户日志表名集合
List userLogTableName = SysCacheUtil.getSysUserLogTableName();
// 判断最新表名是否与该月同月,不同月则创建该月日志表
if (userLogTableName == null ? true : userLogTableName.size() <= 0) {
dbService.createTable("sys_user_log", true);
userLogTableName = SysCacheUtil.flushSysUserLogTableName();
} else {
// userLogTableName.size() > 0
String tableName = userLogTableName.get(userLogTableName.size() - 1);
if (!DateUtil.equals(changeToDate(tableName), new Date(System.currentTimeMillis()))) {
// 不同月,则创建该月表,并更新日志缓存
dbService.createTable("sys_user_log", true);
userLogTableName = SysCacheUtil.flushSysUserLogTableName();
}
}
if (userLogTableName == null ? true : userLogTableName.size() <= 0) {
return preTableName;
}
// 更新缓存
SysCacheUtil.setSysUserLogTableName(userLogTableName);
return userLogTableName.get(userLogTableName.size() - 1);
}
/**
* 获取名称后缀
* 格式: 20170301
*
* @return
*/
@SuppressWarnings("unused")
private String getSufName() {
Date d = new Date();
String s = DateUtil.date2String(d, false);
s = s.replace("-", "");
s = s.substring(0, s.length() - 2);
s = s + "01";
return s;
}
/**
* 计算日志表时间
*
* @param tabelName 表名
* @return
*/
private Date changeToDate(String tabelName) {
int lastIndexOf = tabelName.lastIndexOf('_');
if (lastIndexOf >= 0) {
tabelName = tabelName.substring(lastIndexOf + 1);
String strDate = tabelName.substring(0, 4) + "-" + tabelName.substring(4, 6) + "-"
+ tabelName.substring(6, 8);
return DateUtil.string2Date(strDate);
} else {
return null;
}
}
}
SysUserLogQuery.java
该类为日志表辅助查询类,具体查询条件根据项目实际情况而定
import java.util.Date;
import java.util.List;
/**
* 日志查询类
*/
public class SysUserLogQuery {
/**组织id*/
private String organizationId;
/**组织集合*/
private List organizationIds;
/**开始时间*/
private Date startTime;
/**结束时间*/
private Date endTime;
/**用户名*/
private String username;
/**模块名称*/
private String moduleName;
public String getOrganizationId() {
return organizationId;
}
public void setOrganizationId(String organizationId) {
this.organizationId = organizationId;
}
public List getOrganizationIds() {
return organizationIds;
}
public void setOrganizationIds(List organizationIds) {
this.organizationIds = organizationIds;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getModuleName() {
return moduleName;
}
public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
}
DateUtil.java
该类为自行封装的时间处理工具类,代码如下:
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class DateUtil {
public static Date string2Date(String time) {
try {
Date date = new Date();
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (time.length() <= 10) {
time = time + " 00:00:00";
}
date = sdf.parse(time);
return date;
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
/**
* @param date 时间对象
* @param hms 是否显示时分秒
* @return
*/
public static String date2String(Date date, Boolean hms) {
DateFormat sdf;
if (hms) {
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} else {
sdf = new SimpleDateFormat("yyyy-MM-dd");
}
String time = sdf.format(date);
return time;
}
public static Timestamp string2Timestamp(String time) {
Timestamp ts = new Timestamp(System.currentTimeMillis());
if (time.length() <= 10) {
time = time + " 00:00:00";
}
ts = Timestamp.valueOf(time);
return ts;
}
/**
* @param ts 时间戳对象
* @param hms 是否显示时分秒
* @return
*/
public static String timestamp2String(Timestamp ts, Boolean hms) {
DateFormat sdf;
if (hms) {
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} else {
sdf = new SimpleDateFormat("yyyy-MM-dd");
}
String tsStr = "";
tsStr = sdf.format(ts);
return tsStr;
}
public static Date timestamp2Date(Timestamp ts) {
Date date = new Date();
date = ts;
return date;
}
public static Timestamp date2Timestamp(Date date) {
String time = "";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
time = sdf.format(date);
Timestamp ts = Timestamp.valueOf(time);
return ts;
}
/**
* 计算两个时间之间的小时差
* start - stop
*
* @param start 开始时间
* @param stop 结束时间
* @return
*/
public static Long countHour(Date start, Date stop) {
long diff = start.getTime() - stop.getTime();
long hour = diff / (60 * 60 * 1000);
return hour;
}
/**
* 计算两个时间之间的分钟数差
* start - stop
*
* @param start 开始时间
* @param stop 结束时间
* @return
*/
public static Long countMinute(Date start, Date stop) {
long diff = start.getTime() - stop.getTime();
long min = diff / (60 * 1000);
return min;
}
/**
* 计算两个时间之间的秒数差
* start - stop
*
* @param start 开始时间
* @param stop 结束时间
* @return
*/
public static Long countSecond(Date start, Date stop) {
long diff = start.getTime() - stop.getTime();
long sec = diff / 1000;
return sec;
}
/**
* 按天增加或减时间
*
* @param date
* @param days 增减的天数
* @param hms 是否显示时分秒
* @param isAdd 加减标识,false 是减,true是加
* @return
*/
public static String addOrMinusDate(Date date, int days, Boolean hms, Boolean isAdd) {
long d = (long) days;
SimpleDateFormat df = null;
if (hms) {
df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
} else {
df = new SimpleDateFormat("yyyy-MM-dd");
}
if (!isAdd) {
return df.format(new Date(date.getTime() - (d * 24 * 60 * 60 * 1000)));
} else {
return df.format(new Date(date.getTime() + (d * 24 * 60 * 60 * 1000)));
}
}
/**
* 判断两个日期是否同年同月
*
* @param date1 时间1
* @param date2 时间2
* @return
*/
public static boolean equals(Date date1, Date date2) {
Calendar calendar1 = Calendar.getInstance();
calendar1.setTime(date1);
Calendar calendar2 = Calendar.getInstance();
calendar2.setTime(date2);
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR)
&& calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH);
}
用户日志的记录,主要通过自定义java注解,通过在service方法标记注解,使用spring aop进行日志存储
自定义注解主要包括模块名称、操作内容两个内容,其使用方式为:@LogAnnotation(moduleName = “角色管理”, operate = “新增角色”)
如果需要其他内容,可根据以下源码进行扩展
LogAnnotation.java
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;
/**
* 自定义日志注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface LogAnnotation {
/**模块名称*/
String moduleName() default "";
/**操作内容*/
String operate() default "";
}
注解使用:
@LogAnnotation(moduleName = "角色管理", operate = "新增角色")
@Override
public void saveRole(SysRole role, String[] perIds) {
aop配置
LogInterceptor.java
定义拦截器,重写afterReturning方法,在调用service方法完成之后触发该方法实现日志写入
/**
* 日志拦截器
*
*/
public class LogInterceptor {
/**
* 方法正常完成后执行方法
* @param point
*/
@SuppressWarnings("rawtypes")
public void afterReturning(JoinPoint point) {
try {
Subject subject = null ;
try {
subject = CarfiUserUtil.getSubject();
} catch (Exception e) {
e.printStackTrace();
//发生异常则不进行
return ;
}
SysUserLog userLog = new SysUserLog();
String targetName = point.getTarget().getClass().getName();
Class targetClass = Class.forName(targetName);
String methodName = point.getSignature().getName();
Method[] method = targetClass.getMethods();
Object[] params = point.getArgs(); // 获得参数列表
for (Method m : method) {
if (m.getName().equals(methodName)) {
Class[] tmpCs = m.getParameterTypes();
if (tmpCs.length == params.length) {
// 获取注解内容
LogAnnotation logAnnotation = m.getAnnotation(LogAnnotation.class);
if(logAnnotation != null){
//写入参数
if(params.length>0){
// 使用json转换工具 将参数转为json串,以便存入数据库
String jsonStr = JsonUtil.toJSONStr(params);
userLog.setParams(jsonStr);
}
//获取模块名称
String moduleName = logAnnotation.moduleName();
//获取操作名称
String operate = logAnnotation.operate();
userLog.setModuleName(moduleName);
userLog.setOperate(operate);
userLog.setClassName(targetName);
userLog.setMethodName(methodName);
userLog.setLogId(CommonUtil.generatePrimaryKey());
userLog.setTime(new Date());
HttpServletRequest request = (HttpServletRequest)((WebSubject)subject).getServletRequest();
userLog.setIp(CommonUtil.getIpAddr(request));
userLog.setUserId(CarfiUserUtil.getSysUser().getUserId());
SysUserLogService userLogService = (SysUserLogService) SpringUtil.getBean ("sysUserLogService");
userLogService.addUserLog(userLog);
break;
}
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
SpringUtil的作用为在非Spring IOC容器下获取Spring IOC容器的上下文环境中的类,比如获取某个bean,最常见的如本文中的多次出现的“SysUserLogService userLogService = (SysUserLogService) SpringUtil.getBean (“sysUserLogService”);”通过SpringUtil得到service
SpringUtil.java
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* Spring工具类
*/
public class SpringUtil implements ApplicationContextAware {
/** 上下文 */
private static ApplicationContext applicationContext;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}
/**
* 根据Bean ID获取Bean
*
* @param beanId
* @return
*/
public static Object getBean(String beanId) {
if(applicationContext == null){
return null;
}
return applicationContext.getBean(beanId);
}
}
spring 配置文件(application.xml)中配置相应的bean,在配置文件中注册的SpringUtil bean 应在spring扫描配置之前