引言
要想把一个功能做成动态的,数据必须被保存在一个可以被修改的地方,比如内存或数据库,然后,修改的操作一定要暴露出来。
Logback
感觉还是得先讲一下web环境下logback的启动过程,当然这个也不复杂。
常见的使用方式是在web.xml里配监听器,让logback跟着Tomcat一起启动。除了这个监听器,还需要提供logback配置文件的路径,有了这个参数,就可以按个人喜好安排配置文件的目录。
还有我个人喜欢把这个监听器放在web.xml中的第一个位置上,这样logback优先启动接管整个项目的logger,从最开始大家的logger等级就是统一的。
<listener>
<listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class>
</listener>
<context-param>
<param-name>logbackConfigLocation</param-name>
<param-value>classpath:config/logback.xml</param-value>
</context-param>
还有一种方法是直接把logback.xml放在classpath底下,然后就不管了,需要getLogger()时,会自动从classpath底下去寻找logback.xml。
下面回归正题,private static final Logger logger = LoggerFactory.getLogger(XXX.class);
这是最常见的,slf4j绑定logback然后获取logger的方式。slf4j是接口,logback做为实现,所以上面的类都是slf4j的,但点进getLogger()方法还是可以看见logback的实现的:
@Override
public final Logger getLogger(final String name) {
if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
}
// if we are asking for the root logger, then let us return it without
// wasting time
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
}
int i = 0;
Logger logger = root;
// check if the desired logger exists, if it does, return it
// without further ado.
Logger childLogger = (Logger) loggerCache.get(name);
// if we have the child, then let us return it without wasting time
if (childLogger != null) {
return childLogger;
}
// if the desired logger does not exist, them create all the loggers
// in between as well
String childName;
while (true) {
int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
if (h == -1) {
childName = name;
} else {
childName = name.substring(0, h);
}
// move i left of the last point
i = h + 1;
synchronized (logger) {
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
childLogger = logger.createChildByName(childName);
loggerCache.put(childName, childLogger);
incSize();
}
}
logger = childLogger;
if (h == -1) {
return childLogger;
}
}
}
ch.qos.logback.classic.LoggerContext类中有一个叫loggerCache的成员变量,缓存了所有的logger,Map类型,数据保存在内存中。当getLogger时,优先从该map中查找,没有的话再去创建,创建的逻辑这里就不讲了。
private Map<String, Logger> loggerCache;
同时,logback还提供了一个访问该map的方法:
public List<Logger> getLoggerList() {
Collection<Logger> collection = loggerCache.values();
List<Logger> loggerList = new ArrayList<Logger>(collection);
Collections.sort(loggerList, new LoggerComparator());
return loggerList;
}
ch.qos.logback.classic.Logger,再看一下这个类的源码(太长不贴了)。
Logger类有一个成员变量Level,输出日志就靠这个变量来判断等级,同时这个level是可以被访问,也可以被修改的
// The assigned levelInt of this logger. Can be null.
transient private Level level;
public Level getLevel() {
return level;
}
public synchronized void setLevel(Level newLevel){ ... }
再同时,Level类提供了一个新建其对象的方法,参数sArg表示等级,传入"info",便新建一个info等级的leve对象,传入"error"便返回一个error等级的level,还有就是,sArg这个参数是忽略大小写的。
public static Level valueOf(String sArg) {
return toLevel(sArg, Level.DEBUG);
}
当有了访问方式,而且还有了修改它的方法。
what do you think, boys?
所以,虽然logger数据是保存在xml这种静态文件里的,但因为事先读到内存里,并暴露了显示和修改的口子,logger等级还是想变就变。
Quartz
// 都9102年了,我还在研究不知道是什么时候的技术……
使用方法
定时器,定时启动执行任务。那么这句话拆一拆,共有3个关键词,执行、约定好的时间、任务。所以相应的,分别对应,Quartz这些接口Scheduler(执行器)、Trigger,CronScheduleBuilder(约定好的时间)、Job、JobDetail(任务)。不过从写代码的角度上讲,都是先定义一个任务,然后为这个任务约定一个启动时间,然后再交给执行器。
下面是一个小例子,TestTask实现Job接口,然后覆盖Job接口的execute方法。但新建Job接口的对象却不是靠new TestTask(),必须使用Quartz的JobBuilder类的,而且还要指定一个jobName,好让Quartz可以唯一的标识这个job。同样的道理,Trigger也需要一个唯一的triggerName,group就代表一个组,job和trigger在同一个组内就好。
代码1
public class TestTask implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("===测试quartz开始执行===");
System.out.println("===测试quartz执行结束===");
}
public static void main(String[] args) throws SchedulerException, InterruptedException {
String jobName = "testTask";
String jobTriggerName = jobName + "Trigger";
String jobGroupName = "testGroup";
// 新建一个task,嗯,我就是喜欢叫task
JobDetail job = JobBuilder.newJob(TestTask.class)
.withIdentity(jobName, jobGroupName)
.build();
// 设置约定启动时间(这里1一秒跑一次),以及第一次启动时间
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobTriggerName, jobGroupName)
.startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
.withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ? "))
.build();
// 获取执行器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 任务交给执行器
scheduler.scheduleJob(job, trigger);
scheduler.start();// 准备就绪,开始执行
Thread.sleep(10 * 1000);// 运行10s
scheduler.shutdown();// 关闭
}
}
除了上面写的接口,这里面还出现了JobBuilder、TriggerBuilder、StdSchedulerFactory等类,这些类的关系,包括Job和JobDetail的关系网上还是很多的,这里就不讲了。
还有这个demo是纯原生的写法,但这年头除了学习也没人会这么做了,一般都是和spring结合起来用,借助它的xml进行配置。虽然换了个形势,但其核心逻辑还是没变的,依旧是先定义Task,然后为Task设置cron表达式,最后向Scheduler注册Task。
通过spring配置不一样的地方是不强制实现Job接口,只需要告诉spring该执行Task中的哪一个方法就好了。
代码2
<!-- 自定义的task -->
<bean id="testTask" class="com.jojo.task.TestTask"></bean>
<!--
task详细信息,注意targetMethod这个属性是必填的,也正因为这个属性,
所以不一定非得实现Quartz的Job接口,指出要执行哪一个方法就行了。Job接口的好处在于
可以获取累积执行次数,执行时间等信息。然后留心一下MethodInvokingJobDetailFactoryBean类,
它是spring的。
-->
<bean id="testTaskDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="testTask"></property>
<property name="targetMethod" value="execute"></property>
</bean>
<!-- task触发器,CronTriggerFactoryBean,也是spring的类 -->
<bean id="testTaskTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="testTaskDetail"/>
<property name="cronExpression" value="0/1 * * * * ? "></property>
</bean>
<!-- task执行,SchedulerFactoryBean类,还是spring的 -->
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="testTaskTrigger"/>
</list>
</property>
</bean>
要写这么一长串,确实很麻烦,所以对动态没什么要求的话,不如直接用spring @Scheduled注解,一步到位。
实现动态
// 默认环境为spring web 容器
其实Quartz也是将Task数据保存在内存里的,但是它不像logback可以直接返回全部logger数据,找来找去也只发现了返回即将执行task的方法,而这是肯定不够的:
/**
* <p>
* Calls the equivalent method on the 'proxied' <code>QuartzScheduler</code>.
* </p>
*/
public List<JobExecutionContext> getCurrentlyExecutingJobs() {
return sched.getCurrentlyExecutingJobs();
}
// 最后会找到这个
public List<JobExecutionContext> getExecutingJobs() {
synchronized (executingJobs) {
return java.util.Collections.unmodifiableList(new ArrayList<JobExecutionContext>(
executingJobs.values()));
}
}
虽然quartz没有暴露获取所有task的方法,但是它暴露了增删改的方法,可以往内存里加task,删task,改task。所以这时候为了实现动态就只能依靠数据库了,我们需要建立一张task表,比方说叫sys_task表,在这张表里保存全部task信息,名字,完整类名,cron表达式,参数等等。
借助数据库的逻辑是这样的,首先,在项目启动时,从数据库读取全部task数据,并按代码1的方式全部注册到Quartz的Scheduler中。之后,再把对sys_task表进行的增删查改操作同步给Scheduler。
因此,这里的第一步就是获取Scheduler。
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
这种原生获取Scheduler的方法是不推荐的,很难与项目整合,spring环境下还是得用spring相关的类。
<bean id="quartzSchedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="startupDelay" value="30"/>
<property name="overwriteExistingJobs" value="true"/>
</bean>
把这个加到spring的配置文件里,spring就会自动帮我们创建Scheduler对象,并保存在容器里,使用@Autowired就可以获取到实例。
@Autowired
private Scheduler scheduler;
另外这个类就是代码2中的SchedulerFactoryBean,只是改了个名字。startupDelay参数的意思是延迟启动,因为我们的环境是web程序,所以,必须等web程序启动完后,才开始加载quartz,免得出现什么问题。
有了Scheduler执行器后,下一步就是增删查改TASK了。为节省笔墨,直接放出demo。另外,这个真的是最简单的动态task的demo了,只有关停和修改cron的功能。
第一个注意点,@PostConstruct,通过此注解在spring容器加载的过程中,从数据库查询全部task并注册到Scheduler中。
第二个注意点,注册后,不需要scheduler.start();这行代码,原理暂不清楚,推测是spring自动启动了。
第三个注意点,先暂停Trigger并移除Trigger后,再去删除Job,再Quartz的接口体系中,Trigger类在Job类之上。
@RestController
@RequestMapping("/dynamic/task")
public class DynamicSysTaskController {
@Autowired
private SysTaskService sysTaskService;
@Autowired
private Scheduler scheduler;
private static final Logger logger = LoggerFactory.getLogger(DynamicSysTaskController.class);
private static final String GROUP_NAME = "testGroup";
private static final String JOB_NAME_PREFIX = "testTask-";
private static final String TRIGGER_NAME_SUEFIX = "-trigger";
@RequestMapping("/list")
public Response list() {
Response response = new Response();
List<SysTask> sysTaskList = sysTaskService.selectAll();
response.setData(sysTaskList);
response.setSuccessMessage("");
return response;
}
@RequestMapping("/saveOrUpdate")
public Response saveOrUpdate(@RequestBody SysTask sysTask) throws SchedulerException {
Response response = new Response();
registerTask(sysTask);
response.setSuccessMessage("");
return response;
}
@RequestMapping("/delete")
public Response delete(@RequestBody SysTask sysTask) throws SchedulerException {
Response response = new Response();
String jobName = JOB_NAME_PREFIX + sysTask.getId();
JobKey jobKey = JobKey.jobKey(jobName, GROUP_NAME);
List<Trigger> triggerList = (List<Trigger>) scheduler.getTriggersOfJob(jobKey);
if (CollectionUtils.isEmpty(triggerList)) {
response.setFailMessage("不存在trigger");
return response;
}
for (Trigger trigger : triggerList) {
scheduler.pauseTrigger(trigger.getKey());// 暂停
scheduler.unscheduleJob(trigger.getKey());// 移出
}
scheduler.deleteJob(jobKey);
response.setSuccessMessage("");
return response;
}
@PostConstruct
private void initAllSysTask() throws ClassNotFoundException, SchedulerException {
List<SysTask> sysTaskList = sysTaskService.selectAll();
if (CollectionUtils.isEmpty(sysTaskList)) {
logger.error("没有需要初始化的task");
return;
}
for (SysTask sysTask : sysTaskList) {
registerTask(sysTask);
}
}
private void registerTask(SysTask sysTask) {
try {
String cron = sysTask.getCron();
String className = sysTask.getClassName();
String jobName = JOB_NAME_PREFIX + sysTask.getId();
String triggerName = jobName + TRIGGER_NAME_SUEFIX;
// 校验
boolean cronCheckResult = CronExpression.isValidExpression(cron);
boolean classCheckResult = ClassUtils.isPresent(className, null);
if (!(cronCheckResult && classCheckResult)) {// 校验失败退出
logger.error("此task的配置信息无效:{}", JSON.toJSONString(sysTask));
return;
}
Class<Job> classOfJob = (Class<Job>) ClassUtils.forName(className, null);
// 定义job
JobKey jobKey = JobKey.jobKey(jobName, GROUP_NAME);
JobDetail jobDetail = JobBuilder.newJob(classOfJob).withIdentity(jobKey).build();
// 定义Trigger
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerName, GROUP_NAME)
.startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
.withSchedule(cronScheduleBuilder).build();
// 注册到scheduler中
boolean exists = scheduler.checkExists(jobKey);
if (exists) {
scheduler.rescheduleJob(trigger.getKey(), trigger);
} else {
scheduler.scheduleJob(jobDetail, trigger);
}
logger.error("{} will run at: {} , cronExpression is : {}", jobDetail.getKey(), trigger.getNextFireTime(), cron);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后要注意的是,SchedulerFactoryBean的其它属性:jobFactory、schedulerFactoryClass、configLocation。这些属性是优化动态task的关键,比如日志追踪。