自己動手寫任務調度平臺

本文項目Github地址:https://github.com/zhouhuanghua/z-job

什麼是任務調度平臺呢?暫時不做解釋,先來看一下定時器的發展歷史吧!

首先,new Thread + while (true) + Thread.sleep的方式,雖然很low但是起碼能夠實現對吧——這種方式的問題是過於佔用資源,定時任務一多就暴露出來了。

然後,就是利用一些框架,比如JDK提供的定時器工具Timer和線程池ScheduledThreadPoolExecutor,Netty中基於時間輪實現的HashedWheelTimer,還有大名鼎鼎的Quartz等,在代碼裏面寫死了執行計劃——顯然,這種方式的問題是管理困難,有哪些定時任務在跑,執行計劃是什麼,要想知道得去刨代碼。

接着,基於第二種方式,將執行計劃和任務開關寫到配置文件裏面,達到了統一維護的目的——但是,修改執行計劃、任務啓用關閉,需要修改配置文件然後重新啓動應用,相當麻煩(不過,對於一些小型項目這種方式已經夠用,畢竟容易上手且成本低)。

最後, 就是基於第二種方式,將定時任務以及相關的執行計劃、開關都保存到數據庫,然後提供一個可視化的界面,給用戶在線修改。實現方式就是,以某種標準在代碼裏面寫好業務代碼,當用戶操作時纔將它們加入到定時任務裏面,或者從定時任務裏移除,應用啓動時也會自動將數據庫中啓用狀態的定時任務註冊——這種方式已經很靈性了。

只是,伴隨着系統的流量快速增長,大型網站對高併發高可用的要求逐漸提高,於是衍生出了集羣這麼一個東西。何謂"集羣",比如一個Tomcat的最大併發是500,但是網站的QPS是1000,這誰頂得住啊?於是需要再加一個Tomcat,通過負載均衡將流量分擔,這兩個Tomcat就形成了集羣。注意與系統拆分的區別:一組集羣裏面的每個機器跑的應用是一樣的。

那麼,問題來了:假如我的應用裏有定時任務,那豈不是集羣裏面每臺機器都在跑,這肯定不行的!怎麼解決呢?通過單獨設置機器上面應用的配置,只讓其中一臺跑。這,當然也可以,不過相當麻煩,而且負載均衡下管理界面怎麼解決(聰明的你們也許有辦法)。還有另外一個,微服務:總不能每個子應用設置一個管理界面吧?

扯了一大推,好像有點廢話了。那,什麼是任務調度平臺呢?

任務——調度——平臺:通過一個平臺,對所有機器上面的定時任務進行統一管理。不懂的話看圖

(暫不提風靡全國、功能強大的輕量級分佈式任務調度平臺xxl-job了哈,只說我發明的輪子。手動滑稽)

z-job任務調度平臺架構

其實,還是有必要說明一下滴:首先,需要在具體應用上面配置任務調度平臺的地址信息和自己的信息,當應用啓動時,將自己的應用名稱、IP及端口發送給任務調度平臺,後者保存到數據庫。然後在任務調度平臺手動添加任務信息,需要選擇所屬的應用並填寫任務名稱(具體應用裏面實現了IJobHandler的類並注入到Spring容器後的beanName)、執行計劃的CRON、告警郵件等信息。接着任務調度平臺會根據這個任務的名稱、所屬應用名稱、執行計劃創建一個定時作業(Quartz的Job),並將任務信息以及它的應用信息傳遞進去。最後任務調度平臺的定時作業執行的時候,會拿出傳遞進來的任務信息和它的應用信息,根據應用信息的機器地址發送HTTP請求,請求參數爲任務名稱以及運行參數,具體的應用收到請求後執行對應的JobHandler並將結果返回,任務調度平臺收到結果(如果返回執行失敗或者異常,並且任務信息配了嘗試次數、應用信息裏還有其它機器地址的話,就會進行重試,始終不成功的話發送告警郵件)後,將這一個過程寫入任務調度記錄裏面。

要不"鎮樓圖"先來一波!?

---首頁

---應用列表 

---任務列表。還有更多信息沒展示,後期考慮做個詳情頁面

 

 ---任務調度日誌。調度結果或者任務執行結果爲失敗時,鼠標放到提示的位置會展示失敗原因

---發送的告警郵件。內容是任務調度的記錄詳情

