springboot定时任务

假设场景:单体应用的定时任务,假设我们已经有了一个搭建好的springboot应用,但是需要添加一个定时执行的部分(比如笔者遇到的是定时去请求一个接口数据来更新某个表),这样作为开发人员,笔者选择了最简单的方法,也就是springboot自带的定时器。

1、使用@Scheduled

demo的结构如下:

启动器:

package com.wh.timerdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling//开启定时任务
public class TimerdemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(TimerdemoApplication.class, args);
  }

}

定时器工具类:

package com.wh.timerdemo.util;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* scheduler定时器执行任务的类
*/
@Component
public class TimerUtil {

   private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

   /**
    * 每2s执行一次
    */
   @Scheduled(fixedRate = 5000)
   public void taskOne(){
       System.out.println("定时任务1执行!!!执行时间:" + dateFormat.format(new Date()));
  }

   /**
    * 每天凌晨3:15执行一次
    */
   @Scheduled(cron = "0 15 03 ? * *")//cron的格式会在后面贴出
   public void taskTwo(){
       System.out.println("定时任务2执行!!!执行时间:" + dateFormat.format(new Date()));
  }
}

输出结果:

最简单的定时任务就实现啦~

假设场景:分布式应用的定时任务。当这个项目做了一半、第一版即将发布线上时,我司的上云行动也进行到了白热化阶段,于是笔者就遇到了这样一个问题:多个实例的定时任务是会同时执行的,这样不仅会消耗资源,而且可能还会引起数据库锁。这时我就想到了quartz。但是要注意,使用quartz的前提是集群的时间要设置统一。

2、使用分布式定时任务框架quartz

首先quartz本身是支持分布式的,通过表来管理各节点之间的关系。

1、去quartz官网下载最新的包 http://www.quartz-scheduler.org/

2、下载之后解压,进入如下目录,创建数据库表

quartz-2.2.3-distribution\quartz-2.2.3\docs\dbTables并选择对应的数据库SQL(笔者使用的是MySQL数据库)

3、在pom文件中引入依赖

<!--quartz依赖-->
<dependency>
   <groupId>org.quartz-scheduler</groupId>
   <artifactId>quartz</artifactId>
</dependency>
<dependency>
   <groupId>org.quartz-scheduler</groupId>
   <artifactId>quartz-jobs</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
</dependency>

4、创建 quartz.proiperties 配置文件

org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.makeSchedulerThreadDaemon=true
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.makeThreadsDaemons=true
#线程数量
org.quartz.threadPool.threadCount:20
#线程优先级
org.quartz.threadPool.threadPriority:5
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix=QRTZ_
#特别注意:此处是quartz的数据源,报错就debug跟踪一下查看dbName
org.quartz.jobStore.dataSource = springTxDataSource.schedulerFactoryBean
#加入集群
org.quartz.jobStore.isClustered=true
#容许的最大作业延
org.quartz.jobStore.misfireThreshold=25000
#调度实例失效的检查时间间隔
org.quartz.jobStore.clusterCheckinInterval: 5000

5、quartz的初始化配置,读取配置文件

package com.wh.timerdemo.config;

import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;

public class QuartzConfig {
   // 配置文件路径
   private static final String QUARTZ_CONFIG = "/quartz.properties";
   // 按照自己注入的数据源自行修改
   @Qualifier("writeDataSource")
   @Autowired
   private DataSource dataSource;

   @Autowired
   private AutoWiredSpringBeanToJobFactory autoWiredSpringBeanToJobFactory;

   /**
    * 从quartz.properties文件中读取Quartz配置属性
    * @return
    * @throws IOException
    */
   @Bean
   public Properties quartzProperties() throws IOException {
       PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
       propertiesFactoryBean.setLocation(new ClassPathResource(QUARTZ_CONFIG));
       propertiesFactoryBean.afterPropertiesSet();
       return propertiesFactoryBean.getObject();
  }
   /**
    * JobFactory与schedulerFactoryBean中的JobFactory相互依赖,注意bean的名称
    * 在这里为JobFactory注入了Spring上下文
    *
    * @param applicationContext
    * @return
    */
   @Bean
   public JobFactory buttonJobFactory(ApplicationContext applicationContext) {
       AutoWiredSpringBeanToJobFactory jobFactory = new AutoWiredSpringBeanToJobFactory();
       jobFactory.setApplicationContext(applicationContext);
       return jobFactory;
  }

