mybatis mapper文件url is not_Spring Boot 实战:整合 MyBatis、Quartz

v2-4511d48f14a349305d098fc99d3c9094_1440w.jpg?source=172ae18b

本场 Chat 是一个小实例,这是属于真实项目中的一个定时任务管理的一个小模块。通过集成 Spring Boot、MyBatis、Quartz 简单实现的一个定时任务动态管理的功能。整个整合过程包含如下内容:

  1. 整合 MyBatis
  2. 整合 Quartz
  3. 整合 Druid
  4. Spring 单元测试集成 H2 数据库

整合 MyBatis

添加依赖

<!-- MyBatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis-spring-boot-starter.version}</version>
</dependency>

<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

添加 application.properties 配置

主要添加数据库连接配置和 MyBatis 的 Mapper 的 XML 配置文件路径以及实体类的包。还有一些 MyBatis 的相关配置:MyBatis 相关配置参数参考。

# 配置数据库连接
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/taskmgr?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456

# MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.auto-mapping-unknown-column-behavior=warning
mybatis.configuration.use-generated-keys=true

mybatis.type-aliases-package=com.pingan.wechat.app.entity
mybatis.mapper-locations=classpath:mapper/*.xml

整合通用 Mapper 和分页插件

使用原始的 MyBatis 有个问题就是,每个实体类的通用 CURD 操作等都需要自己写 XML 配置文件,或者在对应的Mapper 文件中写对应的 @Select/@Insert 等注解来实现对应的功能。这是个特别耗时的重复工作。解决这个问题有两种方式:

  1. 使用 MyBatis Generator 自动生成对应的 XML 文件
  2. 集成通用 Mapper

在业务较简单的时候,通常使用不到太多的复杂查询,这个时候集成通用 Mapper 相对简单合适。通用 Mapper 的作者本身也有写两种方式的对比。

MyBatis 通用 Mapper3 文档

另外,为了方便使用支持物理分页,也需要集成分页插件 MyBatis_PageHelper。

添加依赖

<!-- mapper -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>${mapper-spring-boot-starter.version}</version>
</dependency>
<!-- pagehelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper-spring-boot-starter.version}</version>
</dependency>

编写 CommonMapper 接口

编写基本的 CommonMapper 接口,继承通用 Mapper 的接口 Mapper<T>MySqlMapper<T>SelectByIdsMapper<T>DeleteByIdsMapper<T>,其中有些接口只适用于特定的数据库,需要根据实际情况做调整。 其他的业务相关的 Mapper 则需要继承这个 CommonMapper<T> 接口。

对应 Service 层基本 Service 接口和实现,则根据自身需要考虑是否需要。

import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;
import tk.mybatis.mapper.common.ids.DeleteByIdsMapper;
import tk.mybatis.mapper.common.ids.SelectByIdsMapper;

/**
 * 支持单表CURD和批量(MYSQL)操作的通用Mapper.
 * 
 * Created by vioao.
 */
public interface CommonMapper<T> extends Mapper<T>, MySqlMapper<T>, SelectByIdsMapper<T>, DeleteByIdsMapper<T> {

}

添加 application.properties 配置

# 这里配置自己写的基本的 CommonMapper
mapper.mappers=com.xx.xxx.app.mapper.CommonMapper
mapper.not-empty=false
mapper.identity=MYSQL

# PageHelper 插件配置
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql

整合 Quartz

整合 Quartz 时最主要会遇到两个问题:

  1. 实现了 Job 接口的 Quartz 任务类中无法注入使用 Spring 管理的 Bean
  2. 使用 Spring AOP 编程在实现了 Job 接口的 Quartz 任务类中无效,即 Quartz AOP 失效

其实上面两个问题的根本都是因为 Job 实例的创建和管理没有交给 Spring 来管理。下面给出可以解决上述两个问题的整合方式:

添加依赖

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

<!-- quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>${quartz.version}</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>${quartz.version}</version>
</dependency>

编写自定义配置类和自定义 JobFactory

JobFactoy 是 Quartz 提供的一个接口,其作用是用于创建 Job 实例,对应的在 Spring Quartz 中的实现类有 AdaptableJobFactorySpringBeanJobFactory,其中 AdaptableJobFactory 继承于 SpringBeanJobFactory,我们看下其源码:

public interface JobFactory {
    Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException;
}

public class AdaptableJobFactory implements JobFactory {

    @Override
    public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {
        try {
            // 创建Job实例
            Object jobObject = createJobInstance(bundle);
            return adaptJob(jobObject);
        }
        catch (Exception ex) {
            throw new SchedulerException("Job instantiation failed", ex);
        }
    }

