草庐IT

SpringBoot结合XXL-JOB实现定时任务

dovienson 2023-04-14 原文

《从零打造项目》系列文章

工具

ORM框架选型

数据库变更管理

定时任务框架

缓存

安全框架

开发规范

前言

上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。

Quartz的不足

Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:

  • 通过调用API的方式操作任务,不人性化。
  • 需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
  • 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。

Xxl-job介绍

官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。

通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。

更多详细介绍推荐阅读官方文档

项目实践

Spring Boot集成XXL-JOB

Spring Boot 集成 XXL-JOB 主要分为以下两步:

  1. 配置运行调度中心(xxl-job-admin)
  2. 配置运行执行器项目

xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:

  1. GitHub:github.com/xuxueli/xxl…
  2. Gitee:gitee.com/xuxueli0323…

下载完之后,在 doc/db 目录下有数据库脚本 tables_xxl_job.sql,执行下脚本初始化调度数据库 xxl_job,如下图所示:

配置调度中心

将下载的源码解压,用 IDEA 打开,我们需要修改一下 xxl-job-admin 中的一些配置。(我这里下载的是最新版 2.3.1)

1、修改 application.properties,主要是配置一下 datasource 以及 email,其他不需要改变。

### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver


### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=1739468244@qq.com
spring.mail.from=1739468244@qq.com
# 此处不是邮箱登录密码,而是开启SMTP服务后的授权码
spring.mail.password=xxxxx

2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。

<property name="log.path" value="/Users/xxx/xxl-job-2.3.1/logs/xxl-job-admin.log"/>

然后启动项目,正常启动后,访问地址为:http://localhost:8080/xxl-job-admin,默认的账户为 admin,密码为 123456,访问后台管理系统后台。

这样就表示调度中心已经搞定了,下一步就是创建执行器项目。

创建执行器项目

本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。

环境配置

1、引入依赖:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.3</version>
  <relativePath/>
</parent>

<properties>
  <java.version>1.8</java.version>
  <fastjson.version>1.2.73</fastjson.version>
  <hutool.version>5.5.1</hutool.version>
  <mysql.version>8.0.19</mysql.version>
  <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
  <org.projectlombok.version>1.18.20</org.projectlombok.version>
  <druid.version>1.1.18</druid.version>
  <springdoc.version>1.6.9</springdoc.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
  <dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.3.1</version>
  </dependency>

  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus</artifactId>
    <version>3.5.1</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid.version}</version>
  </dependency>

  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
  </dependency>
  <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.12</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${org.mapstruct.version}</version>
  </dependency>
  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>${hutool.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>${springdoc.version}</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

2、application.yml 配置文件

server:
  port: 9090

# xxl-job
xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册""任务结果回调";为空则关闭自动注册;
    executor:
      appname: hresh-job-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
      ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册""调度中心请求并触发任务";
      port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
      logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
      logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则,-1, 关闭自动清理功能;
    accessToken: default_token  # 执行器通讯TOKEN [选填]:非空时启用;

spring:
  application:
    name: xxl-job-practice
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xxl_job?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    lazy-loading-enabled: true

上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。

核心类

1、xxl-job 配置类

@Configuration
public class XxlJobConfig {

  @Value("${xxl.job.admin.addresses}")
  private String adminAddresses;
  @Value("${xxl.job.executor.appname}")
  private String appName;
  @Value("${xxl.job.executor.ip}")
  private String ip;
  @Value("${xxl.job.executor.port}")
  private int port;
  @Value("${xxl.job.accessToken}")
  private String accessToken;
  @Value("${xxl.job.executor.logpath}")
  private String logPath;
  @Value("${xxl.job.executor.logretentiondays}")
  private int logRetentionDays;

  @Bean
  public XxlJobSpringExecutor xxlJobExecutor() {
    // 创建 XxlJobSpringExecutor 执行器
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appName);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
    // 返回
    return xxlJobSpringExecutor;
  }
}

2、xxl-job 工具类

@Component
@RequiredArgsConstructor
public class XxlUtil {

  @Value("${xxl.job.admin.addresses}")
  private String xxlJobAdminAddress;

  private final RestTemplate restTemplate;

  // 请求Url
  private static final String ADD_INFO_URL = "/jobinfo/addJob";
  private static final String REMOVE_INFO_URL = "/jobinfo/removeJob";
  private static final String GET_GROUP_ID = "/jobgroup/loadByAppName";

