当前位置:   article > 正文

canal +RocketMQ实现MySQL与redis,ElasticSearch数据同步_cancal官网

cancal官网

canal +RocketMQ实现MySQL与redis,ElasticSearch数据同步

 

1.引言

我们都知道一个系统最重要的是数据,数据是保存在数据库里。但是很多时候不单止要保存在数据库中,还要同步保存到Elastic Search、HBase、Redis等等。

这时我注意到阿里开源的框架Canal,他可以很方便地同步数据库的增量数据到其他的存储应用

在很多业务情况下,我们都会在系统中引入ElasticSearch搜索引擎作为做全文检索的优化方案,引入redis缓存作为缓存优化查询显示。

如果数据库数据发生更新,这时候就需要在业务代码中写一段同步更新ElasticSearch的代码,同步更新缓存的代码。

这种数据同步的代码跟业务代码耦合性非常高,并且使得代码的可读性降低,我们能不能把这些数据同步的代码抽出来形成一个独立的模块呢?肯定是可以的。

下面我会以一个CMS文章管理为例来演示canal+RocketMQ实现MySQL与ElasticSearch,redis数据同步。

2.技术栈

SpringBoot、canal、RocketMQ、MySQL、ElasticSearch,redis 

 

介绍一下canal,其他的自行学习。

2.1 canal定义

canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费.。

2.2 canal工作原理

在这里插入图片描述

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议

  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )

  • canal 解析 binary log 对象(原始为 byte 流)

canal能做什么
以下参考canal官网。

与其问canal能做什么,不如说数据同步有什么作用。

但是canal的数据同步不是全量的,而是增量。基于binary log增量订阅和消费,canal可以做:

数据库镜像
数据库实时备份
索引构建和实时维护
业务cache(缓存)刷新
带业务逻辑的增量数据处理
 

2.3 架构

在这里插入图片描述

说明:

server代表一个canal运行实例,对应于一个jvm
instance对应于一个数据队列 (1个server对应1…n个instance)
instance模块:

eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
eventStore (数据存储)
metaManager (增量订阅&消费信息管理器)
到这里我们对canal有了一个初步的认识,接下我们就进入实战环节。

3.环境准备
3.1 MySQL 配置
对于自建 MySQL , 需要先开启 Binlog写入功能,配置binlog-format为ROW 模式,my.cnf 中配置如下

[mysqld]
 

  1. log-bin=mysql-bin # 开启 binlog
  2. binlog-format=ROW # 选择 ROW 模式
  3. server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复


**注意:**针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步

授权canal 连接 MySQL 账号具有作为 MySQL slave的权限, 如果已有账户可直接 使用grant 命令授权。

#创建用户名和密码都为canal

  1. CREATE USER canal IDENTIFIED BY 'canal';  
  2. GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
  3. FLUSH PRIVILEGES;



3.2 canal的安装和配置
3.2.1 canal.admin安装和配置
canal提供web ui 进行Server管理、Instance管理。

3.2.1.1 下载 canal.admin, 访问 https://github.com/alibaba/canal/releases 页面 , 选择需要的包下载, 
wget https://github.com/alibaba/canal/releases/download/canal-XXX/canal.admin-XXX.tar.gz

3.2.1.2 解压完成

我们先配置canal.admin之后。通过web ui来配置 cancal server,这样使用界面操作非常的方便。

3.2.1.3 配置修改
vi conf/application.yml
 

  1. server:
  2. port: 8089
  3. spring:
  4. jackson:
  5. date-format: yyyy-MM-dd HH:mm:ss
  6. time-zone: GMT+8
  7. spring.datasource:
  8. address: 127.0.0.1:3306
  9. database: canal_manager
  10. username: canal
  11. password: canal
  12. driver-class-name: com.mysql.jdbc.Driver
  13. url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  14. hikari:
  15. maximum-pool-size: 30
  16. minimum-idle: 1
  17. canal:
  18. adminUser: admin
  19. adminPasswd: admin


