前情提要
本次培训为JAVA中阶培训(一)的拓展,主要讲述RocketMQ的相关原理与使用,以及附带EasyExcel的使用方法,着重于提高Java初级程序员对Java技术的更加深层次的理解,进而培养自己的架构思维,在今后的技术选型中能够更加从容。
同JAVA中阶(一)培训一样,所有例子均来自项目实例。
一、RocketMQ
1、什么是消息队列?
序
在很久很久以前,人们之间的通信方式就是面对面交谈,你说一句,我听一句,虽然简单可靠,但是弊端也很大。
比如,当你成为一个军队的首领,每个属下一有情况就立刻向你汇报,一个还好,但当你的属下有几十个几百个的时候,他们每天不分时间不看场合,都在叽叽喳喳和你汇报情况,那你可能什么都听不到,而且脑袋都要炸掉了。这个时候,你说停,都给我停下,要汇报情况的,去门口排队,一个一个的来,这个就叫做流量削峰,一群人不要一拥而上,都乖乖给我排队去。
然后你就一个接一个的听,听了整整24个小时,实在困的不行,寻思着这样不行呀,如此下去可能就要天妒英才了,于是你又说,来人,发笔和纸,都把要汇报的消息写在纸上,写完后告诉吕秀才,然后听吕秀才的指示,沿着屋里右面墙根,按照指示的位置叠放整齐,汇报的人就可以退下该做啥做啥去吧,等我休息一下,再来看你们的汇报内容,这就叫做异步处理,你终于可以由自己掌控消息获取的进度了,美滋滋的去睡觉了。
而汇报的人把内容写在纸上,叠放好,就可以退下自己做自己该做的事情,而不是一直在门口等待汇报,这个就叫做解耦。
削峰,异步,解耦。这就是消息队列最常用的三大场景。
故事中的下属们,就是消息生产者角色,屋子右面墙根那块地就是消息持久化,吕秀才就是消息调度中心,而你就是消息消费者角色。下属们汇报的消息,应该叠放在哪里,这个消息又应该在哪里才能找到,全靠吕秀才的惊人记忆力,才可以让消息准确的被投放以及消费。
正题
消息队列作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性。主要具有以下优势:
削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)
系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)
提升性能(当存在一对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)
蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)
目前主流的MQ主要是Rocketmq、kafka、Rabbitmq,Rocketmq相比于Rabbitmq、kafka具有主要优势特性有:
• 支持事务型消息(消息发送和DB操作保持两方的最终一致性,rabbitmq和kafka不支持)
• 支持结合rocketmq的多个系统之间数据最终一致性(多方事务,二方事务是前提)
• 支持18个级别的延迟消息(rabbitmq和kafka不支持)
• 支持指定次数和时间间隔的失败消息重发(kafka不支持,rabbitmq需要手动确认)
• 支持consumer端tag过滤,减少不必要的网络传输(rabbitmq和kafka不支持)
• 支持重复消费(rabbitmq不支持,kafka支持)
Rocketmq、kafka、Rabbitmq的详细对比,请参照下表格:
2、RocketMQ详解
序
我们来类比一下现实生活,有一个人想要给另外一个人寄快件,那么就需要先由这个人在网上查询有哪些邮局,然后选择其中一个邮局,把快件投递给它,再由邮局配送到目标人。
1、路由注册
邮局在竣工后,需要与卫星联网,将自己纳入卫星网络管理中,这样就相当于对外宣布,我这个邮局开始运营了,可以收发邮件快递了。 邮局并网之后,如何让卫星持续并及时感知这个邮局在线以及邮局自身信息的调整,使卫星可以随时协调这个邮局呢?
这个时候就需要邮局定时向卫星发一条信息: “哔哔哔————我是邮局C,编号SHC,地址XXXXX,归属中国上海集群,在线,此时此刻2019年3月15日13点21秒”
卫星接收到消息后,拿个小本本记录下来:
“邮局B,BJB,北京,2019年3月15日13点10秒,活着...”
“邮局A,BJA,北京,2019年3月15日13点15秒,活着...”
“邮局C,SHC,上海,2019年3月15日13点21秒,活着...”
上面这个故事,就讲述了NameServer路由注册的基本原理。
NameServer就相当于卫星,内部会维护一个Broker表,用来动态存储Broker的信息。 而Broker就相当于邮局,在启动的时候,会先遍历NameServer列表,依次发起注册请求,保持长连接,然后每隔30秒向NameServer发送心跳包,心跳包中包含BrokerId、Broker地址、Broker名称、Broker所属集群名称等等,然后NameServer接收到心跳包后,会更新时间戳,记录这个Broker的最新存活时间。
NameServer在处理心跳包的时候,存在多个Broker同时操作一张Broker表,为了防止并发修改Broker表导致不安全,路由注册操作引入了ReadWriteLock读写锁,这个设计亮点允许多个消息生产者并发读,保证了消息发送时的高并发,但是同一时刻NameServer只能处理一个Broker心跳包,多个心跳包串行处理。这也是读写锁的经典使用场景,即读多写少。
2、路由剔除
忽然有一天,邮局C的机房进老鼠了,咬断电源线宕机了,而卫星不知道邮局C业务故障了,依旧将带有邮局C的邮局表信息传给寄件人(生产者),寄件人联系邮局C发送快件,但是邮局C机房宕机,业务暂停,处于瘫痪状态,自然也就无法接收快件了。 另一方面,因为快件未能被邮局C收入,也就无法将快件转交给收件人,顾客们久久等不到自己的快件,纷纷投诉,为此邮局C的管理层备受责难。
于是邮政总局技术部开始研究讨论,怎么让卫星可以感知到邮局“失联了”,从而自动排除故障邮局,将其负责的业务交付给其他正常的邮局处理,这样就不会因为某一个邮局出现问题,而导致这个邮局所管辖的部分业务无法处理。
大家众说纷纭,最后敲定了一个方案,让卫星每隔一段时间扫描邮局信息表,如果发现某个邮局上报信息时间与当时扫描时间之间的差值超过了某个预设的阈值,就判定这个邮局“失联了”,将此邮局信息从邮局表中剔除。这样寄件人查询到的邮局表里都是正常营业的邮局信息。
新功能上线运营后,效果不错,大家再也不用担心因为某个邮局故障而导致业务停滞,又过上了泡茶报纸的生活。
这个故事同样在RocketMQ中上演。
正常情况下,如果Broker关闭,则会与NameServer断开长连接,Netty的通道关闭监听器会监听到连接断开事件,然后会将这个Broker信息剔除掉。
异常情况下,NameServer中有一个定时任务,每隔10秒扫描一下Broker表,如果某个Broker的心跳包最新时间戳距离当前时间超多120秒,也会判定Broker失效并将其移除。
细心的人会发现一个问题,NameServer在清除失活Broker之后,并没有主动通知生产者,生产者每隔30秒会请求NameServer并获取最新的路由表,那么就意味着,消息生产者总会有30秒的延时,无法实时感知Broker服务器的宕机。所以在这个30秒里,生产者依旧会向失活Broker发送消息,那么消息发送的高可用性如何保证呢?
3、Broker高可用原理
要解决这个问题得首先谈一谈Broker的负载策略,消息发送队列默认采用轮询机制,消息发送时默认选择异常重试机制来保证消息发送的高可用。当Broker宕机后,虽然消息发送者无法第一时间感知Broker 宕机,但是当消息生产者向Broker发送消息返回异常后,消息生产者会选择另外一个Broker上的消息队列,这样就规避了发生故障的Broker,结合重试机制,巧妙实现消息发送的高可用,同时由于不需要NameServer通知众多不固定的生产者,也降低了NameServer实现的复杂性。
2、在降低NameServer实现复杂性方面,还有一个设计亮点就是NameServer之间是彼此独立无交流的,也就是说NameServer服务器之间在某个时刻的数据并不会完全相同,但是异常重试机制使得这种差异不会造成任何影响。
4、路由发现
天上的卫星是有限的,不易变的,而地上的寄件人是繁多的,易变的。所以寄件人想要知道有哪些邮局,很明显最适合的方式是向卫星发请求,拉取邮局表信息,而不是等卫星给每个人推送。 所以在RocketMQ中,NameServer是不主动推送会客户端的,而是由客户端拉取主题的最新路由信息。
5、CAP理论
NameServer作为注册和发现中心,是整个分布式消息队列调度的灵魂,谈及到分布式,就逃不开CAP理论,C是Consistency(一致性),A是Availability(可用性),P是(容错性),对于分布式架构,网络条件不可控,出现网络分区是不可避免的,因此必须具备分区容错性,那么NameServer就是在AP还是CP中选择了,由于NameServer之间相互独立,很明显,是一个AP设计。
正题
1) Name Server(路由注册中⼼)(吕秀才)(卫星)
Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
2) Broker (消息存储服务器)(墙根)(邮局)
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker与Name Server集群中的所有节点建立长连接,定时(每隔30s)注册Topic信息到所有Name Server。Name Server定时(每隔10s)扫描所有存活broker的连接,如果Name Server超过2分钟没有收到心跳,则Name Server断开与Broker的连接。
3) Producer(消息⽣产者,⽤于向消息服务器发送消息)(士兵)(发件人)
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Producer每隔30s(由ClientConfig的pollNameServerInterval)从Name server获取所有topic队列的最新情况,这意味着如果Broker不可用,Producer最多30s能够感知,在此期间内发往Broker的所有消息都会失败。
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s中扫描所有存活的连接,如果Broker在2分钟内没有收到心跳数据,则关闭与Producer的连接。
4) Consumer(消息消费者)(你-将军)(收件人)
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
Consumer每隔30s从Name server获取topic的最新队列情况,这意味着Broker不可用时,Consumer最多最需要30s才能感知。
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group的所有Consumer发出通知,Group内的Consumer重新分配队列,然后继续消费。
当Consumer得到master宕机通知后,转向slave消费,slave不能保证master的消息100%都同步过来了,因此会有少量的消息丢失。但是一旦master恢复,未同步过去的消息会被最终消费掉。
消费者对列是消费者连接之后(或者之前有连接过)才创建的。我们将原生的消费者标识由 {IP}@{消费者group}扩展为 {IP}@{消费者group}{topic}{tag},(例如xxx.xxx.xxx.xxx@mqtest_producer-group_2m2sTest_tag-zyk)。任何一个元素不同,都认为是不同的消费端,每个消费端会拥有一份自己消费对列(默认是broker对列数量*broker数量)。新挂载的消费者对列中拥有commitlog中的所有数据。
3、实际代码
一、基础配置
(1)、MAVEN 引入(使用阿里云的RocketMQ付费服务,不自行搭建)
com.aliyun.openservices
ons-client
1.2.6
(2)、配置相应的账号信息(mq-config.properties)
ProducerId=保密
ConsumerId=保密
Topic=保密
AccessKey=保密
SecretKey=保密
expression=*
二、生产者使用
(1)、生产者发送消息核心方法
/**
* MQ生产者-发布
*
* @author 陈豆豆
* @date 2020-03-31
*/
@Slf4j
public class RocketMqProducer {
/**
* 消息发送
*
* @param producer 生产者
* @param topic 主体
* @param bean 消息实体
* @throws UnsupportedEncodingException 编码格式异常
* @throws JsonProcessingException 格式转换异常
*/
public static void rocketMqSend(Producer producer, String topic, BaseRocketMqBean bean) throws UnsupportedEncodingException, JsonProcessingException {
SendResult sendResult = producer.send(new Message(topic, bean.getTag(), bean.getKey(), bean.parseToRocketBytes()));
log.info("[{}] RocketMQ发送消息 [{}] 反馈信息 [{}]", topic, bean.toString(), sendResult);
}
}
(2)、生产者发送消息组装核心方法
/**
* 超级店长生产者
*
* @author 陈豆豆
* @date 2020-03-31
*/
@Slf4j
@Service
public class OrangeStoreProducer {
private static final BlockingQueue QUEUE = new LinkedBlockingQueue<>();
private static Producer producer;
private static String Topic = "";
static {
try {
Properties properties = new Properties();
properties.load(OrangeStoreProducer.class.getClassLoader().getResourceAsStream("mq-config.properties"));
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);
Topic = properties.getProperty("Topic");
producer = ONSFactory.createProducer(properties);
producer.start();
log.info("超级店长生产队列启动");
} catch (IOException e) {
log.error("超级店长生产队列启动异常: {}", e.getLocalizedMessage(), e);
}
}
public static void addToQueue(BaseRocketMqBean bean) {
bean.setTag("MS_orange_store_tag");
bean.setKey("MS_orange_store");
QUEUE.offer(bean);
}
@PostConstruct
@SuppressWarnings("all")
public void batchSend() {
Thread thread = new Thread(() -> {
{
while (true) {
try {
// 超过1秒无法获取数据就等待下次获取
BaseRocketMqBean bean = QUEUE.poll(1, TimeUnit.SECONDS);
if (bean == null) {
continue;
}
RocketMqProducer.rocketMqSend(producer, Topic, bean);
} catch (Exception e) {
log.error("同步数据到超级店长生产队列异常:{}", e.getLocalizedMessage(), e);
}
}
}
});
thread.start();
}
}
讲解点
RocketMQ消息分发的两种模式
广播模式(BROADCASTING)和 集群模式(CLUSTERING)
(3)、消息传递结构体
基础结构体
/**
* 阿里云rocketMq队列实体bean父类
*
* @author 陈豆豆
* @date 2020-03-31
*/
@Data
public abstract class BaseRocketMqBean implements Serializable {
private static final long serialVersionUID = 29723539849425729L;
/**
* 队列tag
*/
private String tag;
/**
* 队列messageKey
*/
private String key;
/**
* bean 转换成阿里云json
*
* @return json
* @throws JsonProcessingException 格式转换异常
*/
public byte[] parseToRocketBytes() throws JsonProcessingException, UnsupportedEncodingException {
return new ObjectMapper().writeValueAsString(this).getBytes("UTF-8");
}
}
同步数据结构体
/**
* 同步数据专用bean
*
* @author 陈豆豆
* @date 2020/4/01 10:04
*/
public class SyncMessageBean extends BaseRocketMqBean implements Serializable {
private static final long serialVersionUID = -1535244308014581262L;
/**
* 操作结果
*/
@JsonIgnore
private Boolean success;
/**
* 标题
*/
private String title;
/**
* 提示消息
*/
private String errorMsg;
/**
* 返回码
*/
@JsonIgnore
private Integer errorCode;
/**
* 对象实体
*/
private Object data;
public SyncMessageBean() {
super();
this.success = success;
this.errorMsg = errorMsg;
}
public SyncMessageBean(Boolean success, String message, T data, String title) {
super();
this.success = success;
this.errorMsg = message;
this.data = data;
this.title = title;
}
public SyncMessageBean(boolean success, String message) {
super();
this.title = "操作提示";
this.success = success;
this.errorMsg = message;
}
public SyncMessageBean(Exception ex) {
super();
this.success = false;
this.errorMsg = StringUtils.defaultIfBlank(ex.getMessage(), "程序异常");
}
public static SyncMessageBean success() {
SyncMessageBean messageBean = new SyncMessageBean();
messageBean.setSuccess(true);
messageBean.setErrorMsg("操作成功");
return messageBean;
}
public static SyncMessageBean success(String msg) {
SyncMessageBean messageBean = new SyncMessageBean();
messageBean.setSuccess(true);
messageBean.setErrorMsg(msg);
return messageBean;
}
public static SyncMessageBean fail(String msg) {
SyncMessageBean messageBean = new SyncMessageBean();
messageBean.setSuccess(false);
messageBean.setErrorCode(ErrorCodeEnum.ERROR.getCode());
messageBean.setErrorMsg(msg);
messageBean.setTitle();
return messageBean;
}
public static SyncMessageBean fail() {
return fail("操作失败");
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public void setTitle() {
this.title = "ERROR";
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public Integer getErrorCode() {
return errorCode;
}
public void setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "MessageBean{" +
"success=" + success +
", title='" + title + '\'' +
", errorMsg='" + errorMsg + '\'' +
", errorCode=" + errorCode +
", data=" + data +
'}';
}
}
(4)、生产消息类型
/**
* 同步类型枚举
*
* @author 陈豆豆
* @date 2020/4/1 10:00
*/
public enum ProducerTypeEnum {
/**
* 店员信息同步
*/
POS_CASHIER(0, "POS_CASHIER"),
/**
* 门店电费信息同步
*/
STORE_ELECTRICITY(1, "STORE_ELECTRICITY"),
;
/**
* 编码
*/
private Integer code;
/**
* 名称
*/
private String name;
ProducerTypeEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(5)、调用方法
/**
* 同步门店信息到物联网系统
*
* @param storeCode 门店编号
*/
@Override
public void syncStoreForSmart(String storeCode) {
// 查询对应的门店信息
StoreForSmart store = storeInfoDao.queryStoreForSmart(storeCode);
if (Objects.isNull(store)) {
return;
}
SyncMessageBean messageBean = new SyncMessageBean();
messageBean.setErrorMsg(ProducerTypeEnum.SYNC_STORE_TO_SMART.getName());
messageBean.setData(JSON.toJSONString(store));
OrangeStoreProducer.addToQueue(messageBean);
}
三、消费者
(1)、接受消息核心组件
/**
* @description:接受消息核心组件
* @author: tangn
* @time: 2021/2/7 15:14
*/
@Slf4j
@Component
public class ReceiveHandleComponent {
public static final BlockingQueue RECEIVE_HANDLE = new LinkedBlockingQueue<>();
@Resource
private ConsumerService consumerService;
/**
* 创建线程池
*/
private static final ThreadFactory THREAD_FACTORY = new ThreadFactoryBuilder()
.setNameFormat("ReceiveHandleComponent-%d").setDaemon(true).build();
private static final ExecutorService RECEIVE_HANDLE_POLL = Executors.newFixedThreadPool(16, THREAD_FACTORY);
@PostConstruct
public void judgeFace() {
//执行线程
RECEIVE_HANDLE_POLL.execute(() -> {
while (true) {
try {
// 设置队列超过1秒无法获取数据就等待下次获取
SyncMessageBean syncMessageBean = RECEIVE_HANDLE.poll(1, TimeUnit.SECONDS);
if (Objects.isNull(syncMessageBean)) {
continue;
}
consumerService.receiveHandle(syncMessageBean);
} catch (Exception e) {
log.warn("订阅处理线程异常[{}]", e.getLocalizedMessage(), e);
}
}
});
}
}
(2)、消费者注册
/**
* @description:消费者注册
* @author: tangn
* @time: 2021/2/7 15:09
*/
@Slf4j
@Component
public class QianYunDataConsumer extends ConsumerBean {
private final QianYunDataListener qianYunDataListener;
@Autowired
public QianYunDataConsumer(QianYunDataListener qianYunDataListener) {
this.qianYunDataListener = qianYunDataListener;
}
@SuppressWarnings("Duplicates")
private Subscription postInit() {
Subscription subscription = new Subscription();
try {
Properties properties = new Properties();
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);
properties.load(this.getClass().getClassLoader().getResourceAsStream("mq-config.properties"));
this.setProperties(properties);
String topic = properties.getProperty("Topic");
String expression = properties.getProperty("expression");
subscription.setExpression(expression);
subscription.setTopic(topic);
log.info("消费者配置文件加载成功");
} catch (IOException e) {
log.error("消费者初始化,配置文件加载异常 {}", e.getLocalizedMessage(), e);
}
return subscription;
}
@PostConstruct
@Override
public void start() {
Map consumers = new HashMap<>(1);
consumers.put(postInit(), qianYunDataListener);
this.setSubscriptionTable(consumers);
log.info("消费者启动");
super.start();
}
@PreDestroy
@Override
public void shutdown() {
log.info("消费者关闭");
super.shutdown();
}
}
(4)、消费者消息监听
/**
* @description:消费者消息监听
* @author: tangning
* @time: 2021/2/7 15:12
*/
@Slf4j
@Service
public class QianYunDataListener implements MessageListener {
@Override
public Action consume(Message message, ConsumeContext consumeContext) {
try {
log.info("[{}] 消费队列接收信息,body [{}] ", message, message == null ? "null" : new String(message.getBody(), "ISO8859-1"));
SyncMessageBean messageBean = MqTextUtil.convertMessageToBean(message, SyncMessageBean.class);
log.info("messageId [{}] 订阅信息返回 [{}]", message.getMsgID(), messageBean);
if (Objects.nonNull(messageBean)){
ReceiveHandleComponent.RECEIVE_HANDLE.offer(messageBean);
}
return Action.CommitMessage;
} catch (ErrorCodeException e) {
log.warn("[{}] 消费者异常 [{}]", message, e.getLocalizedMessage(), e);
return Action.ReconsumeLater;
} catch (Throwable e) {
log.error("[{}] 消费者异常 [{}]", message, e.getLocalizedMessage(), e);
return Action.ReconsumeLater;
}
}
}
(5)、消费者消息处理(根据自己的业务进行处理)
/**
* @description: 订阅处理
* @author: MA XIN
* @time: 2021/2/7 15:26
*/
@Slf4j
@Service
public class ConsumerServiceImpl implements ConsumerService {
@Resource
private StoreService storeService;
/**
* 订阅处理
*
* @param messageBean 订阅消息
*/
@Override
public void receiveHandle(SyncMessageBean messageBean) {
// 门店信息同步
if (ConsumerTypeEnum.SYNC_STORE_TO_SMART.getName().equals(messageBean.getErrorMsg())) {
StoreInfo storeInfo = JSON.parseObject(messageBean.getData().toString(), StoreInfo.class);
log.info("门店信息同步订阅解析[{}]", storeInfo);
SimpleMessage simpleMessage = null;
try {
simpleMessage = storeService.syncStoreInfo(storeInfo);
} catch (Exception e) {
log.error("门店信息同步异常[{}]", storeInfo, e);
}
log.info("店信息同步结果[{}],[{}]", storeInfo, simpleMessage);
}
}
}
二、EasyExcel 使用
优势:EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 github地址:https://github.com/alibaba/easyexcel
直接上代码
1、基础引入
com.alibaba
easyexcel
1.1.2-beat1
2、导出EXCEL
(1)、下载结构体
/**
* @author: 唐宁
* @date: 2019/5/4
* @time: 21:05
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AskCarSheetDetailDownloadBean implements Serializable {
private static final long serialVersionUID = -7779152957404073436L;
/**
* 车辆VIN码
*/
@ExcelProperty("VIN码")
private String carCode;
/**
* 车辆状态
*/
@ExcelProperty("车辆状态")
private String carStateIntro;
/**
* 仓库名称
*/
@ExcelProperty("仓库名称")
private String areaName;
/**
* 区域名称
*/
@ExcelProperty("区域名称")
private String positionName;
/**
* 第几排
*/
@ExcelProperty("第几排")
private Integer row;
/**
* 第几列
*/
@ExcelProperty("第几列")
private Integer position;
}
(2)、核心代码及使用方法
/**
* 下载单据详情数据
*
* @param response 响应
* @param dto 查询参数
* @throws IOException
*/
@Override
public void downloadAskCarSheetDetail(HttpServletResponse response, AskCarSheetDetailDTO dto) throws IOException {
List askCarSheetDetailList = sheetDao.getAskCarSheetDetailList(dto);
// 处理下载数据
List askCarSheetDetailDownloadBeanList = new ArrayList<>();
askCarSheetDetailList.forEach(askCarSheetDetailVO -> {
// 构建基础
AskCarSheetDetailDownloadBean askCarSheetDetailDownloadBean = AskCarSheetDetailDownloadBean.builder()
.carCode(askCarSheetDetailVO.getCarCode())
.areaName(askCarSheetDetailVO.getAreaName())
.positionName(askCarSheetDetailVO.getPositionName())
.row(askCarSheetDetailVO.getRow())
.position(askCarSheetDetailVO.getPosition())
.build();
// 车辆状态
if (Objects.nonNull(dto.getCarState())) {
askCarSheetDetailDownloadBean.setCarStateIntro(dto.getCarState().getStr());
} else {
askCarSheetDetailDownloadBean.setCarStateIntro("未找到");
}
// 添加数据
askCarSheetDetailDownloadBeanList.add(askCarSheetDetailDownloadBean);
});
// 处理下载
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("单据详情信息", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), AskCarSheetDetailDownloadBean.class).sheet("单据详情信息").doWrite(askCarSheetDetailDownloadBeanList);
}
三、导入Excel表格
(1)、核心处理方法
/**
* 上传单据信息
*
* @param file 文件
* @return SimpleMessage
*/
@Override
@Transactional(rollbackFor = Exception.class)
public SimpleMessage uploadSheetInfo(MultipartFile file) throws IOException {
// 接收解析出的目标对象(Student)
List outStockSheetBeanList = new ArrayList<>();
// excel中表的列要与对象的字段相对应
EasyExcel.read(file.getInputStream(), OutStockSheetBean.class, new AnalysisEventListener() {
// 每解析一条数据都会调用该方法
@Override
public void invoke(OutStockSheetBean outStockSheetBean, AnalysisContext analysisContext) {
log.info("文件信息[{}]", JSON.toJSONString(outStockSheetBean));
outStockSheetBeanList.add(outStockSheetBean);
}
// 解析完毕的回调方法
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("读取完毕:共[{}]行", outStockSheetBeanList.size());
// 执行后续操作
}
}).sheet().doRead();
}
return new SimpleMessage(ErrorCodeEnum.OK, "成功读取" + outStockSheetBeanList.size() + "条数据");
}