  /**
   * 添加任务
   *
   * @param xxlJobInfo
   * @param appName
   * @return
   */
  public String addJob(XxlJobInfo xxlJobInfo, String appName) {
    Map<String, Object> params = new HashMap<>();
    params.put("appName", appName);
    String json = JSONUtil.toJsonStr(params);
    String result = doPost(xxlJobAdminAddress + GET_GROUP_ID, json);
    JSONObject jsonObject = JSON.parseObject(result);
    Map<String, Object> map = (Map<String, Object>) jsonObject.get("content");
    Integer groupId = (Integer) map.get("id");
    xxlJobInfo.setJobGroup(groupId);
    String xxlJobInfoJson = JSONUtil.toJsonStr(xxlJobInfo);
    return doPost(xxlJobAdminAddress + ADD_INFO_URL, xxlJobInfoJson);
  }

  // 删除job
  public String removeJob(long jobId) {
    MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
    map.add("id", String.valueOf(jobId));
    return doPostWithFormData(xxlJobAdminAddress + REMOVE_INFO_URL, map);
  }

  /**
   * 远程调用
   *
   * @param url
   * @param json
   */
  private String doPost(String url, String json) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<String> entity = new HttpEntity<>(json, headers);
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);
    return responseEntity.getBody();
  }

  private String doPostWithFormData(String url, MultiValueMap<String, String> map) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);
    return responseEntity.getBody();
  }
}

此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。

这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。

3、修改 JobGroupController,新增 loadByAppName 方法

@RequestMapping("/loadByAppName")
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT<XxlJobGroup> loadByAppName(@RequestBody Map<String, Object> map) {
  XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);
  return jobGroup != null ? new ReturnT<XxlJobGroup>(jobGroup)
    : new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null);
}

XxlJobGroupDao 文件以及对应的 xml 文件

XxlJobGroup loadByAppName(Map<String, Object> map);
<select id="loadByAppName" parameterType="java.util.HashMap" resultMap="XxlJobGroup">
		SELECT
		<include refid="Base_Column_List"/>
		FROM xxl_job_group AS t
		WHERE t.app_name = #{appName}
	</select>

4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法

@RequestMapping("/addJob")
	@ResponseBody
	@PermissionLimit(limit = false)
	public ReturnT<String> addJob(@RequestBody XxlJobInfo jobInfo) {
		return xxlJobService.add(jobInfo);
	}

	@RequestMapping("/removeJob")
	@ResponseBody
	@PermissionLimit(limit = false)
	public ReturnT<String> removeJob(String id) {
		return xxlJobService.remove(Integer.parseInt(id));
	}

addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。

@RequestMapping("/add")
	@ResponseBody
	public ReturnT<String> add(XxlJobInfo jobInfo) {
		return xxlJobService.add(jobInfo);
	}

至此,关于调度中心的修改就结束了。

5、XxlService 创建任务

@Service
@Slf4j
@RequiredArgsConstructor
public class XxlService {

  private final XxlUtil xxlUtil;

  @Value("${xxl.job.executor.appname}")
  private String appName;

  public void addJob(XxlJobInfo xxlJobInfo) {
    xxlUtil.addJob(xxlJobInfo, appName);
    long triggerNextTime = xxlJobInfo.getTriggerNextTime();
    log.info("任务已添加,将在{}开始执行任务", DateUtils.formatDate(triggerNextTime));
  }

}

业务代码

1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

  private final UserMapper userMapper;
  private final UserStruct userStruct;
  private final WeatherService weatherService;
  private final XxlService xxlService;

  /**
   * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.
   *
   * @param userRequest 用户请求体
   */
  @Transactional
  public void register(UserRequest userRequest) {
    if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||
        isBlank(userRequest.getPassword())) {
      BusinessException.fail("账号或密码为空!");
    }

    User user = userStruct.toUser(userRequest);
    userMapper.insert(user);

    LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);

    XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh")
        .scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN")
        .glueType("BEAN")
        .executorHandler("sayHelloHandler")
        .executorParam(user.getUsername())
        .misfireStrategy("DO_NOTHING")
        .executorRouteStrategy("FIRST")
        .triggerNextTime(DateUtils.toEpochMilli(scheduleTime))
        .executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build();

    xxlService.addJob(xxlJobInfo);
  }


  public void sayHelloToUser(String username) {
    if (StrUtil.isBlank(username)) {
      log.error("用户名为空");
    }
    User user = userMapper.selectByUserName(username);
    String message = "Welcome to Java,I am hresh.";
    log.info(user.getUsername() + " , hello, " + message);
  }


  public void pushWeatherNotification() {
    List<User> users = userMapper.queryAll();
    log.info("执行发送天气通知给用户的任务。。。");
    WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);
    for (User user : users) {
      log.info(user.getUsername() + "----" + weatherInfo.toString());
    }
  }
}