3.2.1.4 初始化元数据库
初始化元数据库

# 导入初始化SQL
> source conf/canal_manager.sql

初始化SQL脚本里会默认创建canal_manager的数据库,建议使用root等有超级权限的账号进行初始化

canal_manager.sql默认会在conf目录下

3.2.1.5 启动
sh bin/startup.sh

3.2.1.6 启动成功,使用浏览器输入http://ip:8089/ 会跳转到登录界面

使用用户名:admin 密码为:123456 登录
这时候我们的canal.admin就搭建成功了。


3.2.2 下载 canal.deployer, 访问 https://github.com/alibaba/canal/releases页面 , 选择需要的包下载

解压完成

进入conf 目录。


我们先对canal.properties 不做任何修改。

使用canal_local.properties的配置覆盖canal.properties

  1. # register ip
  2. canal.register.ip =
  3. # canal admin config
  4. canal.admin.manager = 127.0.0.1:8089
  5. canal.admin.port = 11110
  6. canal.admin.user = admin
  7. canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
  8. # admin auto register
  9. canal.admin.register.auto = true
  10. canal.admin.register.cluster =



使用如下命令启动canal server

sh bin/startup.sh local

启动成功。同时我们在canal.admin web ui中刷新 server 管理,可以到canal server 已经启动成功。


这时候我们的canal.server 搭建已经成功。

3.2.3 在canal admin ui 中配置Instance管理


3.2.3.1 新建 Instance
选择Instance 管理-> 新建Instance
填写 Instance名称:cms_article
选择 选择所属主机集群
选择 载入模板
修改默认信息

#mysql serverId
canal.instance.mysql.slaveId = 1234
#position info,需要改成自己的数据库信息
canal.instance.master.address = 127.0.0.1:3306 
canal.instance.master.journal.name = 
canal.instance.master.position = 
canal.instance.master.timestamp = 
#canal.instance.standby.address = 
#canal.instance.standby.journal.name =
#canal.instance.standby.position = 
#canal.instance.standby.timestamp = 
#username/password,需要改成自己的数据库信息
canal.instance.dbUsername = canal  
canal.instance.dbPassword = canal
#改成自己的数据库信息(需要监听的数据库)
canal.instance.defaultDatabaseName = cms-manage
canal.instance.connectionCharset = UTF-8
#table regex 需要过滤的表 这里数据库的中所有表
canal.instance.filter.regex = .\*\\..\*

# MQ 配置 日志数据会发送到cms_article这个topic上
canal.mq.topic=cms_article
# dynamic topic route by schema or table regex
#canal.mq.dynamicTopic=mytest1.user,mytest2\\..*,.*\\..*
#单分区处理消息
canal.mq.partition=0

我们这里为了演示之创建一张cms_articla表。


配置好之后,我需要点击保存。此时在Instances 管理中就可以看到此时的实例信息。


3.2.4 修改canal server 的配置文件,选择消息队列处理binlog
canal 1.1.1版本之后, 默认支持将canal server接收到的binlog数据直接投递到MQ, 目前默认支持的MQ系统有:

kafka: https://github.com/apache/kafka
RocketMQ : https://github.com/apache/rocketmq
本案例以RocketMQ为例

我们仍然使用web ui 界面操作。点击 server 管理 - > 点击配置