下面我們就結合代碼看一下開發步驟吧(基於Spring Boot的哦

一、項目結構

一共分爲三個模塊(這裏是爲了方便開發,實際應用時需要分成不同的項目)。

  • z-job-core:核心模塊。
  • z-job-admin:任務調度平臺,依賴z-job-core模塊。
  • z-job-example:使用z-job的一個示例應用,依賴z-job-core模塊即可。

下面我們就逐一模塊進行講解。 

二、z-job-core模塊

這個模塊是做什麼的呢?你要使用一個框架,需要引入它的一些依賴,按照它的一些標準開發代碼吧?z-job-core就是充當這麼一個角色。它的作用有:

  • 提供一個註解,達到Spring Boot自動配置、信息(當前應用的名稱和描述、z-job-admin平臺的IP和端口)寫在屬性上面的目的。
  • 提供開發定時任務時需要遵守的標準,即實現給定的接口。
  • 統一維護當前應用的所有定時任務。
  • 與z-job-admin平臺的交互(重點):即應用啓動時將地址信息註冊到z-job-admin平臺、接收來自z-job-admin平臺的任務調用請求並返回任務執行結果。

1、約定每個任務需要遵守的標準

我們定義標準的是接口,因爲這樣可以確定方法的簽名信息。此外,還需要這個類使用@Component註解將自己注入Spring容器,方便後續收集定時任務實例,並以beanName作爲任務名稱(自帶唯一標識功能)。

  • 入參:z-job-admin平臺發送過來的任務執行參數。
  • 返回:JobInvokeRsp,包含int類型的執行是否成功(code)和String類型的說明(msg)。
/**
 * 任務處理器接口
 *
 * @author z_hh
 */
public interface IJobHandler {

    JobInvokeRsp execute(String params) throws Exception;

}

2、加載配置信息

爲了方便和避免用戶忘記,我們定義一個所有屬性都非空的註解,用在Spring Boot啓動類上(而且,僅當該註解存在時,纔會開啓z-job並加載相關數據)。

  • adminIp:z-job-admin平臺的IP地址
  • adminPort:z-job-admin平臺的端口
  • appName:當前應用的名稱,在z-job-admin平臺裏面表示此應用的唯一標識
  • appDesc:當前應用的描述信息

爲了達到應用的啓動類存在EnableJobAutoConfiguration註解時才加載z-job的目的, 所以用Import註解的方式將兩個類註冊到Spring容器。

/**
 * 開啓任務自動配置
 *
 * @author z_hh
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({JobAutoConfigurationRegistrar.class, JobConfiguration.class})
public @interface EnableJobAutoConfiguration {

    String adminIp();

    int adminPort();

    String appName();

    String appDesc();
}

1)@EnableJobAutoConfiguration第一個Import的是JobAutoConfigurationRegistrar:該類用於讀取註解上面的配置信息,並手動註冊兩個bean到Spring容器。這兩個bean是

  • JobExecutor:這個類的作用是,應用啓動時將地址信息註冊到z-job-admin平臺、統一維護應用的所有定時任務實例、運行指定定時任務。註冊的同時將註解類的配置信息設置進去,並且執行init方法初始化。
  • JobInvokeServletRegistrar:這個類實際上是一個Servlet,用於接收來自z-job-admin平臺的調用任務請求,並且返回任務執行結果。註冊時使用newInstance工廠方法,並將jobExecutor這個bean注入。(爲啥不直接定義一個Controller?因爲控制不了當@EnableJobAutoConfiguration存在時才註冊到Spring容器的目的,條件註解都沒有用)。
/**
 * ImportBeanDefinitionRegistrar
 *
 * @author z_hh
 */
@Slf4j
public class JobAutoConfigurationRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    @Setter
    private Environment environment;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        AnnotationAttributes annotationAttributes = AnnotationAttributes
                .fromMap(annotationMetadata.getAnnotationAttributes(EnableJobAutoConfiguration.class.getName()));
        if (Objects.isNull(annotationAttributes)) {
            log.error("【任務調度平臺】EnableJobAutoConfiguration annotationAttributes is null!");
            return;
        }

        // 註冊JobExecutor
        registerJobExecutor(annotationAttributes, beanDefinitionRegistry);

        // 註冊Servlet
        registerJobInvokeServletRegistrationBean(beanDefinitionRegistry);

    }

    private void registerJobExecutor(AnnotationAttributes annotationAttributes, BeanDefinitionRegistry beanDefinitionRegistry) {
        // 創建配置實例
        JobProperties jobProperties = new JobProperties();
        jobProperties.setAdminIp(annotationAttributes.getString("adminIp"));
        jobProperties.setAdminPort(annotationAttributes.getNumber("adminPort"));
        jobProperties.setAppName(annotationAttributes.getString("appName"));
        jobProperties.setAppDesc(annotationAttributes.getString("appDesc"));
        jobProperties.setIp(NetUtil.getIp());
        jobProperties.setPort(environment.getProperty("server.port", Integer.class, 8080));

        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(JobExecutor.class)
                .setInitMethodName("init")
                .setDestroyMethodName("destroy")
                .addPropertyValue("jobProperties", jobProperties)
                .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
                .getBeanDefinition();

        beanDefinitionRegistry.registerBeanDefinition("jobExecutor", beanDefinition);
        log.info("【任務調度平臺】JobExecutor register success!");
    }

    private void registerJobInvokeServletRegistrationBean(BeanDefinitionRegistry beanDefinitionRegistry) {
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(JobInvokeServletRegistrar.class)
                .setFactoryMethod("newInstance")
                .addPropertyReference("jobExecutor", "jobExecutor")
                .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_NO)
                .getBeanDefinition();

        beanDefinitionRegistry.registerBeanDefinition("JobInvokeServlet", beanDefinition);
        log.info("【任務調度平臺】JobInvokeServletRegistrar register success!");
    }

}

JobExecutor類的代碼如下,init方法會做兩件事

  • 初始化所有JobHandler:將Spring容器裏面所有實現了IJobHandler接口的bean取出來,然後放到一個Map裏面,並以beanName爲鍵。
  • 將自己註冊到調度中心:新起一個線程,使用注入的restTemplate,根據註解配置的z-job-admin平臺地址信息,將當前應用信息(應用名稱和IP端口等)發送到任務調度平臺進行應用的自動註冊。那邊有專門的接口會做靈活處理(針對集羣現象),後面會說。
/**
 * 任務執行器
 *
 * @author z_hh
 */
@Slf4j
public class JobExecutor {

    @Setter
    private JobProperties jobProperties;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ApplicationContext applicationContext;

    private ConcurrentHashMap<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap();

    public IJobHandler registJobHandler(String name, IJobHandler jobHandler) {
        log.info("【任務調度平臺】成功註冊JobHandler >>>>>>>>>> name={},handler={}", name, jobHandler.getClass().getName());
        return (IJobHandler)jobHandlerRepository.put(name, jobHandler);
    }

    public IJobHandler loadJobHandler(String name) {
        return (IJobHandler)jobHandlerRepository.get(name);
    }

    public void init() {
        log.info("【任務調度平臺】JobExecutor init...");
        // 初始化所有JobHandler
        initJobHandler();

        // 將自己註冊到調度中心
        new RegisterAppToAdminThread().start();
    }

    public void destroy() {
        log.info("【任務調度平臺】JobExecutor destroy...");

    }

    public JobInvokeRsp jobInvoke(String name, String params) {
        IJobHandler jobHandler = jobHandlerRepository.get(name);
        if (Objects.isNull(jobHandler)) {
            return JobInvokeRsp.error("任務不存在!");
        }

        try {
            return jobHandler.execute(params);
        } catch (Exception e) {
            log.error("【任務調度平臺】任務{}調用異常:{}", name, e);
            return JobInvokeRsp.error("任務調用異常!");
        }
    }

    private void initJobHandler() {
        String[] beanNames = applicationContext.getBeanNamesForType(IJobHandler.class);
        if (beanNames == null || beanNames.length == 0) {
            return;
        }
        Arrays.stream(beanNames).forEach(beanName -> {
            registJobHandler(beanName, (IJobHandler)applicationContext.getBean(beanName));
        });
    }