   @Bean
   public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
       SchedulerFactoryBean factory = new SchedulerFactoryBean();
       factory.setJobFactory(autoWiredSpringBeanToJobFactory);
       factory.setOverwriteExistingJobs(true);
       factory.setAutoStartup(true); // 设置自行启动
       // 延时启动,应用启动1秒后
       factory.setStartupDelay(1);
       factory.setQuartzProperties(quartzProperties());
       factory.setDataSource(dataSource);// 使用应用的dataSource替换quartz的dataSource
       return factory;
  }
}

6、将任务工厂注入到Spring

package com.wh.timerdemo.config;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import org.springframework.stereotype.Component;

/**
* 为JobFactory注入SpringBean,否则Job无法使用Spring创建的bean
*/
@Component
public class AutoWiredSpringBeanToJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
   private transient AutowireCapableBeanFactory beanFactory;
   @Override
   public void setApplicationContext(final ApplicationContext context) {
       beanFactory = context.getAutowireCapableBeanFactory();
  }
   @Override
   protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
       final Object job = super.createJobInstance(bundle);
       beanFactory.autowireBean(job);
       return job;
  }

}

7、创建任务调度管理,任务的增删改查,起动停止等。

package com.wh.timerdemo.config;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.List;

/**
* quartz的调度器 包含了任务的增删改查 可以配置在页面上调度任务 这里就省略了
*/
public class QuartzManager {
   private static SchedulerFactory schedulerFactory =  new StdSchedulerFactory();

   private Scheduler scheduler = null;