修改配置文件

  1. # ...
  2. # 可选项: tcp(默认), kafka, RocketMQ
  3. canal.serverMode = RocketMQ
  4. # ...
  5. # kafka/rocketmq 集群配置: 192.168.1.117:9092,192.168.1.118:9092,192.168.1.119:9092
  6. canal.mq.servers = 192.168.0.200:9078
  7. canal.mq.retries = 0
  8. # flagMessage模式下可以调大该值, 但不要超过MQ消息体大小上限
  9. canal.mq.batchSize = 16384
  10. canal.mq.maxRequestSize = 1048576
  11. # flatMessage模式下请将该值改大, 建议50-200
  12. canal.mq.lingerMs = 1
  13. canal.mq.bufferMemory = 33554432
  14. # Canal的batch size, 默认50K, 由于kafka最大消息体限制请勿超过1M(900K以下)
  15. canal.mq.canalBatchSize = 50
  16. # Canal get数据的超时时间, 单位: 毫秒, 空为不限超时
  17. canal.mq.canalGetTimeout = 100
  18. # 是否为flat json格式对象
  19. canal.mq.flatMessage = false
  20. canal.mq.compressionType = none
  21. canal.mq.acks = all
  22. # kafka消息投递是否使用事务
  23. canal.mq.transaction = false



修改好之后保存。会自动重启。

此时我们就可以在rocketmq的控制台看到一个cms_article topic已经自动创建了。

3.2.5 配置ElasticSearch启动

我们使用 elasticsearch-head 连接是可以看到节点信息。一会我们就使用 elasticsearch-head 查询es中数据。

4.代码实战
4.1 创建一个springboot 项目
 


4.2 pom.xml文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>rockmq-samples</artifactId>
  7. <groupId>com.lidong.rocketmq</groupId>
  8. <version>1.0.0</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>springboot-canal-rocketmq-es</artifactId>
  12. <properties>
  13. <java.version>1.8</java.version>
  14. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  16. <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
  17. </properties>
  18. <dependencies>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-web</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-test</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>org.apache.rocketmq</groupId>
  33. <artifactId>rocketmq-spring-boot-starter</artifactId>
  34. <version>2.0.4</version>
  35. </dependency>
  36. </dependencies>
  37. <dependencyManagement>
  38. <dependencies>
  39. <dependency>
  40. <groupId>org.springframework.boot</groupId>
  41. <artifactId>spring-boot-dependencies</artifactId>
  42. <version>${spring-boot.version}</version>
  43. <type>pom</type>
  44. <scope>import</scope>
  45. </dependency>
  46. </dependencies>
  47. </dependencyManagement>
  48. <build>
  49. <plugins>
  50. <plugin>
  51. <groupId>org.apache.maven.plugins</groupId>
  52. <artifactId>maven-compiler-plugin</artifactId>
  53. <version>3.8.1</version>
  54. <configuration>
  55. <source>1.8</source>
  56. <target>1.8</target>
  57. <encoding>UTF-8</encoding>
  58. </configuration>
  59. </plugin>
  60. <plugin>
  61. <groupId>org.springframework.boot</groupId>
  62. <artifactId>spring-boot-maven-plugin</artifactId>
  63. <version>2.3.0.RELEASE</version>
  64. <configuration>
  65. <mainClass>com.lidong.RocketmqSyncSamplesApplication</mainClass>
  66. </configuration>
  67. <executions>
  68. <execution>
  69. <id>repackage</id>
  70. <goals>
  71. <goal>repackage</goal>
  72. </goals>
  73. </execution>
  74. </executions>
  75. </plugin>
  76. </plugins>
  77. </build>
  78. </project>


spring-boot-starter-data-elasticsearch:操作es依赖库
rocketmq-spring-boot-starter:操作rocketmq依赖库
其他就不过多介绍了。大家一看就明白了。

4.3 application的配置

  1. server:
  2. port: 8085
  3. rocketmq:
  4. name-server: localhost:9876
  5. spring:
  6. data:
  7. elasticsearch:
  8. cluster-nodes: localhost:9300
  9. cluster-name: my-application
  10. repositories:
  11. enabled: true

 

  • rocketmq.name-server : rocketmq的namesver
  • spring.data.elasticsearch.cluster-nodes :es 节点地址
  • spring.data.elasticsearch.cluster-name: es节点集群名称
  • spring.data.elasticsearch.repositories.enabled :开启es仓库使用