    /**
     * 这里可以看到,Job 实例的创建都是通过 getJobClass().newInstance() 来创建的。
     * 并没有对应的代理类的创建。所以使用 Spring AOP 编程的时候所有需要被代理的 Job 任务
     * 实际上都不会有代理类生成,是无法使用 Spring AOP 编程的
     */    
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        return bundle.getJobDetail().getJobClass().newInstance();
    }

    protected Job adaptJob(Object jobObject) throws Exception {
        if (jobObject instanceof Job) {
            return (Job) jobObject;
        }
        else if (jobObject instanceof Runnable) {
            return new DelegatingJob((Runnable) jobObject);
        }
        else {
            throw new IllegalArgumentException("Unable to execute job class [" + jobObject.getClass().getName() +
                    "]: only [org.quartz.Job] and [java.lang.Runnable] supported.");
        }
    }
}



public class SpringBeanJobFactory extends AdaptableJobFactory implements SchedulerContextAware {

    //此处省略部分代码 ...

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
         // 可以看到 SpringBeanJobFactory 中是使用父类 AdaptableJobFactory 的方法来创建 Job 实例的,所以也不会有代理类的创建
        Object job = super.createJobInstance(bundle);
        if (isEligibleForPropertyPopulation(job)) {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
            MutablePropertyValues pvs = new MutablePropertyValues();
            if (this.schedulerContext != null) {
                pvs.addPropertyValues(this.schedulerContext);
            }
            pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
            pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
            if (this.ignoredUnknownProperties != null) {
                for (String propName : this.ignoredUnknownProperties) {
                    if (pvs.contains(propName) && !bw.isWritableProperty(propName)) {
                        pvs.removePropertyValue(propName);
                    }
                }
                bw.setPropertyValues(pvs);
            }
            else {
                bw.setPropertyValues(pvs, true);
            }
        }
        return job;
    }

     //此处省略部分代码 ...

}

可以发现现有的 Spring 中对 JobFactory 的实现类都无法实现我们的 AOP 编程需求,所以就需要自定义一个 JobFactory 实现类了。

自定义 JobFactory 实现类 CustomSpringBeanJobFactory

/**
     * 自定义JobFactory,将创建Job实例的操作交给Spring管理.
     * 
     * Created by vioao.
     */
    public class CustomSpringBeanJobFactory extends AdaptableJobFactory {
        @Autowired
        private AutowireCapableBeanFactory beanFactory;

        @Override
        protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
            Object jobInstance;
            Class<? extends Job> jobClass = bundle.getJobDetail().getJobClass();

            // 这里将Job的实例创建喝管理交给 Spring,
            // 使用 Spring 的 beanFactory 去获取 Job 实例,
            // 获取不到的话就交由 Spring 的 beanFactory 自动创建一个
            // 并根据名称自动注入和检查依赖关系
            // 这样的话 Job 中就可以实现自动注入和实现 AOP 编程
            try {
                jobInstance = beanFactory.getBean(jobClass);
            } catch (Exception e) {
                jobInstance = beanFactory.createBean(jobClass, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, true);
            }
            return jobInstance;
        }
    }

自定义 Quartz 配置:

/**
     * Quartz 配置类.
     * 
     * Created by vioao.
     */
    @Configuration
    public class QuartzConfiguration {
        private static final String QUARTZ_CONFIG = "quartz.properties";

        @Bean
        public CustomSpringBeanJobFactory customSpringBeanJobFactory(){
            return new CustomSpringBeanJobFactory();
        }

        @Bean
        public SchedulerFactoryBean schedulerFactoryBean() {
            SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

            // 配置使用自定义的 JobFactory
            schedulerFactoryBean.setJobFactory(customSpringBeanJobFactory());
            schedulerFactoryBean.setAutoStartup(true);
            // 设置 Quartz 配置文件路径
            schedulerFactoryBean.setConfigLocation(new ClassPathResource(QUARTZ_CONFIG));
            return schedulerFactoryBean;
        }

        @Bean
        public Scheduler scheduler() {
            return schedulerFactoryBean().getScheduler();
        }
    }

实现动态定时任务管理(根据数据库的配置动态调整)

实现定时任务动态管理主要有两个点:

  • 定时任务数据源从数据库中读取
  • 服务启动时初始化并启动所有符合条件的定时任务

其关键就是设计定时任务的数据库表,将数据库的数据作为定时任务的数据来源,然后使用 Scheduler 的相关 API 重新设置对应的任务的调度。