   /**
    * @Description: 添加一个定时任务
    *
    * @param jobName 任务名
    * @param jobGroupName 任务组名
    * @param triggerName 触发器名
    * @param triggerGroupName 触发器组名
    * @param jobClass 任务
    * @param cron   时间设置,参考quartz说明文档
    */
   @SuppressWarnings({ "unchecked", "rawtypes" })
   public  static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class jobClass, String cron) {
       try {
           // 任务名,任务组,任务执行类
           Scheduler scheduler = schedulerFactory.getScheduler();
           JobDetail jobDetail= JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName).build();


           // 触发器
           TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
           // 触发器名,触发器组
           triggerBuilder.withIdentity(triggerName, triggerGroupName);
           triggerBuilder.startNow();
           // 触发器时间设定
           triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
           // 创建Trigger对象
           CronTrigger trigger = (CronTrigger) triggerBuilder.build();

           // 调度容器设置JobDetail和Trigger
           scheduler.scheduleJob(jobDetail, trigger);

           // 启动
           if (!scheduler.isShutdown()) {
               scheduler.start();
          }
      } catch (Exception e) {
           throw new RuntimeException(e);

      }
  }

   /**
    * @Description: 修改一个任务的触发时间
    *
    * @param jobName
    * @param jobGroupName
    * @param triggerName 触发器名
    * @param triggerGroupName 触发器组名
    * @param cron   时间设置,参考quartz说明文档
    */
   public static  void modifyJobTime(String jobName,String jobGroupName, String triggerName, String triggerGroupName, String cron) {
       try {
           Scheduler scheduler = schedulerFactory.getScheduler();
           TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
           CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
           if (trigger == null) {
               return;
          }

           String oldTime = trigger.getCronExpression();
           if (!oldTime.equalsIgnoreCase(cron)) {
               System.out.println("任务:"+jobName+"被修改");
               /** 方式一 :调用 rescheduleJob 开始 */
              /* // 触发器
               TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
               // 触发器名,触发器组
               triggerBuilder.withIdentity(triggerName, triggerGroupName);
               triggerBuilder.startNow();
               // 触发器时间设定
               triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
               // 创建Trigger对象
               trigger = (CronTrigger) triggerBuilder.build();
               // 方式一 :修改一个任务的触发时间
               scheduler.rescheduleJob(triggerKey, trigger);*/
               /** 方式一 :调用 rescheduleJob 结束 */
               /** 方式二:先删除,然后在创建一个新的Job */
               JobDetail jobDetail = scheduler.getJobDetail(JobKey.jobKey(jobName, jobGroupName));
               Class<? extends Job> jobClass = jobDetail.getJobClass();
               removeJob(jobName, jobGroupName, triggerName, triggerGroupName);
               addJob(jobName, jobGroupName, triggerName, triggerGroupName, jobClass,cron);
               /** 方式二 :先删除,然后在创建一个新的Job */
          }
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
  }

   /**
    * @Description: 移除一个任务
    *
    * @param jobName
    * @param jobGroupName
    * @param triggerName
    * @param triggerGroupName
    */
   public  static  void removeJob(String jobName, String jobGroupName,String triggerName, String triggerGroupName) {
       try {
           Scheduler scheduler = schedulerFactory.getScheduler();
           TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);

           scheduler.pauseTrigger(triggerKey);// 停止触发器
           scheduler.unscheduleJob(triggerKey);// 移除触发器
           scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));// 删除任务
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
  }

   /**
    * @Description:启动所有定时任务
    */
   public  static  void startJobs() {
       try {
           Scheduler scheduler = schedulerFactory.getScheduler();
           scheduler.start();
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
  }

   /**
    * @Description:关闭所有定时任务
    */
   public static void shutdownJobs() {
       try {
           Scheduler scheduler = schedulerFactory.getScheduler();
           if (!scheduler.isShutdown()) {
               scheduler.shutdown();
          }
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
  }

   /**
    * 获取当前正在执行的任务
    * @return
    */
   public static boolean getCurrentJobs(String name){
       try {
           Scheduler scheduler = schedulerFactory.getScheduler();
           List<JobExecutionContext> jobContexts = scheduler.getCurrentlyExecutingJobs();
           for (JobExecutionContext context : jobContexts) {
               if (name.equals(context.getTrigger().getJobKey().getName())) {
                   return true;
              }
          }
      } catch (Exception e) {
           throw new RuntimeException(e);
      }
       return false;
  }

   public Scheduler getScheduler() {
       return scheduler;
  }

   public void setScheduler(Scheduler scheduler) {
       this.scheduler = scheduler;
  }


}

8、创建一个执行的Job,这里包含定时任务执行的逻辑

package com.wh.timerdemo.task;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @DisallowConcurrentExecution : 此标记用在实现Job的类上面,意思是不允许并发执行.
* 注org.quartz.threadPool.threadCount的数量有多个的情况,@DisallowConcurrentExecution才生效
*/
@DisallowConcurrentExecution
public class ButtonTimerJob implements Job {

   private static final Logger logger = LoggerFactory.getLogger(ButtonTimerJob.class);

   /**
    * 核心方法,Quartz Job真正的执行逻辑。
    * @throws JobExecutionException execute()方法只允许抛出JobExecutionException异常
    */

   @Override
   public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
       logger.info("--------------定时任务执行逻辑---------------------");
  }

}

9、创建启动Job类:负责任务的创建启动和配置cron等

package com.wh.timerdemo.task;

import com.wh.timerdemo.config.QuartzManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;

/**
* 定时任务的启动类
*/
@Configuration
public class StartJob implements ApplicationListener<ContextRefreshedEvent> {

   private Logger logger = LoggerFactory.getLogger(this.getClass());

   public void run() {
       logger.info(">> 启动定时任务...");
       //   QuartzManager.startJobs();
       QuartzManager.addJob(
               "SpecialPeriodJob",
               "SpecialPeriodJobGroup",
               "SpecialPeriodTrigger",
               "SpecialPeriodTriggerGroup",
               ButtonTimerJob.class,
               "0/30 * * * * ?");
  }

   @Override
   public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
       System.out.println("启动定时任务......");
       run();
  }
}

启动springboot,此时就不需要 @EnableScheduling 注解啦。

执行结果:

虽然IDEA控制台打印的信息显示我们不是集群启动,但是上线后查看日志,定时任务确实实现了三个实例但是只有一个会运行。

需要注意的是:任务第一次启动后就会存入数据库,再次启动的时候任务已经存在,就不需要再添加一个任务了,直接执行启动任务即可。由于quartz的特性,即使集群中有一个服务挂掉了,其他的定时任务仍会接替执行。

4、扩展

附录1:cron语法——引用自https://www.cnblogs.com/linjiqin/archive/2013/07/08/3178452.html

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

附录2:quartz各张表的作用——引用自https://blog.csdn.net/yhhyhhyhhyhh/article/details/84235374

qrtz_blob_triggers : 以Blob 类型存储的触发器。
qrtz_calendars:存放日历信息, quartz可配置一个日历来指定一个时间范围。
qrtz_cron_triggers:存放cron类型的触发器。
qrtz_fired_triggers:存放已触发的触发器。
qrtz_job_details:存放一个jobDetail信息。
qrtz_locks: 存储程序的悲观锁的信息(假如使用了悲观锁)。
qrtz_paused_trigger_graps:存放暂停掉的触发器。
qrtz_scheduler_state:调度器状态。
qrtz_simple_triggers:简单触发器的信息。
qrtz_trigger_listeners:触发器监听器。
qrtz_triggers:触发器的基本信息。
cron方式需要用到的4张数据表:
qrtz_triggers,qrtz_cron_triggers,qrtz_fired_triggers,qrtz_job_details

附录3:quartz的工作原理——引用自https://blog.51cto.com/simplelife/2314620?source=drh

Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。

节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。

节点争抢Job问题:

因为Quartz使用了一个随机的负载均衡算法, Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。

可以看出采用了Quartz集群采用了悲观锁的方式对triggers表进行行加锁, 以保证任务同步的正确性。

当线程使用上述的SQL对表中的数据执行操作时,数据库对该行进行行加锁; 于此同时, 另一个线程对该行数据执行操作前需要获取锁, 而此时已被占用, 那么这个线程就只能等待, 直到该行锁被释放。

写在最后:
本次定时任务之旅算是告一段落了,在趟雷的路上踩了不少雷,可惜当时时间紧迫,没有来得及把很多错误记录下来,只在最后总结出这样一篇文章和demo分享给各位,小弟才疏学浅写的不好,如有写的不对的地方欢迎各位指正。

本文的参考链接如下:
https://blog.csdn.net/d984881239/article/details/86569818
https://blog.csdn.net/yhhyhhyhhyhh/article/details/84235374