    private class RegisterAppToAdminThread extends Thread {

        private  RegisterAppToAdminThread() {
            super("AppToAdmin-T");
        }

        @Override
        public void run() {
            log.info("【任務調度平臺】開始往調度中心註冊當前應用信息...");
            Map<String, Object> paramMap = new HashMap<>(4);
            paramMap.put("appName", jobProperties.getAppName());
            paramMap.put("appDesc", jobProperties.getAppDesc());
            paramMap.put("address", jobProperties.getIp() + ":" + jobProperties.getPort());
            try {
                restTemplate.postForObject("http://" + jobProperties.getAdminIp() + ":"
                                + jobProperties.getAdminPort() + "/api/job/app/auto_register",
                        paramMap,
                        Object.class);
                log.info("【任務調度平臺】應用註冊到調度中心成功!");
            } catch (Throwable t) {
                log.warn("【任務調度平臺】應用註冊到調度中心失敗:{}", ThrowableUtils.getThrowableStackTrace(t));
            }
        }
    }

}

JobInvokeServletRegistrar類的代碼如下,繼承ServletRegistrationBean類並提供newInstance的工廠方法用於創建Servlet實例注入到Spring容器。它將接收來自z-job-admin平臺的任務調用請求,並根據參數的任務名稱和執行參數利用JobExecutor去調用對應的定時任務執行,最後將結果寫入相應。

/**
 * 任務調度Servlet
 *
 * @author z_hh
 */
@Slf4j
public class JobInvokeServletRegistrar<JobInvokeServlet> extends ServletRegistrationBean {

    @Setter
    private JobExecutor jobExecutor;

    public JobInvokeServletRegistrar() {
        super();
    }

    public static JobInvokeServletRegistrar newInstance() {
        JobInvokeServletRegistrar jobInvokeServletRegistrar = new JobInvokeServletRegistrar();
        jobInvokeServletRegistrar.setServlet(jobInvokeServletRegistrar.new JobInvokeServlet());
        jobInvokeServletRegistrar.setUrlMappings(Collections.singletonList("/api/job/invoke"));
        jobInvokeServletRegistrar.setLoadOnStartup(1);

        return jobInvokeServletRegistrar;
    }

    private class JobInvokeServlet extends HttpServlet {

        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            resp.setHeader("Content-Type", "application/json");
            try {
                Pair reqAndRsp = run(req, resp);
                log.info("【任務調度平臺】執行作業:req={},rsp={}", reqAndRsp.getKey(), reqAndRsp.getValue());
            } catch (Throwable t) {
                String msg = ThrowableUtils.getThrowableStackTrace(t);
                log.warn("任務調用異常:{}", msg);
                String rspStr = JsonUtils.writeValueAsString(JobInvokeRsp.error(msg));
                resp.getOutputStream().write(rspStr.getBytes(Charset.defaultCharset()));
            }
        }

        private Pair run(HttpServletRequest req, HttpServletResponse resp) throws Throwable {
            // 反序列化
            ServletInputStream inputStream = req.getInputStream();
            byte[] body = new byte[req.getContentLength()];
            inputStream.read(body);
            JobInvokeReq jobInvokeReq = (JobInvokeReq)SerializationUtils.deserialize(body);

            // 調用任務
            JobInvokeRsp jobInvokeRsp = JobInvokeServletRegistrar.this.jobExecutor.jobInvoke(jobInvokeReq.getName(), jobInvokeReq.getParams());

            // 響應結果
            String rspStr = JsonUtils.writeValueAsString(jobInvokeRsp);
            resp.getOutputStream().write(rspStr.getBytes(Charset.defaultCharset()));

            // 返回請求和響應提供日誌記錄
            return new Pair(jobInvokeReq, jobInvokeRsp);
        }
    }
}

2)@EnableJobAutoConfiguration第二個Import的是JobConfiguration:它目前只有一個作用,當Spring容器不存在RestTemplate實例時,就創建一個註冊進去。

/**
 * 任務配置類
 *
 * @author z_hh
 */
@Slf4j
public class JobConfiguration {
    {
        log.info("【任務調度平臺】Loading JobAutoConfiguration!");
    }

    @Bean
    @ConditionalOnMissingBean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

 

此外,前面沒說到的類還有幾個,其中獲取當前機器IP地址的工具代碼如下

/**
 * 網絡工具類
 *
 * @author z_hh
 */
@Slf4j
public class NetUtil {

    private NetUtil() {}

    public static String getIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("獲取IP地址異常:", ThrowableUtils.getThrowableStackTrace(e));
            throw new RuntimeException("獲取IP地址異常!");
        }
    }

}

其它的話,可以去看對應的源碼。 

三、z-job-admin模塊

1、表結構,以及對應的增刪改查

表定義了3張,開始曾經想過:應用的信息能否直接放在任務裏面,後來覺得實在不妥,不太方便管理(參考過xxl-job的表哈,它們的也是這樣的,嘻嘻)。所以,3張表就是:任務應用表、任務信息表、任務調度記錄表。

1)任務應用表