1. 设计定时任务调度表并实现对应的 CURD

因为只是一个系统中的简单定时任务管理模块,所以设计比较简单,对应的 CURD 代码就不贴出来了。Entity 定义如下:

@Entity
    @Daata
    public class QuartzTaskEntity {
        private Long id;
        private String taskName;
        private String taskClass;
        private String taskGroup;
        private Integer state;
        private String cron;
    }

2. 实现动态调度代码

/**
     * 定时任务调度器.
     * 
     * Created by vioao.
     */
    @Component
    public class TaskSchedule {
        private static final Logger LOGGER = LoggerFactory.getLogger(TaskSchedule.class);

        @Autowired
        private Scheduler scheduler;

        @Autowired
        private QuartzTaskService quartzTaskService;

        public ApiResult scheduleTask(QuartzTaskEntity task) {
            boolean scheduled = false;
            String msg = "Schedule Success!";
            try {
                Class<?> jobClass = Class.forName(task.getTaskClass());
                if (Job.class.isAssignableFrom(jobClass)) {
                    JobDetail jobDetail = buildJobDetail(task, (Class<? extends Job>) jobClass);
                    Trigger trigger = buildTrigger(task);

                    scheduler.scheduleJob(jobDetail, trigger);
                    scheduled = true;
                    LOGGER.info(msg + "");
                }
            } catch (ClassNotFoundException e) {
                msg = "Schedule Fail! Class not found!";
                LOGGER.error(msg + "Task: " + task);
            } catch (SchedulerException e) {
                msg = "Schedule Fail! " + e.getMessage();
                LOGGER.error(msg + "Task: " + task, e);
            }
            return ApiResult.build(scheduled, msg, task);
        }

        // 此处省略部分代码...

        public ApiResult rescheduleTask(QuartzTaskEntity task) {
            boolean flag = false;
            String msg = "Reschedule task Success!";
            try {
                CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(getTriggerKey(task));
                if (!oldTrigger.getCronExpression().equalsIgnoreCase(task.getCron())) {
                    Trigger newTrigger = buildTrigger(task);
                    scheduler.rescheduleJob(getTriggerKey(task), newTrigger);
                }
                flag = true;
            } catch (SchedulerException e) {
                msg = "Reschedule task Fail! " + e.getMessage();
                LOGGER.error(msg + "Task: " + task, e);
            }
            return ApiResult.build(flag, msg, task);
        }

        public synchronized void startSchedule() {
            try {
                if (scheduler.isShutdown()) {
                    scheduler.start();
                }
            } catch (SchedulerException e) {
                LOGGER.error("Start scheduler fail.", e);
            }
        }

        // 此处省略部分代码...

        private JobDetail buildJobDetail(QuartzTaskEntity task, Class<? extends Job> clazz) {
            return JobBuilder.newJob(clazz).withIdentity(getTaskName(task), getTaskGroup(task)).build();
        }

        private Trigger buildTrigger(QuartzTaskEntity task) {
            return TriggerBuilder.newTrigger().withIdentity(getTaskName(task),    getTaskGroup(task)).startNow().withSchedule(CronScheduleBuilder.cronSchedule(task.getCron())).build();
        }

        private JobKey getJobKey(QuartzTaskEntity task) {
            return JobKey.jobKey(getTaskName(task), getTaskGroup(task));
        }

        private TriggerKey getTriggerKey(QuartzTaskEntity task) {
            return TriggerKey.triggerKey(getTaskName(task), getTaskGroup(task));
        }

        private String getTaskName(QuartzTaskEntity task) {
            return StringUtils.isEmpty(task.getTaskName()) ? task.getTaskClass() : task.getTaskName();
        }

        private String getTaskGroup(QuartzTaskEntity task) {
            return StringUtils.isEmpty(task.getTaskGroup()) ? Scheduler.DEFAULT_GROUP : task.getTaskGroup();
        }
    }

3. 启动时初始化所有定时任务

/**
     * 定时任务启动器:应用启动时启动所有有效的定时任务.
     * 
     * Created by vioao.
     */
    @Component
    public class QuartzTaskStarter implements ApplicationListener<ContextRefreshedEvent> {
        private static final Logger LOGGER = LoggerFactory.getLogger(QuartzTaskStarter.class);

        @Autowired
        private TaskSchedule taskSchedule;
        @Autowired
        private QuartzTaskService quartzTaskService;

        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            try {
                List<QuartzTaskEntity> tasks = quartzTaskService.selectAllValidTask();
                for (QuartzTaskEntity task : tasks) {
                    taskSchedule.scheduleTask(task);
                }
                taskSchedule.startSchedule();
            } catch (Exception e) {
                LOGGER.error("Start all valid task fail.", e);
            }
        }
    }