springboot定时任务之旅的更多相关文章

  1. springboot 定时任务部署至linux服务器上后会执行两次问题

    springboot定时任务在本地运行时,正常执行且只执行一次,但是在maven打包成war包,部署至linux服务器上之后,定时任务奇怪的执行了两次. 由于未做负载均衡,所以可以先排除是因为多台服务 ...

  2. Spring boot(三) springboot 定时任务

    这个不多说,springboot 定时任务非常简单就可以实现了. 30s运行一次 , @Scheduled(cron="0,30 * * * * ?") 通过这个控制定时时间 cr ...

  3. SpringBoot定时任务@Scheduled

    SpringBoot定时任务主要由两个注解完成. @Scheduled加在方法上面. @EnableScheduling加在类上面.可以是Application类,也可以是@Component类,还可 ...

  4. 小D课堂 - 零基础入门SpringBoot2.X到实战_第10节 SpringBoot整合定时任务和异步任务处理_41、SpringBoot定时任务schedule讲解

    笔记 1.SpringBoot定时任务schedule讲解     简介:讲解什么是定时任务和常见定时任务区别 1.常见定时任务 Java自带的java.util.Timer类            ...

  5. SpringBoot学习笔记(七):SpringBoot使用AOP统一处理请求日志、SpringBoot定时任务@Scheduled、SpringBoot异步调用Async、自定义参数

    SpringBoot使用AOP统一处理请求日志 这里就提到了我们Spring当中的AOP,也就是面向切面编程,今天我们使用AOP去对我们的所有请求进行一个统一处理.首先在pom.xml中引入我们需要的 ...

  6. SpringBoot定时任务 - 集成quartz实现定时任务(单实例和分布式两种方式)

    最为常用定时任务框架是Quartz,并且Spring也集成了Quartz的框架,Quartz不仅支持单实例方式还支持分布式方式.本文主要介绍Quartz,基础的Quartz的集成案例本,以及实现基于数 ...

  7. SpringBoot定时任务 - 什么是ElasticJob?如何集成ElasticJob实现分布式任务调度?

    前文展示quartz实现基于数据库的分布式任务管理和job生命周期的控制,那在分布式场景下如何解决弹性调度.资源管控.以及作业治理等呢?针对这些功能前当当团队开发了ElasticJob,2020 年 ...

  8. SpringBoot定时任务 - 开箱即用分布式任务框架xxl-job

    除了前文介绍的ElasticJob,xxl-job在很多中小公司有着应用(虽然其代码和设计等质量并不太高,License不够开放,有着个人主义色彩,但是其具体开箱使用的便捷性和功能相对完善性,这是中小 ...

  9. 带着新人学springboot的应用10(springboot+定时任务+发邮件)

    接上一节,环境一样,这次来说另外两个任务,一个是定时任务,一个是发邮件. 1.定时任务 定时任务可以设置精确到秒的准确时间去自动执行方法. 我要一个程序每一秒钟说一句:java小新人最帅 于是,我就写 ...

随机推荐

  1. 查看安卓APK源码破解

    原文:查看安卓APK源码破解 工具准备: <1>.android4me的AXMLPrinter2工具 <2>dex2jar <3>jd-gui 工具下载:http: ...

  2. UWP使用AppService向另一个UWP客户端应用程序提供服务

    原文:UWP使用AppService向另一个UWP客户端应用程序提供服务 在上篇里,我使用的是寄宿在WPF上的WCF进行两个程序间的通信,在解决问题的同时,我的同事也在思考能否使用UWP来做这件事.于 ...

  3. 【Git】原Git库拆分子目录作为新仓库,并保留log记录

    一.需求描述: 现有一个git仓库,Team A和Team B的人操作同一仓库的不同目录,Team A的dev希望Team B的dev没有权限review属于Team A的代码目录,故现需要先将这个g ...

  4. WPF svg 转 xmal

    原文:WPF svg 转 xmal 今天wpf里面要用矢量图,美工出的是svg格式的,需要将svg格式的转换为xaml 1.第一个尝试是安装Inkscape,这个软件可以直接将svg另存为xaml,但 ...

  5. php 如何利用 soap调用.Net的WebService asmx文件

    原文:php 如何利用 soap调用.Net的WebService asmx文件 最近,帮一个同行测试用.net写的WebService接口,C#调用通过,现在需要测试一下php版本对它的调用,经过各 ...

  6. 深入理解 Win32 PE 文件格式 Matt Pietrek(慢慢体会)

    这篇文章假定你熟悉C++和Win32. 概述 理解可移植可执行文件格式(PE)可以更好地了解操作系统.如果你知道DLL和EXE中都有些什么东西,那么你就是一个知识渊博的程序员.这一系列文章的第一部分, ...

  7. SpringMVC核心架构的具体流程

    核心架构的具体流程步骤如下: 1.首先用户发送请求-->DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行 处理,作为统一访问点,进行全局的流程控 ...

  8. js打印指定元素内容

    var v = document.createElement("div"); //向v中追加打印数据,可以将界面的元素追加进来 var h = window.open(" ...

  9. Qt之获取本机网络信息(超详细)

    经常使用命令行来查看一些计算机的配置信息. 1.首先按住键盘上的“开始键+R键”,然后在弹出的对话框中输入“CMD”,回车 另外,还可以依次点击 开始>所有程序>附件>命令提示符 2 ...

  10. Controls 属性与继承 TShape 类的小练习(使用TShape可以解决很多图形问题)

    本例效果图: 代码文件: unit Unit1; interface uses   Windows, Messages, SysUtils, Variants, Classes, Graphics, ...