CREATE TABLE `z_job_app` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_name` varchar(64) NOT NULL COMMENT '應用名稱',
  `app_desc` varchar(128) NOT NULL COMMENT '應用描述',
  `creator` varchar(32) NOT NULL COMMENT '創建人',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  `create_way` tinyint(1) NOT NULL COMMENT '創建方式:1-自動,2-手工',
  `update_time` datetime DEFAULT NULL COMMENT '最後更新時間',
  `address_list` varchar(512) NOT NULL COMMENT '應用地址列表,多個逗號分隔',
  `enabled` tinyint(1) NOT NULL COMMENT '啓用狀態:1-啓用,0-停用',
  `is_deleted` tinyint(1) NOT NULL COMMENT '是否刪除:1-是,0-否',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_app_name` (`app_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2)任務信息表

CREATE TABLE `z_job_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_app_id` int(11) NOT NULL COMMENT '任務所屬應用id',
  `job_name` varchar(64) NOT NULL COMMENT '任務名稱',
  `job_desc` varchar(512) DEFAULT '' COMMENT '任務描述',
  `alarm_email` varchar(512) DEFAULT '' COMMENT '報警郵件,多個逗號分隔',
  `creator` varchar(32) NOT NULL COMMENT '創建人',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  `create_way` tinyint(1) NOT NULL COMMENT '創建方式:1-自動,2-手工',
  `update_time` datetime DEFAULT NULL COMMENT '最後更新時間',
  `run_cron` varchar(128) NOT NULL COMMENT '任務執行CRON',
  `run_strategy` tinyint(1) NOT NULL COMMENT '任務執行策略:1-隨機,2-輪詢',
  `run_param` varchar(512) DEFAULT '' COMMENT '任務執行參數',
  `run_timeout` smallint(3) NOT NULL COMMENT '任務執行超時時間,單位秒',
  `run_fail_retry_count` smallint(3) NOT NULL COMMENT '任務執行失敗重試次數',
  `trigger_last_time` datetime DEFAULT NULL COMMENT '上次調度時間',
  `trigger_next_time` datetime DEFAULT NULL COMMENT '下次調度時間',
  `enabled` tinyint(1) NOT NULL COMMENT '啓用狀態:1-啓用,0-停用',
  `is_deleted` tinyint(1) NOT NULL COMMENT '是否刪除:1-是,0-否',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name_appid` (`job_name`,`job_app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3)任務調度記錄表

CREATE TABLE `z_job_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_id` int(11) NOT NULL COMMENT '任務ID',
  `run_address_list` varchar(512) NOT NULL COMMENT '本次運行的地址',
  `run_fail_retry_count` smallint(3) NOT NULL COMMENT '任務執行失敗重試次數',
  `trigger_start_time` datetime NOT NULL COMMENT '調度開始時間',
  `trigger_end_time` datetime NOT NULL COMMENT '調度結束時間',
  `trigger_result` tinyint(1) NOT NULL COMMENT '調度結果:1-成功,0-失敗',
  `trigger_msg` varchar(3000) DEFAULT '' COMMENT '調度日誌',
  `job_run_result` tinyint(1) DEFAULT '0' COMMENT '任務執行結果:1-成功,0-失敗',
  `job_run_msg` varchar(3000) DEFAULT '' COMMENT '任務執行日誌',
  PRIMARY KEY (`id`),
  KEY `idx_job_id` (`job_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4)增刪改查

使用spring-data-jpa,因爲簡單。該技術不在本文討論範圍內,你也可以使用Mybatis、甚至JDBC。

2、任務應用的自動註冊

前面z-job-core模塊說過,應用啓動時會將自己的信息註冊到任務調度平臺,這邊提供接口,並且會靈活處理。怎麼個靈活法呢?君請看,會判斷應用傳過來的應用名稱和地址信息

  • 應用名稱不存在,直接插入。
  • 應用名稱存在,手動新增的話提示重複,否則:如果地址也存在,忽略;如果地址不存在,合併地址。

PS:Controller層面需要加一個獨佔鎖保證併發時線程安全。

public Result<JobApp> insert(JobApp jobApp) {
        if (Objects.nonNull(jobApp.getId())) {
            jobApp.setId(null);
        }

        // 根據appName查詢數據
        JobApp exampleObj = new JobApp();
        exampleObj.setAppName(jobApp.getAppName());
        exampleObj.setIsDeleted(IsDeletedEnum.NO.getCode());
        Example<JobApp> example = Example.of(exampleObj);
        Optional<JobApp> JobAppOptional = dao.findOne(example);

        // 如果不存在,直接保存
        if (!JobAppOptional.isPresent()) {
            return Result.ok(save(jobApp));
        }

        // 如果是手動添加,提示重複
        if (Objects.equals(jobApp.getCreateWay(), CreateWayEnum.MANUAL.getCode())) {
            return Result.err("應用名稱已存在!");
        }

        // 比較地址
        JobApp existsJobApp = JobAppOptional.get();
        List<String> addressList = Arrays.stream(existsJobApp.getAddressList().split(","))
                .filter(StringUtils::hasText)
                .collect(Collectors.toList());
        // 存在,但是地址已經包含,忽略
        if (addressList.contains(jobApp.getAddressList())) {
            return Result.ok(existsJobApp);
        }
        // 存在,但是地址還沒包含,合併地址
        addressList.add(jobApp.getAddressList());
        String newAddressList = addressList.stream().reduce((s1, s2) -> s1 + "," + s2).orElse("");
        existsJobApp.setAddressList(newAddressList);
        return Result.ok(save(existsJobApp));
    }

3、Quartz Job的註冊以及移除(重點)

1)開始介紹z-job任務調度平臺架構的時候說過,使用Quartz作爲定時器的。在Spring Boot下怎麼使用呢?

引入依賴並註冊一個Scheduler的bean到Spring容器之後,就可以在需要的地方注入並使用了。

        <!--  作業調度框架Quartz  -->
		<dependency>
			<groupId>org.quartz-scheduler</groupId>
			<artifactId>quartz</artifactId>
			<version>${quartz-scheduler-version}</version>
		</dependency>
		<dependency>
			<groupId>org.quartz-scheduler</groupId>
			<artifactId>quartz-jobs</artifactId>
			<version>${quartz-scheduler-version}</version>
		</dependency>

 

@Bean
    public Scheduler scheduler() throws SchedulerException {
        return StdSchedulerFactory.getDefaultScheduler();
    }

2)新增任務或者將任務從停用改爲啓用時都需要註冊一個定時作業。除此之外,任務調度平臺啓動的時候也需要將啓用狀態的任務進行註冊。當任務停用的時候,需要將對應的定時作業移除。

定時作業:所有的Quartz Job都使用一個類相同的邏輯,執行的時候根據傳遞進來的任務信息和它的應用信息,通過JobInvoker調度器發起遠程調用,如果失敗了會根據任務的重試機制進行對應的處理,終究沒成功的話會發送告警郵件,最後將整個調度過程插入到任務調度記錄裏,並修改任務的下一次調度時間。

/**
 * Quartz任務
 *
 * @author z_hh
 */
@Slf4j
public class QuartzJob implements Job {

    private static final byte SUCCESS = 1;
    private static final byte ERROR = 0;

    private JobInvoker jobInvoker;

    private MailSendService mailSendService;