4.4 创建es操作的实体类和仓库类
4.4.1 EsCmsArticle实体类

  1. import org.springframework.data.annotation.Id;
  2. import org.springframework.data.elasticsearch.annotations.Document;
  3. import java.io.Serializable;
  4. import java.util.Date;
  5. /**
  6. * 文章详情
  7. *
  8. * String indexName();//索引库的名称,个人建议以项目的名称命名
  9. * String type() default "";//类型,个人建议以实体的名称命名
  10. * short shards() default 5;//默认分区数
  11. * short replicas() default 1;//每个分区默认的备份数
  12. * String refreshInterval() default "1s";//刷新间隔
  13. * String indexStoreType() default "fs";//索引文件存储类型
  14. *
  15. **/
  16. @Document(indexName = "canal-rocketmq-es", type = "cms-article")
  17. public class EsCmsArticle implements Serializable {
  18. @Id
  19. private Long courseId;
  20. /** 标题 */
  21. private String title;
  22. /** 摘要 */
  23. private String abstractX;
  24. /** 内容 */
  25. private String content;
  26. /** 年龄段 */
  27. private String ageRange;
  28. /** 图片 */
  29. private String image;
  30. /** 查看次数 */
  31. private Long viewNumber;
  32. /** 作者 */
  33. private String author;
  34. /** 来源 */
  35. private String source;
  36. /** 所属分类 */
  37. private Long classId;
  38. /** 关键字 */
  39. private String keyWords;
  40. /** 描述 */
  41. private String description;
  42. /** 文章url */
  43. private String url;
  44. /**
  45. * 文章状态
  46. */
  47. private Integer status;
  48. /**
  49. * 创建时间
  50. */
  51. private Date createTime;
  52. /**
  53. * 修改时间
  54. */
  55. private Date updateTime;
  56. public void setCourseId(Long courseId)
  57. {
  58. this.courseId = courseId;
  59. }
  60. public Long getCourseId()
  61. {
  62. return courseId;
  63. }
  64. public void setTitle(String title)
  65. {
  66. this.title = title;
  67. }
  68. public String getTitle()
  69. {
  70. return title;
  71. }
  72. public void setAbstractX(String abstractX)
  73. {
  74. this.abstractX = abstractX;
  75. }
  76. public String getAbstractX()
  77. {
  78. return abstractX;
  79. }
  80. public void setContent(String content)
  81. {
  82. this.content = content;
  83. }
  84. public String getContent()
  85. {
  86. return content;
  87. }
  88. public void setAgeRange(String ageRange)
  89. {
  90. this.ageRange = ageRange;
  91. }
  92. public String getAgeRange()
  93. {
  94. return ageRange;
  95. }
  96. public void setImage(String image)
  97. {
  98. this.image = image;
  99. }
  100. public String getImage()
  101. {
  102. return image;
  103. }
  104. public void setViewNumber(Long viewNumber)
  105. {
  106. this.viewNumber = viewNumber;
  107. }
  108. public Long getViewNumber()
  109. {
  110. return viewNumber;
  111. }
  112. public void setAuthor(String author)
  113. {
  114. this.author = author;
  115. }
  116. public String getAuthor()
  117. {
  118. return author;
  119. }
  120. public void setSource(String source)
  121. {
  122. this.source = source;
  123. }
  124. public String getSource()
  125. {
  126. return source;
  127. }
  128. public void setClassId(Long classId)
  129. {
  130. this.classId = classId;
  131. }
  132. public Long getClassId()
  133. {
  134. return classId;
  135. }
  136. public void setKeyWords(String keyWords)
  137. {
  138. this.keyWords = keyWords;
  139. }
  140. public String getKeyWords()
  141. {
  142. return keyWords;
  143. }
  144. public void setDescription(String description)
  145. {
  146. this.description = description;
  147. }
  148. public String getDescription()
  149. {
  150. return description;
  151. }
  152. public void setUrl(String url)
  153. {
  154. this.url = url;
  155. }
  156. public String getUrl()
  157. {
  158. return url;
  159. }
  160. public Integer getStatus() {
  161. return status;
  162. }
  163. public void setStatus(Integer status) {
  164. this.status = status;
  165. }
  166. public Date getCreateTime() {
  167. return createTime;
  168. }
  169. public void setCreateTime(Date createTime) {
  170. this.createTime = createTime;
  171. }
  172. public Date getUpdateTime() {
  173. return updateTime;
  174. }
  175. public void setUpdateTime(Date updateTime) {
  176. this.updateTime = updateTime;
  177. }
  178. @Override
  179. public String toString() {
  180. return "CmsArticle{" +
  181. "courseId=" + courseId +
  182. ", title='" + title + '\'' +
  183. ", abstractX='" + abstractX + '\'' +
  184. ", content='" + content + '\'' +
  185. ", ageRange='" + ageRange + '\'' +
  186. ", image='" + image + '\'' +
  187. ", viewNumber=" + viewNumber +
  188. ", author='" + author + '\'' +
  189. ", source='" + source + '\'' +
  190. ", classId=" + classId +
  191. ", keyWords='" + keyWords + '\'' +
  192. ", description='" + description + '\'' +
  193. ", url='" + url + '\'' +
  194. ", status=" + status +
  195. ", createTime=" + createTime +
  196. ", updateTime=" + updateTime +
  197. '}';
  198. }
  199. }


