SpringBoot自定义starter开发分布式任务调度实践

🚀 优质资源分享 🚀

学习路线指引(点击解锁)知识定位人群定位
🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

概述

需求

在前面的博客《Java定时器演进过程和生产级分布式任务调度ElasticJob代码实战》中,我们已经熟悉ElasticJob分布式任务的应用,其核心实现为elasticjob-lite-spring-boot-starter,少量配置开箱即用;还有前面也有博客文档谈谈走进Spring Boot源码学习之路和浅谈入门,了解Spring Boot的原理,没看过伙伴可以先翻看下前面的文章。SpringBoot官网已经提供非常多的starter使用,然而今天我们就来模拟封装一个简易的分布式任务调度实现定时任务选主执行和故障自动转移的starter,本篇主要重心在于基于SpringBoot官网标准start封装的模板和步骤。

相关概念

  • 应用程序上下文
    在Spring应用程序中,应用程序上下文是组成应用程序的对象(或“bean”)的网络。它包含我们的Web控制器,服务,存储库以及我们的应用程序可能需要的任何(通常是无状态的)对象。
  • 配置
    使用注释@Configuration标注的类,扮演添加到应用程序上下文的bean工厂。它可能包含带注释的工厂方法,@Bean其返回值由Spring自动添加到应用程序上下文中。
    简而言之,Spring配置为应用程序上下文提供bean。
  • 自动配置
    自动配置是Spring自动发现的@Configuration类。只要该类位于在类路径classpath上,即可自动配置,并将配置的结果添加到应用程序上下文中。自动配置可以是有条件的,使得其激活取决于外部因素,例如具有特定值的特定配置参数。
  • 自动配置模块
    自动配置模块是包含自动配置类的Maven或Gradle模块。这样,我们就可以构建自动为应用程序上下文做贡献的模块,添加某个功能或提供对某个外部库的访问。我们在Spring Boot应用程序中使用它所要做的就是在我们的pom.xml或者包含它的依赖项build.gradle。
    Spring Boot团队大量使用此方法将Spring Boot与外部库集成。
  • Spring Boot Starter
    Spring Boot Starter是一个Maven或Gradle模块,其唯一目的是提供“使用某个功能”“开始”所需的所有依赖项。这通常意味着它是一个单独的pom.xml或build.gradle文件,包含一个或多个自动配置模块的依赖项以及可能需要的任何其他依赖项。在Spring Boot应用程序中,我们只需要包含此启动器Starter即可使用该功能。

制作starter基本步骤

  • 提供了一个配置类,该配置类定义了我们需要的对象的实例化过程;
  • 提供了一个spring.factories文件,包含了配置类的全限定名;
  • 将配置类和spring.factories文件打包为一个启动器starter;
  • 程序启动时通过加载starter.jar包的spring.factories文件信息,然后通过反射实例化文件里面的类。

SpringBoot启动简述

Spring Boot 在启动的时候会做这几件事情

  • Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。
  • 根据 spring.factories 配置加载 AutoConfigure 类。
  • 根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context。

其实也就是 Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程中直接注入对应 Bean 资源即可。

实践

创建项目

  • 首先建立light-job-spring-boot-starter-autoconfigure的空项目,然后在项目中添加light-job-spring-boot-starter-autoconfigure的Maven模块,这里的light-job-spring-boot-starter-autoconfigure模块则是实现简易的分布式任务调度。

image-20220707103058207

  • 然后再新建一个专门作为依赖light-job-spring-boot-starter-autoconfigure模块空实现的maven模块,名称为light-job-spring-boot-starter,这个也是参考SpringBoot官网封装标准,具体可以看前面的文章如何说明spring-boot-starter-data-redis的官网实现。

image-20220707104422700

autoconfigure实现

参考GitHub基于分布式任务实现的一些代码,这里核心主要是构建一个light-job自动装配配置文件读取类和一个light-job自动装配配置类。

light-job-spring-boot-starter-autoconfigure模块添加Pom依赖

xml version=<span class="hljs-string""1.0" encoding="UTF-8"?>
"http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4.0.0
 
 org.springframework.boot
 spring-boot-starter-parent
 2.6.4
  
 
 com.itxs
 light-job-spring-boot-starter-autoconfigure
 1.0
 jar
 light-job-spring-boot-starter-autoconfigure
 Demo project for Spring Boot

 
 UTF-8
 UTF-8
 1.8
 2.6.4
 3.4.6
 3.4
 2.3.2
 

 
 
 org.springframework.boot
 spring-boot-starter
 provided
 
 
 org.springframework.boot
 spring-boot-starter-web
 provided
 
 
 org.springframework.boot
 spring-boot-configuration-processor
 true
 
 
 org.springframework
 spring-tx
 provided
 
 
 org.springframework
 spring-context-support
 provided
 

 
 javax.servlet
 javax.servlet-api
 provided
 

 
 org.apache.commons
 commons-lang3
 ${commons-lang3.version}
 

 
 org.apache.zookeeper
 zookeeper
 ${zookeeper.version}
 
 
 org.slf4j
 slf4j-log4j12
 
 
 

 
 com.google.code.gson
 gson
 

 
 org.quartz-scheduler
 quartz
 ${quartz.version}
 provided
 
 
 org.quartz-scheduler
 quartz-jobs
 ${quartz.version}
 provided
 
 


