• Quartz-Spring定时任务器持久化,通过Service动态添加,删除,启动暂停任务


    原文地址:https://blog.csdn.net/ljqwstc/article/details/78257091

    首先添加maven的依赖:

    <!--quartz定时任务-->
            <dependency>
                <groupId>org.quartz-scheduler</groupId>
                <artifactId>quartz</artifactId>
                <version>2.2.1</version>
            </dependency>
            <dependency>
                <groupId>org.quartz-scheduler</groupId>
                <artifactId>quartz-jobs</artifactId>
                <version>2.2.1</version>

    spring配置文件中添加如下bean

    <!--
        自动装配类
        @Autowired
        private Scheduler scheduler;
        -->
        <!-- quartz持久化存储  -->
        <!--实现动态配置只需定义一下schedulerbean就可以了-->
        <bean name="schedulerFactoryBean"
              class="org.springframework.scheduling.quartz.SchedulerFactoryBean" lazy-init="true">
            <property name="dataSource">
                <ref bean="dataSource"/>
            </property>
            <property name="applicationContextSchedulerContextKey" value="applicationContext"/>
            <property name="quartzProperties">
                <props>
                    <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
                    <!-- 线程池配置 -->
                    <prop key="org.quartz.threadPool.class">${org.quartz.threadPool.class}</prop>
                    <prop key="org.quartz.threadPool.threadCount">${org.quartz.threadPool.threadCount}</prop>
                    <prop key="org.quartz.threadPool.threadPriority">${org.quartz.threadPool.threadPriority}</prop>
                    <prop key="org.quartz.jobStore.misfireThreshold">${org.quartz.jobStore.misfireThreshold}</prop>
                    <!-- JobStore 配置 -->
                    <prop key="org.quartz.jobStore.class">${org.quartz.jobStore.class}</prop>
                    <!-- 集群配置 -->
                    <prop key="org.quartz.jobStore.isClustered">true</prop>
                    <prop key="org.quartz.jobStore.clusterCheckinInterval">15000</prop>
                    <prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">1</prop>
                    <!-- 数据表设置 -->
                    <prop key="org.quartz.jobStore.tablePrefix">${org.quartz.jobStore.tablePrefix}</prop>
                    <prop key="org.quartz.jobStore.dataSource">${org.quartz.jobStore.dataSource}</prop>
                </props>
            </property>
        </bean>
    解释:
    1.一般公司项目中都已经定义好了dataSource,比如我们公司是用DruidDataSource,所以直接拿来用就可以了。如果没有的话,需要配置一个dataSource,这里就不阐述了。
    2.因为是要持久化的,所以org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX  
    这样设置以后就是持久化,会将任务的一些相关信息和配置存入到数据库中。具体需要用到什么数据表,下面会有。
    3.Scheduler简单说相当于一个容器一样,里面放着jobDetail和Trriger
    4.其他的一些配置可以根据自己的业务需求,到quartz的官网查看配置文档进行添加。

    这个是quartz.properties文件

    #============================================================================
    # Configure Datasources
    #============================================================================
    #JDBC驱动
    #org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
    #org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8
    #org.quartz.dataSource.myDS.user = root
    #org.quartz.dataSource.myDS.password = root
    #org.quartz.dataSource.myDS.maxConnections =5
    #集群配置
    org.quartz.scheduler.instanceName: DefaultQuartzScheduler
    org.quartz.scheduler.rmi.export: false
    org.quartz.scheduler.rmi.proxy: false
    org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
     
    org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount: 10
    org.quartz.threadPool.threadPriority: 5
    org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
     
    org.quartz.jobStore.misfireThreshold: 60000
     
    #============================================================================
    # Configure JobStore
    #============================================================================
     
    #默认配置,数据保存到内存
    #org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
    #持久化配置
    org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
    org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    org.quartz.jobStore.useProperties:true
     
    #数据库表前缀
    org.quartz.jobStore.tablePrefix:qrtz_
    org.quartz.jobStore.dataSource:myDS

    这个是quartz官方的表,运行就可以了,就建表成功了。

    #
    # Quartz seems to work best with the driver mm.mysql-2.0.7-bin.jar
    #
    # PLEASE consider using mysql with innodb tables to avoid locking issues
    #
    # In your Quartz properties file, you'll need to set 
    # org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    #
     
    DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
    DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
    DROP TABLE IF EXISTS QRTZ_LOCKS;
    DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_TRIGGERS;
    DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
    DROP TABLE IF EXISTS QRTZ_CALENDARS;
     
     
    CREATE TABLE QRTZ_JOB_DETAILS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        JOB_NAME  VARCHAR(200) NOT NULL,
        JOB_GROUP VARCHAR(200) NOT NULL,
        DESCRIPTION VARCHAR(250) NULL,
        JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
        IS_DURABLE VARCHAR(1) NOT NULL,
        IS_NONCONCURRENT VARCHAR(1) NOT NULL,
        IS_UPDATE_DATA VARCHAR(1) NOT NULL,
        REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
        JOB_DATA BLOB NULL,
        PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        JOB_NAME  VARCHAR(200) NOT NULL,
        JOB_GROUP VARCHAR(200) NOT NULL,
        DESCRIPTION VARCHAR(250) NULL,
        NEXT_FIRE_TIME BIGINT(13) NULL,
        PREV_FIRE_TIME BIGINT(13) NULL,
        PRIORITY INTEGER NULL,
        TRIGGER_STATE VARCHAR(16) NOT NULL,
        TRIGGER_TYPE VARCHAR(8) NOT NULL,
        START_TIME BIGINT(13) NOT NULL,
        END_TIME BIGINT(13) NULL,
        CALENDAR_NAME VARCHAR(200) NULL,
        MISFIRE_INSTR SMALLINT(2) NULL,
        JOB_DATA BLOB NULL,
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
            REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_SIMPLE_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        REPEAT_COUNT BIGINT(7) NOT NULL,
        REPEAT_INTERVAL BIGINT(12) NOT NULL,
        TIMES_TRIGGERED BIGINT(10) NOT NULL,
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_CRON_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        CRON_EXPRESSION VARCHAR(200) NOT NULL,
        TIME_ZONE_ID VARCHAR(80),
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_SIMPROP_TRIGGERS
      (          
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        STR_PROP_1 VARCHAR(512) NULL,
        STR_PROP_2 VARCHAR(512) NULL,
        STR_PROP_3 VARCHAR(512) NULL,
        INT_PROP_1 INT NULL,
        INT_PROP_2 INT NULL,
        LONG_PROP_1 BIGINT NULL,
        LONG_PROP_2 BIGINT NULL,
        DEC_PROP_1 NUMERIC(13,4) NULL,
        DEC_PROP_2 NUMERIC(13,4) NULL,
        BOOL_PROP_1 VARCHAR(1) NULL,
        BOOL_PROP_2 VARCHAR(1) NULL,
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_BLOB_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        BLOB_DATA BLOB NULL,
        PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
        FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
            REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_CALENDARS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        CALENDAR_NAME  VARCHAR(200) NOT NULL,
        CALENDAR BLOB NOT NULL,
        PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        TRIGGER_GROUP  VARCHAR(200) NOT NULL, 
        PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_FIRED_TRIGGERS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        ENTRY_ID VARCHAR(95) NOT NULL,
        TRIGGER_NAME VARCHAR(200) NOT NULL,
        TRIGGER_GROUP VARCHAR(200) NOT NULL,
        INSTANCE_NAME VARCHAR(200) NOT NULL,
        FIRED_TIME BIGINT(13) NOT NULL,
        SCHED_TIME BIGINT(13) NOT NULL,
        PRIORITY INTEGER NOT NULL,
        STATE VARCHAR(16) NOT NULL,
        JOB_NAME VARCHAR(200) NULL,
        JOB_GROUP VARCHAR(200) NULL,
        IS_NONCONCURRENT VARCHAR(1) NULL,
        REQUESTS_RECOVERY VARCHAR(1) NULL,
        PRIMARY KEY (SCHED_NAME,ENTRY_ID)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_SCHEDULER_STATE
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        INSTANCE_NAME VARCHAR(200) NOT NULL,
        LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
        CHECKIN_INTERVAL BIGINT(13) NOT NULL,
        PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
    CREATE TABLE QRTZ_LOCKS
      (
        SCHED_NAME VARCHAR(120) NOT NULL,
        LOCK_NAME  VARCHAR(40) NOT NULL, 
        PRIMARY KEY (SCHED_NAME,LOCK_NAME)
    )ENGINE=MyISAM DEFAULT CHARSET=utf8;  
     
     
    commit;

    ok,到这里,我们需要的配置基本完成,接下来,就是怎么样去创建任务,实现任务的动态添加,修改。

     
    首先,我们回顾一下,如果要执行一个任务,是不是要自定义一个类,比如SendEmailJob,然后这个类实现Job接口,再重写excute方法?然后再定义jobDetail,Trigger,然后再Scheduler.schdulerJob(jobDetail,trigger),安排任务?
     
    那如果业务需求改了,现在需要通知用户的任务,抓取新闻的任务,这样的话就需要写非常非常多的job实现类,对吧?
     
    所以,考虑到这一点,我们可以写一个QuartzJobFactory类实现Job接口,作为统一的Job入口,并且在job的上下文中加入一个我们POJO类(保存着一些job的相关信息),那么,在QuartzJobFactory类的excute方法中,我们可以根据我们传进去的POJO类对象获取当前job的相关信息,从而实现动态的去具体执行什么任务(方法)。
     

    下面详细解释下实现方法:

    这个是我自己定义的POJO类对象,用于保存当前任务的详细信息。
     
    public class ScheduleJob implements Serializable {
     
        private static final long serialVersionUID = -5115028108119830917L;
        /**
         * 任务名称
         */
        private String jobName;
        /**
         * 任务分组
         */
        private String jobGroup;
        /**
         * 触发器名称(默认和任务名称相同)
         */
        private String triggerName;
        /**
         * 触发器分组(默认和任务分组相同)
         */
        private String triggerGroup;
        /**
         * 任务需要调用的是哪个类的类名
         */
        private String className;
        /**
         * 任务需要调用的是哪个类的方法名
         */
        private String methodName;
        /**
         * 方法所需参数数组
         */
        private ArrayList paramArray;
        /**
         * 任务运行时间表达式
         */
        private String cron;
        /**
         * 任务运行时间(特指只运行一次的任务)
         */
        private String runDate;
        /**
         * 任务描述
         */
        private String desc;
        getter and setter.....
    }
    这个POJO类主要的作用的,保存任务的一些相关信息。当我们在创建jobDetail的时候,可以获取他的dataMap,set进去。这样我们可以在QuartzJobFactory的excute方法中的参数中获取到dataMap,并从中拿到这个POJO类对象,从而获取了该job具体要怎么执行,执行哪个方法,方法参数是什么,就都有了!
     
    接下来,我们继续看:
     
    这个是我定义的QuartzJobFactory,Job的统一入口:
     
    @PersistJobDataAfterExecution
    public class QuartzJobFactory implements Job {
     
        /**
         * The constant logger.
         */
        private static final Logger logger = LoggerFactory.getLogger(Constants.LOG_SYSTEM_RUN);
     
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            ScheduleJob scheduleJob = (ScheduleJob) jobExecutionContext.getMergedJobDataMap().get("scheduleJob");
            //获取service层接口名和接口方法名
            String springBeanName = scheduleJob.getClassName();
            String methodName = scheduleJob.getMethodName();
            //获取参数数组和参数类型数组
            ArrayList paramArray = scheduleJob.getParamArray();
            Class[] paramType = new Class[paramArray.size()];
            for (int i = 0; i < paramArray.size(); i++) {
                paramType[i] = paramArray.get(i).getClass();
            }
            /**
             * 反射运行service层的方法
             */
            Object bean = SpringContextsUtil.getBean(springBeanName);
            Method method = ReflectionUtils.findMethod(bean.getClass(), methodName, paramType);
            ReflectionUtils.invokeMethod(method, bean, paramArray.toArray());
     
            logger.info("任务名称 = [" + scheduleJob.getJobName() + "]," +
                    "任务调用的类=[" + scheduleJob.getClassName() + "]," +
                    "任务调用的方法=[" + scheduleJob.getMethodName() + "]---->>成功启动运行");
        }
    }
    有状态的两个注解
    支持并发
    @PersistJobDataAfterExecution
    如果不支持并发
    @disallowconcurrentexecution
     
    通过SpringContextsUtil工具类调用getBean去获取bean,这里的参数是String 类型的beanName,也就是我们配置文件配置bean是name那个属性。或者是我们在写Service层的时候,通过注@Service("beanName")   ,当然是推荐后者啦!简单方便。
    获得了bean以后,在通过方法名获取Method,再利用反射就可以实现对Service层方法的调用了。
     
    下面是我的SpringContextUtil工具类:
     
    @Component
    public class SpringContextsUtil implements ApplicationContextAware {
     
     
        private static ApplicationContext applicationContext;
     
        public static Object getBean(String beanName) throws BeansException {
            return applicationContext.getBean(beanName);
        }
     
        public static <T> T getBean(String beanName, Class<T> clazs) {
            return applicationContext.getBean(beanName, clazs);
        }
     
        @Override
        public void setApplicationContext(ApplicationContext applicationContext)
                throws BeansException {
            SpringContextsUtil.applicationContext = applicationContext;
        }
    }
    ok,现在就只剩下对任务的添加,删除,启动,暂停等动态操作了。
     
    为了方便管理,在我的quartz项目中,我的jobName和triggerName相同,jobGroup和triggerGroup相同。
     
    既然是对任务的一些动态操作。我创了一个QuartzProducer接口,里面有对job的动态操作的一些方法。如下:
    public interface QuartzProducer {
     
        /**
         * 添加简单任务,只运行一次的任务
         *
         * @param jobName
         * @param jobGroup
         * @param className
         * @param methodName
         * @param paramArray
         * @param runDateTime 格式:yyyyMMddHHmmss  
         * @throws SchedulerException
         * @throws ParseException
         */
        public void addSimpleJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String runDateTime) throws SchedulerException, ParseException;
     
     
        /**
         * 添加循环任务,特定时间循环运行,例如每个星期3,12点运行等
         *
         * @param jobName
         * @param jobGroup
         * @param className
         * @param methodName
         * @param paramArray
         * @param cron
         * @throws SchedulerException
         */
        public void addCronJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String cron) throws SchedulerException;
     
        /**
         * 修改简单任务,一般指修改运行时间
         *
         * @param jobName
         * @param jobGroup
         * @param runDateTime 格式:yyyyMMddHHmmss  
         */
        public void updateSimpleJob(String jobName, String jobGroup, String runDateTime) throws SchedulerException, ParseException;
     
        /**
         * 修改cron任务,一般指修改循环运行时间
         *
         * @param jobName
         * @param jobGroup
         * @param cron
         */
        public void updateCronJob(String jobName, String jobGroup, String cron) throws SchedulerException;
     
        /**
         * 移除任务
         *
         * @param jobName
         * @param jobGroup
         */
        public void deleteJob(String jobName, String jobGroup) throws SchedulerException;
     
        /**
         * 移除所有任务
         */
        public void deleteAll() throws SchedulerException;
     
        /**
         * 暂停任务
         *
         * @param jobName
         * @param jobGroup
         */
        public void pauseJob(String jobName, String jobGroup) throws SchedulerException;
     
        /**
         * 暂停所有任务
         */
        public void pauseAll() throws SchedulerException;
     
        /**
         * 恢复某个任务
         *
         * @param jobName
         * @param jobGroup
         */
        public void resumeJob(String jobName, String jobGroup) throws SchedulerException;
     
        /**
         * 恢复所有
         */
        public void resumeAll() throws SchedulerException;
     
        /**
         * 关闭任务调度器
         */
        public void shutDown() throws SchedulerException;
     
        /**
         * 开启任务调度器
         */
        public void startScheduler() throws SchedulerException;
    }

    实现类:

    //定义该bean的name为"quartzProducer"
    @Service("quartzProducer")
    public class QuartzProducerImpl implements QuartzProducer {
        
        
        //虽然在Spring配置中配置的是SchedulerFactoryBean这个类,但是我们自动转配就写这样,一样是可以使用的
        @Autowired
        private Scheduler scheduler;
     
        @Override
        public void addSimpleJob(String jobName, String jobGroup, String className, String methodName, ArrayList paramArray, String runDateTime) throws SchedulerException, ParseException {
            //判断是否已存在相同jobName,jobGroup,若存在则删除
            if (scheduler.getJobDetail(JobKey.jobKey(jobName, jobGroup)) != null) {
                deleteJob(jobName, jobGroup);
            }
            JobDetail jobDetail = JobBuilder.newJob(QuartzJobFactory.class).withIdentity(jobName, jobGroup).build();
            //任务具体执行的内容封装,返回给job统一入口
            ScheduleJob job = new ScheduleJob(jobName, jobGroup, jobName, jobGroup, className, methodName, paramArray, null, runDateTime, null);
            //将我自定义的POJO类存放到DataMap中
            jobDetail.getJobDataMap().put("scheduleJob", job);
            //创建SimpleTrigger,在特定时间仅运行一次
            Date runDate = DateUtils.parseDate(runDateTime, DateStyle.YYYYMMDDHHMMSS.getValue());
            SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).
                    startAt(runDate).build();
            scheduler.scheduleJob(jobDetail, trigger);
        }
     
    }
    只贴了一个添加的方法,其他方法可以自行实现。注释应该比较清楚了,这里不过多赘述。
     
    ok,到这里,我们已经完全了quartz跟spring的整合,以及如何调用service层的业务逻辑,任务的动态处理。
     
     
    接下来就非常非常简单了,一般来说就是前端调用controller层,给它传参,比如要调用什么接口,接口的方法,jobname,jobgroup,方法参数,什么时候运行,或者是cron表达式。然后controller层再调用我们刚刚写的quartzProducer就可以了。
     
     
    其他方法,比如定时任务修改,停用,删除,都是类似的,controller-->service
     
    好了,大概的思路就是这样子,如果有什么不对的地方请拍砖,有什么不太清楚的地方也请拍砖!
     
     
     
     
     
  • 相关阅读:
    使用Jquery EasyUi常见问题解决方案
    短信平台接口调用方法参考
    linux查找日志技巧
    Linux 上传 启动 删除...命令总结
    java 验证手机号码、电话号码(包括最新的电信、联通和移动号码)
    Web Services 中XML、SOAP和WSDL的一些必要知识
    Mac环境下配置PhpStorm
    Python爬虫刷回复
    Django和layim实现websocket
    Python爬虫刷回复
  • 原文地址:https://www.cnblogs.com/dyh004/p/9366359.html
Copyright © 2020-2023  润新知