4.4.2 CmsArticleRepository 仓库类

  1. import com.alibaba.fastjson.JSON;
  2. import com.lidong.canal.bean.CanalBean;
  3. import com.lidong.canal.bean.CmsArticle;
  4. import com.lidong.canal.es.entity.EsCmsArticle;
  5. import com.lidong.canal.es.repository.CmsArticleRepository;
  6. import org.apache.rocketmq.spring.annotation.ConsumeMode;
  7. import org.apache.rocketmq.spring.annotation.MessageModel;
  8. import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
  9. import org.apache.rocketmq.spring.core.RocketMQListener;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.beans.BeanUtils;
  13. import org.springframework.beans.factory.annotation.Autowired;
  14. import org.springframework.stereotype.Component;
  15. import java.util.List;
  16. import java.util.Optional;
  17. @Component
  18. @RocketMQMessageListener(
  19. topic = "cms_article",
  20. consumerGroup = "cms-article",
  21. selectorExpression = "*",
  22. consumeMode = ConsumeMode.ORDERLY,
  23. messageModel = MessageModel.CLUSTERING,
  24. consumeThreadMax = 1
  25. )
  26. public class SpringConsumer implements RocketMQListener<String> {
  27. private Logger logger = LoggerFactory.getLogger(SpringConsumer.class.getSimpleName());
  28. @Autowired
  29. CmsArticleRepository cmsArticleRepository;
  30. /**
  31. * 实现方式很简单吧,但是你也看见了代码中就没有消息能够消费是否成功后的确认方式,因为实现的onMessage()方法是个void的,还好看过原始的rocketmq的消费者实现方式,也就是rocketmq-client.jar的实现,它是MessageListener.java类来实现消息监听接收的,而它有2个继承接口类MessageListenerConcurrently.java和MessageListenerOrderly.java,这样就好找了,直接收一下这2个接口的实现类,乖乖,果然找到了在rocket-spring-boot的jar里面,就是DefaultRocketMQListenerContainer.java这个类,看下其中一个实现
  32. *
  33. *
  34. * @param msg
  35. */
  36. @Override
  37. public void onMessage(String msg) {
  38. System.out.println("接收到消息 -> " + msg);
  39. CanalBean canalBean = JSON.parseObject(msg, CanalBean.class);
  40. String table = canalBean.getTable();
  41. System.out.println(table.toString());
  42. String type = canalBean.getType();
  43. System.out.println(type);
  44. List<CmsArticle> data = canalBean.getData();
  45. data.stream().forEach(tbTest -> {
  46. EsCmsArticle esCmsArticle = new EsCmsArticle();
  47. System.out.println(tbTest.toString());
  48. if ("UPDATE".equals(type) && "cms_article".equals(table)) {
  49. Optional<EsCmsArticle> article = cmsArticleRepository.findById(tbTest.getCourseId());
  50. //删除缓存
  51. //操作es
  52. if (article.isPresent()) {
  53. EsCmsArticle cmsArticle = article.get();
  54. BeanUtils.copyProperties(tbTest, cmsArticle);
  55. cmsArticleRepository.save(cmsArticle);
  56. logger.info("id = {} 编辑es成功", cmsArticle.getCourseId());
  57. } else {
  58. BeanUtils.copyProperties(tbTest, esCmsArticle);
  59. cmsArticleRepository.save(esCmsArticle);
  60. logger.info("id = {} 添加es成功", esCmsArticle.getCourseId());
  61. }
  62. } else if ("INSERT".equals(type) && "cms_article".equals(table)) {
  63. BeanUtils.copyProperties(tbTest, esCmsArticle);
  64. //添加缓存
  65. //操作es
  66. cmsArticleRepository.save(esCmsArticle);
  67. logger.info("id = {} 添加es成功", esCmsArticle.getCourseId());
  68. }
  69. });
  70. }
  71. }