2、WeatherService,获取天气温度等信息,这里就不贴代码了。

3、UserController,只有一个用户注册方法

@RestController
@RequiredArgsConstructor
public class UserController {

  private final UserService userService;

  @PostMapping("/register")
  public Result<Object> register(@RequestBody UserRequest userRequest) {
    userService.register(userRequest);
    return Result.ok();
  }

}

任务处理器

这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。

1、DemoHandler,仅用作演示,没什么实际含义。

@RequiredArgsConstructor
@Slf4j
public class DemoHandler extends IJobHandler {

  @XxlJob(value = "demoHandler")
  @Override
  public void execute() throws Exception {
    log.info("自动任务" + this.getClass().getSimpleName() + "执行");
  }
}

2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。

@Component
@RequiredArgsConstructor
public class SayHelloHandler {

  private final UserService userService;

  @XxlJob(value = "sayHelloHandler")
  public void execute() {
    String param = XxlJobHelper.getJobParam();
    userService.sayHelloToUser(param);
  }
}

在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail” 设置任务结果并替代方法出参,示例代码如下

@XxlJob("demoJobHandler")
public void execute() {
  String param = XxlJobHelper.getJobParam();    // 获取参数
  XxlJobHelper.handleSuccess();                 // 设置任务结果
}

3、WeatherNotificationHandler,每天定时发送天气通知

@Component
@RequiredArgsConstructor
public class WeatherNotificationHandler extends IJobHandler {

  private final UserService userService;

  @XxlJob(value = "weatherNotificationHandler")
  @Override
  public void execute() throws Exception {
    userService.pushWeatherNotification();
  }
}

测试

1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。

2、新增任务

控制台输出:

com.msdn.time.handler.DemoHandler        : 自动任务DemoHandler执行

2、利用 postman 来注册用户

去 UI 任务管理页面,可以看到代码创建的任务。

1分钟后,控制台输出如下:

3、在 UI 任务管理页面手动新增任务,用来发送天气通知。

点击执行一次,控制台输出如下:

实际应用中,对于手动创建的任务,直接点击启动就可以了。

这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler

@XxlJob(value = "sayHelloHandler")
  public void execute() {
    String param = XxlJobHelper.getJobParam();
    userService.sayHelloToUser(param);

    long jobId = XxlJobHelper.getJobId();
    xxlUtil.removeJob(jobId);
  }

重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。

等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。

问题

控制台输出邮件注册错误

11:01:48.740 logback [RMI TCP Connection(1)-127.0.0.1] WARN  o.s.b.a.mail.MailHealthIndicator - Mail health check failed
javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256

原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。

解决方案:

总结

通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。

感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!

参考文献

XXL-JOB动态创建任务详解篇2

Spring Boot 集成 XXL-JOB 任务调度平台

有关SpringBoot结合XXL-JOB实现定时任务的更多相关文章

  1. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  2. ruby-on-rails - 结合 meta_search 与 acts_as_taggable_on - 2

    我在开发的Rails3网站的一些搜索功能上遇到了一个小问题。我有一个简单的Post模型,如下所示:classPost我正在使用acts_as_taggable_on来更轻松地向我的帖子添加标签。当我有一个标记为“rails”的帖子并执行以下操作时,一切正常:@posts=Post.tagged_with("rails")问题是,我还想搜索帖子的标题。当我有一篇标题为“Helloworld”并标记为“rails”的帖子时,我希望能够通过搜索“hello”或“rails”来找到这篇帖子。因此,我希望标题列的LIKE语句与acts_as_taggable_on提供的tagged_with方法

  3. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  4. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  9. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  10. ruby-on-rails - Rake 任务仅调用一次时执行两次 - 2

    我写了一个非常简单的rake任务来尝试找到这个问题的根源。namespace:foodotaskbar::environmentdoputs'RUNNING'endend当在控制台中执行rakefoo:bar时,输出为:RUNNINGRUNNING当我执行任何rake任务时会发生这种情况。有没有人遇到过这样的事情?编辑上面的rake任务就是写在那个.rake文件中的所有内容。这是当前正在使用的Rakefile。requireFile.expand_path('../config/application',__FILE__)OurApp::Application.load_tasks这里

随机推荐