添加 quartz.properties 配置文件

# 简单实用内存类存储任务状态
# 更多配置使用请访问 Quartz 官网
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 3
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

处理多机部署的问题

Quartz 本身有个分布式集群的方案,但是这个方案要求创建多张 Quartz 相关的表,且这边需求也并不是非常复杂的定时任务管理,不需要分布式集群的方式,只需要满足一个任务在对应的配置时间周期内只执行一次就可以了。所以这边不打算使用其自带的分布式解决方案,如果需要分布式解决方案的可以自己去看看 Quartz 官方文档,或者是使用开源的定时任务管理系统,如 Elastic-Job。

对于自身简单的需求,这边起初有两个方案:

1. 根据时间周期配置以及部署的机器数量,将实际的执行时间按照机器数量均分,错开每一台机子的执行时间。

  • 优点:无需引入第三方组件,实现简单
  • 缺点:需要预先知道部署的机器数量,部署实例数动态拓展的话则会有问题

2. 多机执行时,利用分布式锁,使得每次执行就只有一个实例真正获得执行权;删除任务时,通过消息订阅删除所有机子的 Job。

  • 优点:和部署实例数量无关,方案通用
  • 缺点:需要引入第三方组件

由于这边项目本身就有使用 Redis,而 Redis 也满足实现分布式锁和简单消息发布订阅功能的需求,所以这边选择了第二种方案。该方案的实现主要有两个重点:

1. 实现基于 Redis 的分布式锁

这个的实现同样也是有两个点需要关注:

  • 解锁时确保不同线程不会释放掉其他线程的锁
  • 锁超时后的处理(通常采用续租的模式来处理)

具体的实现这里就不贴出来了,已有开源的包可以支持到如上的功能,详见 Redisson,建议去看看他的源码实现学习一下。这东西面试的时候也经常会问。

2. 删除任务时,通过 Redis 的发布订阅功能通知集群中的每一台实例

这个 Easy,就不细说了,利用 Redis PubSub 功能做就好。

整合 Druid

现在 Druid 官方已经给出了一个 starter 的依赖。整合 Druid 已经非常简单了,只需要添加对应的依赖和配置即可。

添加 Druid 依赖

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

添加 application.properties 配置

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# druid
# see more config about druid: https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.datasource.druid.min-idle=5
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000

spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true

spring.datasource.druid.filters= stat,wall,slf4j
spring.datasource.druid.filter.stat.slow-sql-millis= 5000

Spring 单元测试集成 H2 数据库

在编写 Dao 的测试用例的时候,会对数据库中的数据进行操作。但是一般我们不想测试用例的运行对我们的开发库/测试服务库中的数据造成污染。那么可以考虑集成 H2 数据库来运行测试用例。集成如下。

添加依赖

<!-- test -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

添加 application.properties 配置

此处的 application.properties 文件是 test 目录下的配置文件。

# 数据源连接
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=root
spring.datasource.password=

# 设置测试启动时执行的创建schema的脚本文件
spring.datasource.schema=classpath:db/schema.sql

# 设置测试启动时执行的插入数据的脚本文件
spring.datasource.data=classpath:db/data.sql

此处两个配置文件比较重要:

  • classpath:db/schema.sql schema.sql 保存了对应的测试用例中用到的表 Schema 定义,需要符合 H2 数据库语法。
  • classpath:db/data.sql data.sql 保存的是测试用例执行前 H2 中需要初始化的数据

其余的测试用例的编写和 Spring 官方说明一致。

编写测试用例

/**
 * 定时任务实体服务测试类.
 * 
 * Created by vioao.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class QuartzServiceTest {
    @Autowired
    QuartzTaskService quartzTaskService;

    @Test
    public void testTaskExist() {
        QuartzTaskEntity quartzTaskEntity = new QuartzTaskEntity();
        quartzTaskEntity.setTaskClass("TestTask2");
        quartzTaskEntity.setState(0);
        int inserted = quartzTaskService.insert(quartzTaskEntity);
        Assert.assertEquals(1, inserted);

        Assert.assertEquals(true, quartzTaskService.taskExist(quartzTaskEntity));
    }
}

总结

至此,一个完整的用例算是完成了,整个从表定义到开发集成插件,再到后面的测试用例编写过程都有相关的说明,算是一个较为简单的模块。当然,将这个定时任务模块单独打包作为一个独立的服务运行也是可以的。Done,Bye。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值