    public QuartzJob() {
        this.jobInvoker = BeanUtils.getBean(JobInvoker.class);
        this.mailSendService = BeanUtils.getBean(MailSendService.class);
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        JobApp jobApp = (JobApp) jobDataMap.get("JobApp");
        JobInfo jobInfo = (JobInfo) jobDataMap.get("jobInfo");
        log.info("任務{}開始調度...", jobInfo.getJobName());

        JobLog jobLog = new JobLog();
        jobLog.setJobId(jobInfo.getId());
        jobLog.setTriggerStartTime(new Date());
        try {
            jobRun(jobApp, jobInfo, jobLog);
            jobLog.setTriggerResult((byte)1);
            jobLog.setTriggerMsg("調度成功!");
        } catch (Throwable t) {
            String msg = ThrowableUtils.getThrowableStackTrace(t);
            log.warn("任務{}調度出現異常:{}", jobInfo.getJobName(), msg);
            jobLog.setTriggerResult((byte)0);
            jobLog.setTriggerMsg("調度異常:" + msg);
        }
        jobLog.setTriggerEndTime(new Date());
        // 記錄任務的本次和下次調用時間
        jobInfo.setTriggerLastTime(jobExecutionContext.getFireTime());
        jobInfo.setTriggerNextTime(jobExecutionContext.getNextFireTime());

        // 插入日誌
        jobLog.save();

        // 更新任務
        jobInfo.save();

        // 發送郵件
        sendMail(jobApp, jobInfo, jobLog);

        log.info("任務{}調度結束!", jobInfo.getJobName());
    }

    private void jobRun(JobApp jobApp, JobInfo jobInfo, JobLog jobLog) {

        List<String> addressList = Arrays.stream(jobApp.getAddressList().split(","))
                .filter(StringUtils::hasText)
                .collect(Collectors.toList());
        Iterator<String> iterator = addressList.iterator();
        JobInvokeRsp jobInvokeRsp = null;
        List<String> hasInvokeAddress = new ArrayList<>();
        int failRetryCount = jobInfo.getRunFailRetryCount(),
                readyRetryCount = -1;
        while (iterator.hasNext() && ++readyRetryCount <= failRetryCount) {
            String address = iterator.next();
            hasInvokeAddress.add(address);
            try {
                jobInvokeRsp = jobInvoker.invoke(address, jobInfo.getJobName(), jobInfo.getRunParam());
                if (jobInvokeRsp.isOk()) {
                    break;
                }
                log.warn("調用{}的{}任務失敗:{}", address, jobInfo.getJobName(), jobInvokeRsp.getMsg());
            } catch (Throwable t) {
                String msg = ThrowableUtils.getThrowableStackTrace(t);
                log.warn("調用{}的{}任務時出現異常:{}", address, jobInfo.getJobName(), msg);
                jobInvokeRsp = JobInvokeRsp.error("任務調用異常:" + msg);
            }
            iterator.remove();
        }
        if (Objects.isNull(jobInvokeRsp)) {
            jobInvokeRsp = JobInvokeRsp.error("沒有進行任務調用!");
        }
        jobLog.setJobRunResult(jobInvokeRsp.getCode());
        jobLog.setJobRunMsg(sub3000String(jobInvokeRsp.getMsg()));

        jobLog.setRunFailRetryCount(Objects.equals(readyRetryCount, -1) ? 0 : readyRetryCount);
        jobLog.setRunAddressList(hasInvokeAddress.stream().reduce((s1, s2) -> s1 + "," + s2).orElse(""));
    }

    private void sendMail(JobApp jobApp, JobInfo jobInfo, JobLog jobLog) {
        // 調度失敗並且郵件不爲空
        if ((Objects.equals(jobLog.getTriggerResult(), (byte)0)
                || Objects.equals(jobLog.getJobRunResult(), (byte)0))
                && StringUtils.hasText(jobInfo.getAlarmEmail())) {
            String alarmEmailStr = jobInfo.getAlarmEmail();
            List<String> mailList = null;
            if (alarmEmailStr.contains(",")) {
                mailList = Arrays.asList(alarmEmailStr.split(","));
            } else {
                mailList = Collections.singletonList(alarmEmailStr);
            }
            String subject = String.format("應用%s的%s任務調度失敗!", jobApp.getAppName(), jobInfo.getJobName());
            try {
                String content = new ObjectMapper().writeValueAsString(jobLog);
                mailList.forEach(m -> {
                    mailSendService.sendSimpleMail(m, subject, content);
                });
            } catch (JsonProcessingException e) {
                log.error("任務調度失敗告警郵件發送失敗:{}", ThrowableUtils.getThrowableStackTrace(e));
            }
        }
    }

    private String sub3000String(String str) {
        if (StringUtils.hasText(str) && str.length() > 3000) {
            return str.substring(0, 2888) + "...更多請查看日誌記錄!";
        }
        return str;
    }
}

任務調度器:使用RestTemplate將任務名稱和執行參數發送給具體的應用。

/**
 * 任務調度器
 *
 * @author z_hh
 */
@Component
public class JobInvoker {

    @Autowired
    private RestTemplate restTemplate;

    private static final String PREFIX = "http://";

    private static final String PATH = "/api/job/invoke";

    /**
     * 任務調度器
     *
     * @param url 目標地址
     * @param jobHandler 任務名稱
     * @param params 執行參數
     * @return 調用任務結果
     */
    public JobInvokeRsp invoke(String url, String jobHandler, String params) {
        JobInvokeReq req = new JobInvokeReq();
        req.setName(jobHandler);
        req.setParams(params);

        byte[] dataBytes = SerializationUtils.serialize(req);
        return restTemplate.postForObject(PREFIX + url + PATH, dataBytes, JobInvokeRsp.class);
    }
}

發送郵件:引入依賴,配置發送方郵箱,編寫發送服務。

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mail</artifactId>
		</dependency>
  # 發送郵箱配置
  mail:
    # QQ郵箱主機
    host: smtp.qq.com
    # 用戶名
    username: [email protected]
    # QQ郵箱開啓SMTP的授權碼
    password: 000xxx

 

/**
 * 郵件發送服務
 *
 * @author z_hh
 */