4.6 SpringBootApplication启动类

  1. import org.springframework.boot.SpringApplication;
  2. import org.springframework.boot.autoconfigure.SpringBootApplication;
  3. @SpringBootApplication
  4. public class RocketmqToEsSamplesApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(RocketmqToEsSamplesApplication.class, args);
  7. }
  8. }


4.7 CanalBean类 接收mq的数据实体

  1. public class CanalBean implements Serializable {
  2. //数据
  3. private List<CmsArticle> data;
  4. //数据库名称
  5. private String database;
  6. private long es;
  7. //递增,从1开始
  8. private int id;
  9. //是否是DDL语句
  10. private boolean isDdl;
  11. //表结构的字段类型
  12. private MysqlType mysqlType;
  13. //UPDATE语句,旧数据
  14. private List<CmsArticle> old;
  15. //主键名称
  16. private List<String> pkNames;
  17. //sql语句
  18. private String sql;
  19. private SqlType sqlType;
  20. //表名
  21. private String table;
  22. private long ts;
  23. //(新增)INSERT、(更新)UPDATE、(删除)DELETE、(删除表)ERASE等等
  24. private String type;
  25. //get set ...
  1. public class MysqlType implements Serializable {
  2. private String id;
  3. private String commodity_name;
  4. private String commodity_price;
  5. private String number;
  6. private String description;
  7. //get set..
  1. public class SqlType implements Serializable {
  2. private int id;
  3. private int commodity_name;
  4. private int commodity_price;
  5. private int number;
  6. private int description;
  7. //get set..
  8. }
  1. import java.io.Serializable;
  2. import java.util.Date;
  3. public class CmsArticle implements Serializable {
  4. /** $column.columnComment */
  5. private Long courseId;
  6. /** 标题 */
  7. private String title;
  8. /** 摘要 */
  9. private String abstractX;
  10. /** 内容 */
  11. private String content;
  12. /** 年龄段 */
  13. private String ageRange;
  14. /** 图片 */
  15. private String image;
  16. /** 查看次数 */
  17. private Long viewNumber;
  18. /** 作者 */
  19. private String author;
  20. /** 来源 */
  21. private String source;
  22. /** 所属分类 */
  23. private Long classId;
  24. /** 关键字 */
  25. private String keyWords;
  26. /** 描述 */
  27. private String description;
  28. /** 文章url */
  29. private String url;
  30. /**
  31. * 文章状态
  32. */
  33. private Integer status;
  34. /**
  35. * 创建时间
  36. */
  37. private Date createTime;
  38. /**
  39. * 修改时间
  40. */
  41. private Date updateTime;
  42. }




 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/445357
推荐阅读
相关标签
  

闽ICP备14008679号