折叠 

创建LightJobProperties读取配置文件

package com.itxs.lightjob.config;

import com.itxs.lightjob.zk.ZKManager.KEYS;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@ConfigurationProperties(prefix = "light.job",ignoreInvalidFields = true)
public class LightJobProperties {

	private String enabled;
	private String zkConnect;
	private String rootPath = "/light/job";
	private int zkSessionTimeout = 60000;
	private String zkUsername;
	private String zkPassword;
	private List ipBlackList;
 
 
 private List targetBean;
 private List targetMethod;
 private List cronExpression;
 private List startTime;
 private List period;
 private List delay;
 private List params;
 private List type;
 private List extKeySuffix;
 private List beforeMethod;
 private List afterMethod;
 private List threadNum;
 
 
 public Map getConfig(){
 Map properties = new HashMap();
 properties.put(KEYS.zkConnectString.key, zkConnect);
 if(StringUtils.isNotBlank(rootPath)){
 properties.put(KEYS.rootPath.key, rootPath);
 }
 if(zkSessionTimeout > 0){
 properties.put(KEYS.zkSessionTimeout.key, zkSessionTimeout+"");
 }
 if(StringUtils.isNotBlank(zkUsername)){
 properties.put(KEYS.userName.key, zkUsername);
 }
 if(StringUtils.isNotBlank(zkPassword)){
 properties.put(KEYS.password.key, zkPassword);
 }
 StringBuilder sb = new StringBuilder();
 if(ipBlackList != null && ipBlackList.size() > 0){
 for(String ip:ipBlackList){
 sb.append(ip).append(",");
 }
 sb.substring(0,sb.lastIndexOf(","));
 }
 properties.put(KEYS.ipBlacklist.key, sb.toString());
 return properties;
 }

 public String getEnabled() {
 return enabled;
 }

 public void setEnabled(String enabled) {
 this.enabled = enabled;
 }

 public String getZkConnect() {
 return zkConnect;
 }
 public void setZkConnect(String zkConnect) {
 this.zkConnect = zkConnect;
 }
 public String getRootPath() {
 return rootPath;
 }
 public void setRootPath(String rootPath) {
 this.rootPath = rootPath;
 }
 public int getZkSessionTimeout() {
 return zkSessionTimeout;
 }
 public void setZkSessionTimeout(int zkSessionTimeout) {
 this.zkSessionTimeout = zkSessionTimeout;
 }
 public String getZkUsername() {
 return zkUsername;
 }
 public void setZkUsername(String zkUsername) {
 this.zkUsername = zkUsername;
 }
 public String getZkPassword() {
 return zkPassword;
 }
 public void setZkPassword(String zkPassword) {
 this.zkPassword = zkPassword;
 }
 public List getIpBlackList() {
 return ipBlackList;
 }
 public void setIpBlackList(List ipBlackList) {
 this.ipBlackList = ipBlackList;
 }


 public List getTargetBean() {
 return targetBean;
 }


 public void setTargetBean(List targetBean) {
 this.targetBean = targetBean;
 }


 public List getTargetMethod() {
 return targetMethod;
 }


 public void setTargetMethod(List targetMethod) {
 this.targetMethod = targetMethod;
 }


 public List getCronExpression() {
 return cronExpression;
 }


 public void setCronExpression(List cronExpression) {
 this.cronExpression = cronExpression;
 }


 public List getStartTime() {
 return startTime;
 }


 public void setStartTime(List startTime) {
 this.startTime = startTime;
 }


 public List getPeriod() {
 return period;
 }


 public void setPeriod(List period) {
 this.period = period;
 }


 public List getDelay() {
 return delay;
 }


 public void setDelay(List delay) {
 this.delay = delay;
 }


 public List getParams() {
 return params;
 }


 public void setParams(List params) {
 this.params = params;
 }


 public List getType() {
 return type;
 }


 public void setType(List type) {
 this.type = type;
 }


 public List getExtKeySuffix() {
 return extKeySuffix;
 }