@Component
public class MailSendService {

    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    /**
     * 發送普通郵件
     *
     * @param to 收件人
     * @param subject 郵件主題
     * @param content 郵件內容
     * @throws MailException
     */
    public void sendSimpleMail(String to, String subject, String content) throws MailException {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(content);

        mailSender.send(message);
    }
}

註冊定時作業:首先根據任務的名稱、描述以及所屬應用名稱使用QuartzJob類構建一個JobDetail實例,然後根據任務的執行計劃構建Trigger實例,接着將任務信息、應用信息傳遞到JobDetail實例的JobDataMap裏,最後使用scheduler註冊到作業調度中。

public Result register(JobInfo jobInfo) {
        // 獲取任務組信息
        Result<JobApp> JobAppResult = JobAppService.getById(jobInfo.getJobAppId());
        if (JobAppResult.isErr()) {
            return JobAppResult;
        }
        JobApp JobApp = JobAppResult.get();

        // 創建jobDetail實例
        JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                .withIdentity(jobInfo.getJobName(), JobApp.getAppName())
                .withDescription(jobInfo.getJobDesc())
                .build();

        // 定義調度觸發規則corn
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobInfo.getJobName(), JobApp.getAppName())
                .startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
                .withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getRunCron()))
                .startNow()
                .build();

        // 傳遞一些數據到任務裏面
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        jobDataMap.put("JobApp", JobApp);
        jobDataMap.put("jobInfo", jobInfo);

        // 把作業和觸發器註冊到任務調度中
        try {
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            log.error("註冊任務異常:{}", ThrowableUtils.getThrowableStackTrace(e));
            return Result.err("註冊任務異常!");
        }

        // 啓動
        try {
            if (!scheduler.isShutdown()) {
                scheduler.start();
            }
        } catch (SchedulerException e) {
            log.error("啓動scheduler異常:{}", ThrowableUtils.getThrowableStackTrace(e));
            return Result.err("啓動scheduler異常!");
        }

        return Result.ok();
    }

移除定時作業:很簡單,根據任務的名稱以及它的應用名稱組成一個JobKey,然後使用scheduler進行刪除即可。

public Result disable(Long id) {
        // 查詢和校驗
        Result<JobInfo> jobInfoResult = getById(id);
        if (jobInfoResult.isErr()) {
            return jobInfoResult;
        }
        JobInfo jobInfo = jobInfoResult.get();
        if (Objects.equals(jobInfo.getEnabled(), EnabledEnum.NO.getCode())) {
            return Result.err("任務已經處於停用狀態!");
        }

        // 查詢對應任務組
        Result<JobApp> JobAppResult = JobAppService.getById(jobInfo.getJobAppId());
        if (JobAppResult.isErr()) {
            return JobAppResult;
        }

        // 從scheduler移除任務
        JobApp jobApp = JobAppResult.get();
        JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobApp.getAppName());
        try {
            boolean deleteJobResult = scheduler.deleteJob(jobKey);
            if (!deleteJobResult) {
                return Result.err("停用定時任務失敗!");
            }
        } catch (SchedulerException e) {
            log.error("停用定時任務異常:{}", ThrowableUtils.getThrowableStackTrace(e));
            return Result.err("停用定時任務異常!");
        }

        // 修改數據狀態
        jobInfo.setEnabled(EnabledEnum.NO.getCode());
        jobInfo.setUpdateTime(new Date());
        save(jobInfo);

        return Result.ok("停用定時任務成功!");
    }

任務調度平臺啓動時將啓用的任務註冊到作業調度中

/**
 * 應用啓動後註冊定時任務
 *
 * @author z_hh
 */
@Component
@Slf4j
public class RegisterJobOnAppStart implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private JobInfoService jobInfoService;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        jobInfoService.queryAll().stream()
            .filter(jobInfo ->
                Objects.equals(jobInfo.getIsDeleted(), IsDeletedEnum.NO.getCode())
                    && Objects.equals(jobInfo.getEnabled(), EnabledEnum.YES.getCode())
            )
            .forEach(jobInfo -> {
                Result registerResult = jobInfoService.register(jobInfo);
                if (registerResult.isErr()) {
                    log.error("任務[id={},jobName={}]註冊失敗:{}", jobInfo.getId(), jobInfo.getJobName(), registerResult.getMsg());
                }
                log.info("任務[id={},jobName={}]註冊成功!", jobInfo.getId(), jobInfo.getJobName());
            });
    }
}

好像沒有特別重要的了,其它的話找源碼看一下吧? 

四、z-job-example模塊

一切就緒之後,就可以編寫一個示例爽一把了,這是最開心的時刻。

1、引入z-job-core的依賴

<dependency>
            <groupId>cn.zhh</groupId>
            <artifactId>z-job-core</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

2、啓動類加上EnableJobAutoConfiguration註解並配置任務調度平臺的IP和端口,以及當前應用的名稱和描述

@EnableJobAutoConfiguration(adminIp = "127.0.0.1",
	adminPort = 8888,
	appName = "example",
	appDesc = "示例應用")

3、編寫一個定時任務類,實現IJobHandler接口並重寫execute方法,加上Component註解

/**
 * 示例任務1
 *
 * @author z_hh
 */
@Component
public class JobExample1 implements IJobHandler {

    @Override
    public JobInvokeRsp execute(String params) throws Exception {
        return JobInvokeRsp.success("我執行成功啦!收到參數:" + params);
    }
}

4、首先啓動z-job-admin項目,然後啓動項目,看到控制檯輸出。應用自動註冊到任務調度平臺

5、任務調度平臺添加對應的任務信息。默認啓用狀態,它會自動註冊到Quartz作業調度中

6、時間到了之後會執行任務調度

7、查看調度日誌

至此,z-job整個核心流程已經開發完成了。

五、前端技術

前端使用的主要框架有Bootstrap、JQuery以及Vue。不是我擅長的領域,你們看一下代碼就好。

六、項目涉及的其它技術

除了介紹核心代碼以外,還有一些個人覺得是亮點的分享一下。

1、定義通用結果返回對象,並使用Aspect切面處理帶來的事務問題。

一般是Controller層使用的。如果是Service層使用,就要考慮事務回滾的問題(因爲Spring的Transactional是默認拋出RuntimeException時才觸發事務回滾的),一般推薦拋出自定義業務異常並結合統一異常處理器進行轉化處理

