【Java EE 学习 77 上】【数据采集系统第九天】【通过AOP实现日志管理】【通过Spring石英调度动态生成日志表】【日志分表和查询】
一、需求分析
日志数据在很多行业中都是非常敏感的数据,它们不能删除只能保存和查看,这样日志表就会越来越大,我们不可能永远让它无限制的增长下去,必须采取一种手段将数据分散开来。假设现在整个数据库需要保存的数据量比较少,但是只有日志表的数据量会很大,在这种情况下我们可以考虑使用分表策略分散保存日志数据。
针对当前系统来讲,可以这么做:每个月创建一张新表用于保存当月的日志数据。当然这只是初期的保存日志的思路。
1.解决问题的方法就是分表,那么什么时候创建新表呢?
(1).如果服务器不关闭,假设一直处于运行状态,每个月的月末创建下一个月的日志表好像是比较不错的,但是这和软件测试的边界值条件相符合,是非常容易出错的地方,所以,在每个月的中间创建新表是比较不错的选择;处于系统的健壮性考虑,创建接下来两个月的表是比较合适的。
(2).当前月的表如何创建。
服务器不可能一开始就是开启的,所以我们需要在服务器开启的时候就创建好当前月的日志表,但是只是创建当前月的日志表还是不够的,还需要创建接下来两个月使用的日志表,或许你会问为什么,之后就交给某个定时器每个月中间自动创建表不就可以了吗?但是你没有考虑到,如果服务器的启动时间正好是在一个月的下半个月怎么办?这时候到了下个月的时候就没有日志表可用了。
2.接下来还有问题就是怎么将日志信息保存到当前月的日志表
这个实际上是比较简单的,我们通过一个规则根据时间动态的指定创建的表名,同样在插入数据的时候也能够使用相同的规则插入到当前月的表中。
3.怎么将数据从多个表中取出来
使用sql的union连接查询即可达到目的。
二、使用spring的石英调度定时创建日志表
使用spring石英调度任务的步骤如下:
1.创建石英调度任务类
该类必须继承org.springframework.scheduling.quartz.QuartzJobBean抽象类并重写executeInternal方法
1 package com.kdyzm.schedual; 2 3 import org.quartz.JobExecutionContext; 4 import org.quartz.JobExecutionException; 5 import org.springframework.scheduling.quartz.QuartzJobBean; 6 7 import com.kdyzm.service.LogService; 8 import com.kdyzm.utils.LogUtils; 9 /** 10 * 创建的石英任务:使用spring集成的石英调度,动态生成日志表 11 * @author kdyzm 12 * 13 */ 14 public class GenerateLogsTableTask extends QuartzJobBean{ 15 private LogService logService; 16 public LogService getLogService() { 17 return logService; 18 } 19 public void setLogService(LogService logService) { 20 this.logService = logService; 21 } 22 /** 23 * 执行调度任务的方法 24 * 每月15号创建下两个月需要用到的日志表 25 */ 26 @Override 27 protected void executeInternal(JobExecutionContext context) throws JobExecutionException { 28 String tableName=LogUtils.createGenerateLogsTableName(1); 29 String sql="create table if not exists "+tableName+" like logs"; 30 this.logService.executeSql(sql); 31 System.out.println(tableName+" 表生成了!"); 32 tableName=LogUtils.createGenerateLogsTableName(2); 33 sql="create table if not exists "+tableName+" like logs"; 34 this.logService.executeSql(sql); 35 System.out.println(tableName+" 表生成了!"); 36 } 37 }
这里需要创建一个LogUtils工具类并且封装一个生成日志表名的方法:该方法的参数是一个偏移量,如果是整数表示下几个月,如果是负数表示是上几个月,使用Calendar类给出的方法能够非常快速的计算出来加上几个月或者减去几个月之后的日期。
1 package com.kdyzm.utils; 2 3 import java.util.Calendar; 4 5 /** 6 * 专门针对日志生成流程定义的工具类 7 * @author kdyzm 8 * 9 */ 10 public class LogUtils { 11 //动态生成日志表名的方法 12 public static String createGenerateLogsTableName(int offset){ 13 Calendar calendar=Calendar.getInstance(); 14 // month=(month+offset-1)%month+1; 15 //计算偏移之后的动态表名 16 calendar.add(Calendar.MONTH, offset); 17 int year=calendar.get(Calendar.YEAR); 18 int month=calendar.get(Calendar.MONTH)+1; 19 return "logs_"+year+"_"+month; 20 } 21 }
2.配置applicationConext.xml
为了更加清晰的完成该项任务,单独使用一个配置文件完成该项任务的配置。
配置步骤:
(1)使用org.springframework.scheduling.quartz.JobDetailBean封装石英任务
1 <bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailBean"> 2 <property name="jobClass" value="com.kdyzm.schedual.GenerateLogsTableTask"></property> 3 <!-- 通过spring管理的bean必须通过这种方式注入到schema中 --> 4 <property name="jobDataAsMap"> 5 <map> 6 <entry key="logService" value-ref="logService"></entry> 7 </map> 8 </property> 9 </bean>
(2)设置触发器Bean,设置任务的调度策略
1 <!-- 触发器bean,设置任务的调度策略 --> 2 <bean id="cronTriggerBean" class="org.springframework.scheduling.quartz.CronTriggerBean"> 3 <property name="jobDetail" ref="jobDetailBean"></property> 4 <property name="cronExpression"> 5 <!-- 这个表达式的意思是:每个月的15号 --> 6 <value>0 0 0 15 * ? *</value> 7 </property> 8 </bean>
这里cronExpression中的值怎么填写是比较重要的,这里有7个参数需要填写,必须明白这七个参数的意思是什么
这七个参数分别对应着 [秒] [分] [小时] [日] [月] [周] [年]
其中"日"和"周"两个字段是相互对立的两个字段,"日"值的是一个月的几号,"周"指的是一周的星期几,两者是"有你无我"的立场。
序号 |
说明 |
是否必填 |
允许填写的值 |
允许的通配符 |
1 |
秒 |
是 |
0-59 |
, - * / |
2 |
分 |
是 |
0-59 |
, - * / |
3 |
小时 |
是 |
0-23 |
, - * / |
4 |
日 |
是 |
1-31 |
, - * ? / L W |
5 |
月 |
是 |
1-12 or JAN-DEC |
, - * / |
6 |
周 |
是 |
1-7 or SUN-SAT |
, - * ? / L # |
7 |
年 |
否 |
empty 或 1970-2099 |
, - * / |
(3)使用调度工厂bean激活触发器,启动石英任务
1 <!-- 调度器工厂bean,激活触发器,启动石英任务的 --> 2 <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> 3 <property name="triggers"> 4 <ref bean="cronTriggerBean"/> 5 </property> 6 </bean>
从上述三个步骤来看,后一个步骤依次对前面的任务进行了封装。
完整的schedual.xml配置文件:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" 4 xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans file:///D:\程序\java\Spring\spring-framework-4.2.1\spring-framework-4.2.1.RELEASE\schema/beans/spring-beans-2.5.xsd 6 http://www.springframework.org/schema/context file:///D:\程序\java\Spring\spring-framework-4.2.1\spring-framework-4.2.1.RELEASE\schema/context/spring-context-2.5.xsd 7 http://www.springframework.org/schema/aop file:///D:\程序\java\Spring\spring-framework-4.2.1\spring-framework-4.2.1.RELEASE\schema/aop/spring-aop-2.5.xsd 8 http://www.springframework.org/schema/tx file:///D:\程序\java\Spring\spring-framework-4.2.1\spring-framework-4.2.1.RELEASE\schema/tx/spring-tx-2.5.xsd"> 9 <!-- 配置调度任务的spring配置文件 --> 10 11 <!-- 任务明细bean,对石英任务进行封装 --> 12 <bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailBean"> 13 <property name="jobClass" value="com.kdyzm.schedual.GenerateLogsTableTask"></property> 14 <!-- 通过spring管理的bean必须通过这种方式注入到schema中 --> 15 <property name="jobDataAsMap"> 16 <map> 17 <entry key="logService" value-ref="logService"></entry> 18 </map> 19 </property> 20 </bean> 21 <!-- 触发器bean,设置任务的调度策略 --> 22 <bean id="cronTriggerBean" class="org.springframework.scheduling.quartz.CronTriggerBean"> 23 <property name="jobDetail" ref="jobDetailBean"></property> 24 <property name="cronExpression"> 25 <!-- 这个表达式的意思是:每个月的15号 --> 26 <value>0 0 0 15 * ? *</value> 27 </property> 28 </bean> 29 <!-- 调度器工厂bean,激活触发器,启动石英任务的 --> 30 <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> 31 <property name="triggers"> 32 <ref bean="cronTriggerBean"/> 33 </property> 34 </bean> 35 </beans>
3.配置web.xml配置文件
由于配置文件是单独的配置文件,所以需要在web.xml配置文件中单独声明:
1 <context-param> 2 <param-name>contextConfigLocation</param-name> 3 <param-value>classpath:spring/applicationContext.xml,classpath:spring/schedual.xml</param-value> 4 </context-param>
三、使用Spring监听器动态创建当前月和接下来两个月需要用到的日志表。
这和初始化权限表的的流程几乎完全相同,只是不需要配置applicationContext.xml配置文件了,只需要给监听器类加上注解纳入spring管理即可。
1 package com.kdyzm.listener; 2 3 import javax.annotation.Resource; 4 5 import org.springframework.context.ApplicationEvent; 6 import org.springframework.context.ApplicationListener; 7 import org.springframework.context.event.ContextRefreshedEvent; 8 import org.springframework.stereotype.Component; 9 10 import com.kdyzm.service.LogService; 11 import com.kdyzm.utils.LogUtils; 12 /** 13 * 动态生成当前月份的日志表 14 * 这里一次性生成三个月份的日志表,防止服务器启动的时间是在一个月的下半个月,即在15号之后 15 * @author kdyzm 16 * 17 */ 18 @Component 19 public class InitLogTableListener implements ApplicationListener{ 20 @Resource(name="logService") 21 private LogService logService; 22 @Override 23 public void onApplicationEvent(ApplicationEvent event) { 24 if(event instanceof ContextRefreshedEvent){ 25 //生成当前月的日志表 26 String tableName=LogUtils.createGenerateLogsTableName(0); 27 String sql="create table if not exists "+tableName+" like logs"; 28 logService.executeSql(sql); 29 System.out.println(tableName+" 表已经生成!"); 30 31 //生成下一个月的日志表 32 tableName=LogUtils.createGenerateLogsTableName(1); 33 sql="create table if not exists "+tableName+" like logs"; 34 logService.executeSql(sql); 35 System.out.println(tableName+" 表已经生成!"); 36 37 //生成第二个月的日志表 38 tableName=LogUtils.createGenerateLogsTableName(2); 39 sql="create table if not exists "+tableName+" like logs"; 40 logService.executeSql(sql); 41 System.out.println(tableName+" 表已经生成!"); 42 } 43 } 44 }
这时候Service中必须提供执行SQL语句的方法,使用hibernate中的SQLQuery对象即可完成该项任务,略。
完成二、三中的任务之后,动态创建当前月份的日志表的任务和定时创建后两个月的日志表的功能就已经实现了:启动tomcat服务器的时候就能够发现创建三张表的日志信息,同时每个月的14号00:00就会发现创建两个表的日志信息。
四、保存日志到日志分表
之前保存日志的时候保存到的表是log表,现在的任务是需要将日志保存到当前月对应的表中。这就需要考虑从哪里实现该该方法比较好,能够改动最小的代码实现该功能。
回顾Service,Service调用saveEntity方法保存日志对象,实际上调用的方法是LogDaoImpl对象中的方法,由于该类中没有定义saveEntity方法,所以会自动到父类中查找是否有该方法,结果在BaseDaoImpl类中找到了该方法,所以就调用了该方法,采用的泛型是Log,所以保存到了log表中。基于该流程,最合适的地方只有两个,一个是Service中国的save方法,另一个就是DAO中的save方法,综合考虑还是调用DAO中的方法,在LogDaoImpl类中重写BaseDaoImpl中的saveEntity方法,这样Service中的代码根本不需要改变就能够实现预定的目标了。
1 package com.kdyzm.dao.impl; 2 3 import java.util.Collection; 4 5 import org.springframework.stereotype.Repository; 6 7 import com.kdyzm.dao.base.impl.BaseDaoImpl; 8 import com.kdyzm.domain.Log; 9 import com.kdyzm.utils.LogUtils; 10 import com.kdyzm.utils.StringUtils; 11 @Repository("logDao") 12 public class LogDaoImpl extends BaseDaoImpl<Log>{ 13 /** 14 * 重写父类BaseDaoImpl中的方法,这里要动态指定表名,所以不能再使用hibernate提供的保存数据的方法 15 * 直接使用原生的slq语句来保存即可 16 */ 17 @Override 18 public void saveEntity(Log log) { 19 String tableName=LogUtils.createGenerateLogsTableName(0); 20 String sql="insert into "+tableName+" (" 21 + " logId,operateParams,operateResult,operator,operatorDate,operatorName,resultMessage) " 22 + "values (?,?,?,?,?,?,?)"; 23 this.executeSql(sql, 24 StringUtils.getUUIDString(), 25 log.getOperateParams(), 26 log.getOperateResult(), 27 log.getOperator(), 28 log.getOperatorDate(), 29 log.getOperatorName(), 30 log.getResultMessage() 31 ); 32 } 33 }
五、读取日志
由于日志已经分散到了多个表中,如果想要获取指定的日志数据,就必须到多个表中查询日志数据,这样就需要使用到了union关键字;和之前的保存日志的遇到的问题相同,之前查询所有日志使用的表是log表,现在需要查询当前月份对应的表,处理方式和之前保存日志数据使用的方式相同,只需要重写LogDaoImpl中的findAllEntities方法即可,默认查询当前月和上一个月的日志表(其实本应该指定范围的,暂时化简一下处理过程,只是查询当前月份的日志数据和上一个月份的日志数据),这样最终LogDaoImpl的形态就变成了这样:
1 package com.kdyzm.dao.impl; 2 3 import java.util.Collection; 4 5 import org.springframework.stereotype.Repository; 6 7 import com.kdyzm.dao.base.impl.BaseDaoImpl; 8 import com.kdyzm.domain.Log; 9 import com.kdyzm.utils.LogUtils; 10 import com.kdyzm.utils.StringUtils; 11 @Repository("logDao") 12 public class LogDaoImpl extends BaseDaoImpl<Log>{ 13 /** 14 * 重写父类中的方法,这里要动态指定表名,所以不能再使用hibernate提供的保存数据的方法 15 * 直接使用原生的slq语句来保存即可 16 */ 17 @Override 18 public void saveEntity(Log log) { 19 String tableName=LogUtils.createGenerateLogsTableName(0); 20 String sql="insert into "+tableName+" (" 21 + " logId,operateParams,operateResult,operator,operatorDate,operatorName,resultMessage) " 22 + "values (?,?,?,?,?,?,?)"; 23 this.executeSql(sql, 24 StringUtils.getUUIDString(), 25 log.getOperateParams(), 26 log.getOperateResult(), 27 log.getOperator(), 28 log.getOperatorDate(), 29 log.getOperatorName(), 30 log.getResultMessage() 31 ); 32 } 33 //重写该方法,因为该方法必须实现多表联合查询 34 @Override 35 public Collection<Log> findAllEntities() { 36 String tableName=LogUtils.createGenerateLogsTableName(0);//当前月的日志表 37 String talbeName1=LogUtils.createGenerateLogsTableName(-1);//上个月的日志表 38 String sql="select * from "+tableName+" union select * from "+talbeName1+" order by operatorDate desc"; 39 return this.findAllEntitiesBySql(sql); 40 } 41 }
最终的效果和之前未做分表处理的效果完全相同。