动态Logger等级 & 动态Task启停

416 阅读9分钟

引言

要想把一个功能做成动态的,数据必须被保存在一个可以被修改的地方,比如内存或数据库,然后,修改的操作一定要暴露出来。


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的其它属性:jobFactoryschedulerFactoryClassconfigLocation。这些属性是优化动态task的关键,比如日志追踪。