/**
 * 通用結果返回對象
 *
 * @author z_hh
 */
@ToString
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 6547662806723050209L;
    private static final int SUCCESS = 200;
    private static final int ERROR = 500;

    @Getter
    private Integer code;
    @Getter
    private String msg;
    @Getter
    private T content;

    private Result(Integer code, String msg, T content) {
        this.code = code;
        this.msg = msg;
        this.content = content;
    }

    public static <T> Result<T> ok() {
        return new Result<>(SUCCESS, null, (T)null);
    }

    public static <T> Result<T> ok(String msg) {
        return new Result<>(SUCCESS, msg, (T)null);
    }

    public static <T> Result<T> ok(T content) {
        return new Result<>(SUCCESS, null, content);
    }

    public static <T> Result<T> ok(String msg, T content) {
        return new Result<>(SUCCESS, msg, content);
    }

    public static <T> Result<T> err() {
        return new Result<>(ERROR, null, (T)null);
    }

    public static <T> Result<T> err(String msg) {
        return new Result<>(ERROR, msg, (T)null);
    }

    public boolean isOk() {
        return Objects.equals(this.code, SUCCESS);
    }

    public boolean isErr() {
        return Objects.equals(this.code, ERROR);
    }

    public T get() {
        if (isErr()) {
            throw new UnsupportedOperationException("result is error!");
        }
        return this.content;
    }
}

如果Service層使用,並需要返回Result.ok() == false時進行事務回滾,那麼就需要結合Aspect切面進行手工回滾處理了。

/**
 * 對Spring的事務註解@Transactional做進一步處理,
 * 結合Service的返回值類型Result,做出是否啓動事務回滾
 *
 * @author z_hh
 */
@Aspect
@Component
public class TransactionalAspect {

    @Around(value = "@annotation(org.springframework.transaction.annotation.Transactional)&&@annotation(transactional)")
    public Object verify(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable {

        // 執行切面方法,獲得返回值
        Object result = pjp.proceed();

        // 檢測&強行回滾
        boolean requireRollback = requireRollback(result);
        if (requireRollback) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        // 返回切面方法運行結果
        return result;
    }

    private static boolean requireRollback(Object result) throws Exception {
        // 如果方法返回值不是Result對象,則不需要回滾
        if (!(result instanceof Result)) {
            return false;
        }

        // 如果result.isOk() == true,也不需要回滾
        Result r = (Result) result;
        if (!r.isOk()) {
            return false;
        }

        // 如果@Transactional啓用了新事物(propagation = Propagation.REQUIRES_NEW),需要回滾
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus().isNewTransaction();
        if (isNewTransaction) {
            return true;
        }

        // 如果方法沒有被其它@Transactional註釋的方法嵌套調用,說明該線程的事物已運行完畢,則需要回滾
        //  此處使用了較多的反射底層語法,強行訪問Spring內部的private/protected 方法、字段,存在一定的風險
        Object currentTransactionInfo = executePrivateStaticMethod(TransactionAspectSupport.class, "currentTransactionInfo"),
                oldTransactionInfo = getPrivateFieldValue(currentTransactionInfo, "oldTransactionInfo");
        if (oldTransactionInfo == null) {
            return true;
        }

        // 其它情況,不回滾
        return false;
    }

    private static Object getPrivateFieldValue(Object target, String fieldName) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(target);
    }

    private static Object executePrivateStaticMethod(Class<?> targetClass, String methodName) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Method method = targetClass.getDeclaredMethod(methodName);
        method.setAccessible(true);
        return method.invoke(null);
    }
}

2、Swagger2的正確使用姿勢

怎麼使用我就不說了,就講下怎麼樣可以讓Controller看起來舒服點。處理方式就是,每個Controller定義一個Api接口,將註解信息寫在接口上面。如

/**
 * 任務信息API
 *
 * @author z_hh
 */
@Api(tags = "任務信息API")
public interface JobInfoApi {

    @ApiOperation("分頁查詢任務")
    @GetMapping("/page_query")
    public Result<Page<JobInfoPageQueryRsp>> pageQuery(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
                                                       @RequestParam(required = false, defaultValue = "10") Integer pageSize);
}

 

/**
 * 任務信息控制器
 *
 * @author z_hh
 */
@RestController
@RequestMapping("/job/info")
@Slf4j
public class JobInfoController implements JobInfoApi {

    @Autowired
    private JobInfoService jobInfoService;

    @Override
    public Result<Page<JobInfoPageQueryRsp>> pageQuery(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
                                                       @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
        Result<Page<JobInfoPageQueryRsp>> pageResult = jobInfoService.queryByPage(pageNum, pageSize);
        return pageResult;
    }
}

3、利用Aspect切面優雅對validation的請求參數進行校驗

如果是在Controller層進行校驗,那麼可以直接在方法的參數前面加@Valid註解,然後定義全局統一異常處理器處理MethodArgumentNotValidException即可。這裏是給假如要在Service層做校驗的提供一種思路。

我的相關博客:https://blog.csdn.net/qq_31142553/article/details/89430100https://blog.csdn.net/qq_31142553/article/details/86547201https://blog.csdn.net/qq_31142553/article/details/85645957

1)定義一個標記接口(什麼也沒有,類似Serializable),讓需要校驗的類實現。

/**
 * 需要校驗的請求對象
 *
 * @author z_hh
 */
public interface ValidateReq {
}
/**
 * 添加任務應用請求
 *
 * @author z_hh
 */
@ApiModel("添加任務應用請求")
@Data
public class JobAppAddReq implements ValidateReq {

    @ApiModelProperty(value = "應用名稱", required = true)
    @NotBlank(message = "應用名稱不能爲空")
    private String appName;
}

2)定義一個Aspect切面,在控制層對這些請求對象進行校驗。

這裏可以使用@Around環繞切面,如果校驗不通過,直接返回錯誤的Result結果,那麼就沒有第三步了。

/**
 * 校驗請求對象切面
 *
 * @author z_hh
 */
@Aspect
@Component
public class ValidateReqAspect {