 public void setExtKeySuffix(List extKeySuffix) {
 this.extKeySuffix = extKeySuffix;
 }


 public List getBeforeMethod() {
 return beforeMethod;
 }


 public void setBeforeMethod(List beforeMethod) {
 this.beforeMethod = beforeMethod;
 }


 public List getAfterMethod() {
 return afterMethod;
 }


 public void setAfterMethod(List afterMethod) {
 this.afterMethod = afterMethod;
 }

 public List getThreadNum() {
 return threadNum;
 }


 public void setThreadNum(List threadNum) {
 this.threadNum = threadNum;
 }
}
折叠 

创建自动装配类LightJobAutoConfiguration.java

package com.itxs.lightjob.config;


import com.itxs.lightjob.ZKScheduleManager;
import com.itxs.lightjob.core.TaskDefine;
import com.itxs.lightjob.util.ScheduleUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

@Configuration
@EnableConfigurationProperties({LightJobProperties.class})
@ConditionalOnProperty(value = "light.job.enabled", havingValue = "true")
@ComponentScan()
public class LightJobAutoConfiguration {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(LightJobAutoConfiguration.class);
	
	@Autowired
	private LightJobProperties uncodeScheduleConfig;
	
	@Bean(name = "zkScheduleManager", initMethod="init")
	public ZKScheduleManager commonMapper(){
		ZKScheduleManager zkScheduleManager = new ZKScheduleManager();
		zkScheduleManager.setZkConfig(uncodeScheduleConfig.getConfig());
		List list = initAllTask();
 zkScheduleManager.setInitTaskDefines(list);
 LOGGER.info("=====>ZKScheduleManager inited..");
 return zkScheduleManager;
 }
 
 private List initAllTask(){
 List list = new ArrayList();
 int total = 0;
 if(uncodeScheduleConfig.getTargetBean() != null){
 total = uncodeScheduleConfig.getTargetBean().size();
 }
 for(int i = 0; i < total; i++){
 TaskDefine taskDefine = new TaskDefine();
 if(uncodeScheduleConfig.getTargetBean() != null){
 String value = uncodeScheduleConfig.getTargetBean().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setTargetBean(value);
 }
 }
 if(uncodeScheduleConfig.getTargetMethod() != null){
 String value = uncodeScheduleConfig.getTargetMethod().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setTargetMethod(value);
 }
 }
 if(uncodeScheduleConfig.getCronExpression() != null){
 String value = uncodeScheduleConfig.getCronExpression().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setCronExpression(value);
 }
 }
 if(uncodeScheduleConfig.getStartTime() != null){
 String value = uncodeScheduleConfig.getStartTime().get(i);
 if(StringUtils.isNotBlank(value)){
 Date time = null;
 try {
 time = ScheduleUtil.transferStringToDate(value);
 } catch (ParseException e) {
 e.printStackTrace();
 }
 if(time != null){
 taskDefine.setStartTime(time);
 }
 }
 }
 if(uncodeScheduleConfig.getPeriod() != null){
 String value = uncodeScheduleConfig.getPeriod().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setPeriod(Long.valueOf(value));
 }
 }
 if(uncodeScheduleConfig.getDelay() != null){
 String value = uncodeScheduleConfig.getDelay().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setDelay(Long.valueOf(value));
 }
 }
 
 if(uncodeScheduleConfig.getParams() != null){
 String value = uncodeScheduleConfig.getParams().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setParams(value);
 }
 }
 
 if(uncodeScheduleConfig.getType() != null){
 String value = uncodeScheduleConfig.getType().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setType(value);
 }
 }
 
 if(uncodeScheduleConfig.getExtKeySuffix() != null){
 String value = uncodeScheduleConfig.getExtKeySuffix().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setExtKeySuffix(value);
 }
 }
 if(uncodeScheduleConfig.getBeforeMethod() != null){
 String value = uncodeScheduleConfig.getBeforeMethod().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setBeforeMethod(value);
 }
 }
 if(uncodeScheduleConfig.getAfterMethod() != null){
 String value = uncodeScheduleConfig.getAfterMethod().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setAfterMethod(value);
 }
 }
 if(uncodeScheduleConfig.getThreadNum() != null){
 String value = uncodeScheduleConfig.getThreadNum().get(i);
 if(StringUtils.isNotBlank(value)){
 taskDefine.setThreadNum(Integer.valueOf(value));
 }
 }
 list.add(taskDefine);
 }
 return list;
 }
}
折叠 

