一、简介
xxl-job是通过一个中心式的调度平台,调度多个执行器执行任务,调度中心通过DB锁保证集群分布式调度的一致性,这样扩展执行器会增大DB的压力,但是如果实际上这里数据库只是负责任务的调度执行。但是如果没有大量的执行器的话和任务的情况,是不会造成数据库压力的。实际上大部分公司任务数,执行器并不多(虽然面试经常会问一些高并发的问题)。
相对来说,xxl-job中心式的调度平台轻量级,开箱即用,操作简易,上手快,与SpringBoot有非常好的集成,而且监控界面就集成在调度中心,界面又简洁,对于企业维护起来成本不高,还有失败的邮件告警等等。这就使很多企业选择xxl-job做调度平台。
二、过程
- 新建一个SpringBoot项目,或使用现有的SpringBoot项目;
- maven配置pom.xml,导入相关的依赖
- 编写application.yml,如:项目路径名及端口,mybatisplus配置,mysql多数据源配置。
- 新建DataSourceContextHolder 用于设置,获取,清空 当前线程内的数据源变量。
- 新建 MultipleDataSource 实现 AbstractRoutingDataSource 类。重写determineCurrentLookupKey(),通过DataSourceContextHolder 获取数据源变量,用于当作lookupKey取出指定的数据源。
- 新建DataSourceEnum 用于存放数据源名称。
- 新建注解 DataSource,用于下面aop类中当作切入点来选择数据源。
- 编写aop类 --> DataSourceAspect.java
- 新建并配置以下的三个类
DruidConfiguration:
StatViewServlet 和 WebStatFilter Druid监控配置和监控过滤器。
MybatisplusConfiguration:
mybatisplus 分页插件,SQL执行效率插件;
数据源Bean,MultipleDataSource 注入;
SqlSessionFactory注入;SwaggerConfiguration:
自动生成的接口文档,不需要频繁更新接口文档,保证接口文档与代码的一致。
- 测试mybatis-plus+多数据源配置。
三、项目结构
四、详细步骤
1.新建一个SpringBoot项目,或使用现有的SpringBoot项目;
2.maven配置pom.xml,导入相关的依赖
<properties>
<druid.version>1.1.22</druid.version>
<swagger.version>2.7.0</swagger.version>
</properties>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.1.0</version>
</dependency>
3.编写application.yml,如:项目路径名及端口,mybatisplus配置,mysql多数据源配置。
server:
servlet:
context-path: /ssm-zyy
spring:
datasource:
druid:
db1:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/student?serverTimezone=UTC
initialSize: 5
minIdle: 5
maxActive: 20
db2:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/teacher?serverTimezone=UTC
initialSize: 5
minIdle: 5
maxActive: 20
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.xxl.entity
global-config:
#主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
id-type: 0
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 2
#驼峰下划线转换
db-column-underline: true
#mp2.3+ 全局表前缀 mp_
#table-prefix: mp_
#刷新mapper 调试神器
refresh-mapper: true
#数据库大写下划线转换
#capital-mode: true
#逻辑删除配置(下面3个配置)
logic-delete-value: 4
logic-not-delete-value: 0
configuration:
#配置返回数据库(column下划线命名&&返回java实体是驼峰命名),自动匹配无需as(没开启这个,SQL需要写as: select user_id as userId)
map-underscore-to-camel-case: true
cache-enabled: false
#配置JdbcTypeForNull, oracle数据库必须配置
jdbc-type-for-null: 'null'
4.新建DataSourceContextHolder 用于设置,获取,清空 当前线程内的数据源变量。
package com.xxl.datasource.multiple;
/**
* @Description: 用于设置,获取,清空 当前线程内的数据源变量。
* @author zhuyangyang
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();
/**
* 设置数据源
* @param db
*/
public static void setDataSource(String db){
contextHolder.set(db);
}
/**
* 取得当前数据源
* @return
*/
public static String getDataSource(){
return contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clear(){
contextHolder.remove();
}
}
5.新建 MultipleDataSource 实现 AbstractRoutingDataSource 类。重写determineCurrentLookupKey(),通过DataSourceContextHolder 获取数据源变量,用于当作lookupKey取出指定的数据源。
package com.xxl.datasource.multiple;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Description: 实现 AbstractRoutingDataSource 类.
* @author zhuyangyang
*/
public class MultipleDataSource extends AbstractRoutingDataSource {
/**
* 重写determineCurrentLookupKey()
* 通过DataSourceContextHolder 获取数据源变量,
* 用于当作lookupKey取出指定的数据源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
6.新建DataSourceEnum 用于存放数据源名称。
package com.xxl.datasource.enums;
public enum DataSourceEnum {
DB1("db1"),DB2("db2");
private String value;
DataSourceEnum(String value){this.value=value;}
public String getValue() {
return value;
}
}
7.新建注解 DataSource,用于下面aop类中当作切入点来选择数据源。
package com.xxl.datasource.annotation;
import com.xxl.datasource.enums.DataSourceEnum;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceEnum value() default DataSourceEnum.DB1;
}
8.编写aop类 --> DataSourceAspect.java
package com.xxl.datasource.aop;
import com.xxl.datasource.annotation.DataSource;
import com.xxl.datasource.multiple.DataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Description: 用于aop类中当作切入点来选择数据源
* @author zhuyangyang
*/
@Component
@Slf4j
@Aspect
@Order(-1)
public class DataSourceAspect {
@Pointcut("@within(com.xxl.datasource.annotation.DataSource) || @annotation(com.xxl.datasource.annotation.DataSource)")
public void pointCut(){
}
@Before("pointCut() && @annotation(dataSource)")
public void doBefore(DataSource dataSource){
log.info("选择数据源---"+dataSource.value().getValue());
DataSourceContextHolder.setDataSource(dataSource.value().getValue());
}
@After("pointCut()")
public void doAfter(){
DataSourceContextHolder.clear();
}
}
9.新建并配置以下的三个类
DruidConfiguration:
StatViewServlet 和 WebStatFilter Druid监控配置和监控过滤器。
package com.xxl.config; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DruidConfiguration { /** * @Description: 监控配置 * @author zhuyangyang * @date 2021/12/16 17:10 */ @Bean public ServletRegistrationBean startViewServlet(){ ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*"); // IP白名单 servletRegistrationBean.addInitParameter("allow","127.0.0.1"); // IP黑名单(共同存在时,deny优先于allow) servletRegistrationBean.addInitParameter("deny","127.0.0.1"); //控制台管理用户 servletRegistrationBean.addInitParameter("loginUsername","admin"); servletRegistrationBean.addInitParameter("loginPassword","123456"); //是否能够重置数据 servletRegistrationBean.addInitParameter("resetEnable","false"); return servletRegistrationBean; } /** * @Description: 监控过滤器 * @author zhuyangyang * @date 2021/12/16 17:10 */ @Bean public FilterRegistrationBean statFilter(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); //添加过滤规则 filterRegistrationBean.addUrlPatterns("/*"); //忽略过滤的格式 filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }
MybatisplusConfiguration:
mybatisplus 分页插件,SQL执行效率插件;
数据源Bean,MultipleDataSource 注入;
SqlSessionFactory注入;package com.xxl.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.xxl.datasource.enums.DataSourceEnum; import com.xxl.datasource.multiple.MultipleDataSource; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration @MapperScan("com.xxl.mapper*") public class MyBatiesPlusConfiguration { /* * 分页插件,自动识别数据库类型 * 多租户,请参考官网【插件扩展】 */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 开启 PageHelper 的支持 // paginationInterceptor.setLocalPage(true); return paginationInterceptor; } /** * SQL执行效率插件 */ @Bean @Profile({"dev","qa"})// 设置 dev test 环境开启 public PerformanceInterceptor performanceInterceptor() { PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor(); performanceInterceptor.setMaxTime(1000); performanceInterceptor.setFormat(true); return performanceInterceptor; } @Bean(name = "db1") @ConfigurationProperties(prefix = "spring.datasource.druid.db1" ) public DataSource db1() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "db2") @ConfigurationProperties(prefix = "spring.datasource.druid.db2" ) public DataSource db2() { return DruidDataSourceBuilder.create().build(); } /** * 动态数据源配置 * @return */ @Bean @Primary public DataSource multipleDataSource(@Qualifier("db1") DataSource db1, @Qualifier("db2") DataSource db2) { MultipleDataSource multipleDataSource = new MultipleDataSource(); Map< Object, Object > targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceEnum.DB1.getValue(), db1); targetDataSources.put(DataSourceEnum.DB2.getValue(), db2); //添加数据源 multipleDataSource.setTargetDataSources(targetDataSources); //设置默认数据源 multipleDataSource.setDefaultTargetDataSource(db1); return multipleDataSource; } @Bean("sqlSessionFactory") public SqlSessionFactory sqlSessionFactory() throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(multipleDataSource(db1(),db2())); //sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*/*Mapper.xml")); MybatisConfiguration configuration = new MybatisConfiguration(); //configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class); configuration.setJdbcTypeForNull(JdbcType.NULL); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); sqlSessionFactory.setConfiguration(configuration); sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor() paginationInterceptor() //添加分页功能 }); //sqlSessionFactory.setGlobalConfig(globalConfiguration()); return sqlSessionFactory.getObject(); } }
SwaggerConfiguration:
自动生成的接口文档,不需要频繁更新接口文档,保证接口文档与代码的一致。
package com.xxl.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; //http://localhost:8083/ssm-zyy/swagger-ui.html @Configuration @EnableSwagger2 public class SwaggerConfiguration { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.xxl.controller")) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("多数据源 SpringBoot+MyBatis-PLUS 测试服务") .description("多数据源 SpringBoot+MyBatis-PLUS 测试文档") .termsOfServiceUrl("http://www.baidu.com") .version("1.0") .build(); } }
10.测试mybatis-plus+多数据源配置。
实体类
package com.xxl.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Api("学生实体对象")
@Data
@TableName("t_student")
public class Student {
@ApiModelProperty("学生id")
@TableId(type = IdType.AUTO)
private Integer id;
@ApiModelProperty("学生姓名")
private String name;
@ApiModelProperty("学生年龄")
private Integer age;
@ApiModelProperty("学生班级")
private String classname;
}
package com.xxl.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Api("老师实体对象")
@Data
@TableName("t_teacher")
public class Teacher {
@ApiModelProperty("老师id")
@TableId(type = IdType.AUTO)
private Integer id;
@ApiModelProperty("老师姓名")
private String name;
@ApiModelProperty("老师年龄")
private Integer age;
@ApiModelProperty("老师所教学科")
private String subject;
}
Mapper
package com.xxl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xxl.entity.Student;
public interface StudentMapper extends BaseMapper<Student> {
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.mapper.StudentMapper">
</mapper>
package com.xxl.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xxl.entity.Teacher;
public interface TeacherMapper extends BaseMapper<Teacher> {
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxl.mapper.TeacherMapper">
</mapper>
Service
package com.xxl.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxl.entity.Student;
public interface StudentService extends IService<Student> {
}
package com.xxl.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxl.entity.Student;
import com.xxl.mapper.StudentMapper;
import com.xxl.service.StudentService;
import org.springframework.stereotype.Service;
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
}
package com.xxl.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xxl.entity.Teacher;
public interface TeacherService extends IService<Teacher> {
}
package com.xxl.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxl.datasource.annotation.DataSource;
import com.xxl.datasource.enums.DataSourceEnum;
import com.xxl.entity.Teacher;
import com.xxl.mapper.TeacherMapper;
import com.xxl.service.TeacherService;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.List;
@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {
@Override
@DataSource(DataSourceEnum.DB2)
public boolean save(Teacher entity) {
return super.save(entity);
}
@Override
@DataSource(DataSourceEnum.DB2)
public boolean removeById(Serializable id) {
return super.removeById(id);
}
@Override
@DataSource(DataSourceEnum.DB2)
public boolean updateById(Teacher entity) {
return super.updateById(entity);
}
@Override
@DataSource(DataSourceEnum.DB2)
public Teacher getById(Serializable id) {
return super.getById(id);
}
@Override
@DataSource(DataSourceEnum.DB2)
public List<Teacher> list(Wrapper<Teacher> queryWrapper) {
return super.list(queryWrapper);
}
@Override
@DataSource(DataSourceEnum.DB2)
public List<Teacher> list() {
return super.list();
}
@Override
@DataSource(DataSourceEnum.DB2)
public IPage<Teacher> page(IPage<Teacher> page, Wrapper<Teacher> queryWrapper) {
return super.page(page,queryWrapper);
}
@Override
@DataSource(DataSourceEnum.DB2)
public IPage<Teacher> page(IPage<Teacher> page) {
return super.page(page);
}
}
VO
package com.xxl.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("学生vo")
public class StudentVo {
@ApiModelProperty("学生姓名")
private String name;
@ApiModelProperty("学生年龄")
private Integer age;
@ApiModelProperty("学生班级")
private String classname;
}
package com.xxl.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("老师vo")
public class TeacherVo {
@ApiModelProperty("老师姓名")
private String name;
@ApiModelProperty("老师年龄")
private Integer age;
@ApiModelProperty("老师教的学科")
private String subject;
}
Controller
package com.xxl.controller;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xxl.entity.Student;
import com.xxl.service.StudentService;
import com.xxl.vo.StudentVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api("对学生表CRUD")
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@ApiOperation("添加学生")
@PostMapping("/add")
public String add(@RequestBody StudentVo student){
Student stu = new Student();
stu.setName(student.getName());
stu.setAge(student.getAge());
stu.setClassname(student.getClassname());
return studentService.save(stu)?"添加成功":"添加失败";
}
@ApiOperation("删除学生")
@DeleteMapping("/delete/{id}")
public String delete(@ApiParam("学生的主键id")@PathVariable(value = "id") Integer id){
return studentService.removeById(id)?"删除成功":"删除失败";
}
@ApiOperation("修改学生")
@PostMapping("/update")
public String update(@RequestBody Student student){
return studentService.updateById(student)?"修改成功":"修改失败";
}
@ApiOperation(value = "查询学生")
@GetMapping("/list")
public List<Student> list(){
Wrapper<Student> wrapper = new QueryWrapper<>();
return studentService.list(wrapper);
}
}
package com.xxl.controller;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xxl.entity.Teacher;
import com.xxl.service.TeacherService;
import com.xxl.vo.TeacherVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api("对老师表CRUD")
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@ApiOperation(value = "添加老师")
@PostMapping("/add")
public String add(@RequestBody TeacherVo teacher){
Teacher tea = new Teacher();
tea.setName(teacher.getName());
tea.setAge(teacher.getAge());
tea.setSubject(teacher.getSubject());
return teacherService.save(tea)?"添加成功":"添加失败";
}
@ApiOperation("删除老师")
@DeleteMapping("/delete/{id}")
public String delete(@ApiParam("老师的主键id")@PathVariable(value = "id") Integer id){
return teacherService.removeById(id)?"删除成功":"删除失败";
}
@ApiOperation("修改老师")
@PostMapping("/update")
public String update(@RequestBody Teacher teacher){
return teacherService.updateById(teacher)?"修改成功":"修改失败";
}
@ApiOperation(value = "查询老师")
@GetMapping("/list")
public List<Teacher> list(){
Wrapper<Teacher> wrapper = new QueryWrapper<>();
return teacherService.list(wrapper);
}
}
11.效果