    private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Before("execution(* cn.zhh.admin.controller.*.*(..))")
    public void validateReq(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();

        for(int i = 0, length = args.length; i < length; ++i) {
            Object obj = args[i];
            if (obj instanceof ValidateReq) {
                validate(obj);
            }
        }
    }

    private <T> void validate(T t) {
        // 校驗對象
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t, new Class[0]);
        // 存在校驗錯誤的話,拼接所有錯誤信息
        if (constraintViolations.size() > 0) {
            StringBuilder validateError = new StringBuilder();

            ConstraintViolation constraintViolation;
            for(Iterator iterator = constraintViolations.iterator(); iterator.hasNext(); validateError.append(constraintViolation.getMessage()).append(";")) {

                constraintViolation = (ConstraintViolation)iterator.next();
            }
            // 拋出異常,統一異常處理器將會處理
            throw new IllegalArgumentException(validateError.toString());
        }
    }

}

3)定義全局統一異常處理器將IllegalArgumentException轉化爲錯誤的Result對象。

/**
 * 全局異常處理器
 *
 * @author z_hh
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 處理Throwable異常
     * @param t 異常對象
     * @return 統一Result
     */
    @ExceptionHandler(Throwable.class)
    public Result handleThrowable(Throwable t) {
        String msg = ThrowableUtils.getThrowableStackTrace(t);
        log.error("統一處理未知異常:{}", msg);
        return Result.err(msg);
    }

    /**
     * 處理非法參數異常
     * @param e 異常對象
     * @return 統一Result
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Result handleIllegalArgumentException(IllegalArgumentException e) {
        log.error("統一處理非法參數異常:{}", ThrowableUtils.getThrowableStackTrace(e));
        return Result.err(e.getMessage());
    }
}

4、Spring Data Jpa的Active Record模式實現

我的相關博客:https://blog.csdn.net/qq_31142553/article/details/82959626

Active Record,即AR模式,就是讓實體對象本身具備數據庫操作(增刪改查)的能力。違反了低耦合的設計原則,但有時候使用確實方便。

1)定義一個公共實體基類,子類繼承時需要在泛型上面寫入自己的類型以及主鍵類型、對應DAO的類型。

這裏面會根據DAO泛型找到對應的Dao Bean實例以實現對數據庫的操作。然後,子類可以選擇重寫獲取主鍵值的方法,如果沒有覆蓋,就會通過反射找到有@ID註解的那個字段取值。

/**
 * AR模式的實體基類
 *
 * @author z_hh
 */
public class ActiveRecord<T extends ActiveRecord, ID, DAO> {

    private JpaRepository<T, ID> jpaRepository;

    /**
     * 達到延遲加載的效果
     *
     * @return dao對象
     */
    private JpaRepository<T, ID> dao() {
        return Optional.ofNullable(jpaRepository).orElseGet(() -> {
            Type type = this.getClass().getGenericSuperclass();
            Type[] parameter = ((ParameterizedType) type).getActualTypeArguments();
            Class<DAO> daoClazz = (Class<DAO>)parameter[2];
            if (daoClazz.isAnnotationPresent(Repository.class)) {
                Repository annotation = daoClazz.getAnnotation(Repository.class);
                return jpaRepository = (JpaRepository<T, ID>) BeanUtils.getBean(annotation.value());
            }
            String clazzName = daoClazz.getSimpleName();
            String beanName = clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1);
            return jpaRepository = (JpaRepository<T, ID>) BeanUtils.getBean(beanName);
        });
    }

    /**
     * 保存this對象
     *
     * @return 保存過的對象
     */
    public T save() {
        return dao().save((T)this);
    }

    /**
     * 根據this的主鍵刪除數據
     */
    public void deleteById() {
        dao().deleteById(pkVal());
    }

    /**
     * 通過this構造Example進行查詢
     *
     * @return 結果列表
     */
    public List<T> findAllByExample() {
        return dao().findAll(Example.of((T)this));
    }

    /**
     * 根據this的主鍵查詢數據
     *
     * @return Optional對象
     */
    public Optional<T> findById() {
        return dao().findById(pkVal());
    }

    /**
     * 推薦子類重寫該方法,返回主鍵的值
     *
     * @return
     */
    protected ID pkVal() {
        return Arrays.stream(this.getClass().getDeclaredFields())
            .filter(f -> f.isAnnotationPresent(Id.class))
            .map(f -> {
                f.setAccessible(true);
                return (ID) ReflectionUtils.getField(f, this);
            })
            .findAny()
            .orElse((ID)null);

    }
}

2)子類繼承基類,並設置上面的泛型參數。

/**
 * 任務應用
 *
 * @author z_hh
 */
@Data
@Entity(name = "z_job_app")
public class JobApp extends ActiveRecord<JobApp, Long, JobAppDao> implements Serializable {
  private static final long serialVersionUID = 1L;

  /**
   * id
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
}

3)這樣,一個實體類就具備了增刪改查的能力。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ActiveRecordTest {
    @Test
    public void testSave() throws Exception {
        JobApp jobApp = new JobApp();
        jobApp.setAppName("example");
        jobApp.setAppDesc("實例應用");
        jobApp.setCreator("ZHH");
        jobApp.setCreateTime(new Date());
        jobApp.setCreateWay((byte) 0);
        jobApp.setAddressList("127.0.0.1:8080");
        jobApp.setEnabled((byte) 0);
        jobApp.setIsDeleted((byte) 0);
        jobApp = jobApp.save();
        Assert.assertNotNull(jobApp.getId());
    }

    @Test
    public void testFindAllByExample() {
        JobApp jobApp = new JobApp();
        jobApp.setId(1L);
        List<JobApp> jobAppList = jobApp.findAllByExample();
        Assert.assertFalse(jobAppList.isEmpty());
    }

    @Test
    public void findById() {
        JobApp jobApp = new JobApp();
        jobApp.setId(1L);
        Optional<JobApp> jobAppOptional = jobApp.findById();
        Assert.assertTrue(jobAppOptional.isPresent());
    }

}

寫了大半天終於搞完了?,有什麼問題歡迎在評論區留言哦!

本文項目Github地址:https://github.com/zhouhuanghua/z-job

本文項目源碼CSDN下載地址:https://download.csdn.net/download/qq_31142553/11330231

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章