然后在resources目录下的META-INF目录下创建spring.factories文件,跟SpringBoot其他starter一样,输出自动装配类的全类名;springboot项目默认只会扫描本项目下的带@Configuration注解的类,如果自定义starter,不在本工程中,是无法加载的,所以要配置META-INF/spring.factories配置文件。配置了META-INF/spring.factories配置文件是springboot实现starter的关键点,springboot的这种配置加载方式是一种类SPI(Service Provider Interface)的方式,SPI可以在META-INF/services配置接口扩展的实现类,springboot中原理类似,只是名称换成了spring.factories而已。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itxs.lightjob.config.LightJobAutoConfiguration

其他还有自动装配类的具体实现代码文件,如下面目录,主要利用zookeeper做分布式协调如分布式选主,执行maven install打包和安装到本地maven仓库。

image-20220707142623981

light-job-spring-boot-starter

light-job-spring-boot-starter是不做实现,主要管理依赖,Pom文件内容如下

xml version=<span class="hljs-string""1.0" encoding="UTF-8"?>
"http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4.0.0

 com.itxs
 light-job-spring-boot-starter
 1.0
 jar

 
 8
 8
 
 
 
 com.itxs
 light-job-spring-boot-starter-autoconfigure
 1.0
 
 


最后我们执行maven install打包和安装到本地maven仓库。

调用示例

示例工程中加入light-job-spring-boot-starter依赖,这里选择前面文章示例的库存微服务模块中添加

        
 com.itxs
 light-job-spring-boot-starter
 1.0
 

创建演示任务并放到Spring容器里管理

package cn.itxs.ecom.storage.job;

import org.springframework.stereotype.Component;

@Component
public class DemoTask {

    public void execute() {
        System.out.println("===========execute start!=========");
        System.out.println("===========do job!=========");
        System.out.println("===========execute end !=========");
    }
}

配置文件增加

light:
  job:
    enabled: true
    zk-connect: 192.168.4.27:2181,192.168.4.28:2181,192.168.4.29:2181
    root-path: /ecom/storage
    zk-session-timeout: 60000
    target-bean:
      - demoTask
    target-method:
      - execute
    period:
      - 1000
    cron-expression:
      - 0/10 * * * * ?

启动三个库存微服务模块,在第1个库存微服务模块看到demoTask任务已经根据配置每十秒在运行

image-20220707145207648

关闭第1个库存微服务模块程序后,通过zookeeper重新选举一个节点定时执行,从下面看选择第3个库存微服务模块每十秒实行

image-20220707145317534

Redis读取配置赋值lightjob

zookeeper地址配置可以放到配置中心如Nacos,如果目前我们配置数据是放在Redis中,可以通过System.setProperty设置系统变量的方式来实现,先注释zk-connect的配置,这是启动程序就会报错

image-20220707150301012

RedisConfig配置类中增加实现BeanPostProcessor接口实现其postProcessAfterInitialization方法,在bean初始化后读取redis值设置环境变量值。

package cn.itxs.ecom.storage.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfig implements BeanPostProcessor{
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
 RedisTemplate template = new RedisTemplate();
 template.setConnectionFactory(redisConnectionFactory);
 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
 ObjectMapper om = new ObjectMapper();
 om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
 //om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON\_FINAL);
 jackson2JsonRedisSerializer.setObjectMapper(om);
 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
 // key采用String的序列化方式
 template.setKeySerializer(stringRedisSerializer);
 // hash的key也采用String的序列化方式
 template.setHashKeySerializer(stringRedisSerializer);
 // value序列化方式采用jackson
 //template.setValueSerializer(jackson2JsonRedisSerializer);
 template.setValueSerializer(stringRedisSerializer);
 // hash的value序列化方式采用jackson
 //template.setHashValueSerializer(jackson2JsonRedisSerializer);
 template.setHashValueSerializer(stringRedisSerializer);
 template.afterPropertiesSet();
 return template;
 }

 @Override
 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
 {
 //在redisTemplate Bean初始化之后设置light.job.zk-connect为公共集群的zk地址
 if (beanName.equals("redisTemplate")){
 log.info("postProcessAfterInitialization match beanName {}",beanName);
 try {
 RedisTemplate redisObj = (RedisTemplate) bean;
 String zkConnect = (String)redisObj.opsForHash().get("clusterinfo", "zookeeper-server");
 if (StringUtils.isNotBlank(zkConnect)) {
 log.info("postProcessAfterInitialization get zkConnect ={}", zkConnect);
 System.setProperty("light.job.zk-connect", zkConnect);
 log.info("System.setProperty light.job.zk-connect={}", zkConnect);
 }
 } catch (Exception e) {
 log.error("postProcessAfterInitialization operate redisTemplate {} failed", e);
 }
 }
 return null;
 }
}
折叠 

启动后可以看到正常每十秒执行定时任务

**本人博客网站 **IT小神 www.itxiaoshen.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值