页面发布实战
使用技术:
- springboot
- vue
- mongodb
- gridfs
- freemarker
- rabbitmq
目标:
通过我们的cms程序可以实现动态发布界面到站点服务器上,不再需要程序员手动修改界面,拷贝文件。
分析流程
- cms系统根据模板和dataurl静态化界面
- 将静态化的界面存储到mongodb中
- 通过rabbitmq给cms-client发送消息,通知获取内容
- cms-client获收到消息开始从Mongodb中下载文件
- cms-client下载文件完成后通过rabbitmq通知cms程序处理结果
RabbitMQ 队列和交换机定义
交换机: EXCHANGE_CMS_PAGE_POST
本次通讯使用的时rabbitmq中的路由模式,所以消息的发送和反馈都可以使用这一个交换机
消息反馈队列: QUEUE_CMS_PAGE_POST
根据业务场景分析,
- 此处生产者为1个 消费者为多个,所以反馈部分可以使用工作模式,即反馈队列的路由键和队列名称设置为一样
- 因为同一个站点可以能部署多个服务器进行负载均衡,所以这部分可以使用发布订阅模式,每个cms-client监听不同的队列,但是同一个站点服务器下的cms-client监听的队列绑定的路由键均为站点id
cms中的RabbitmqConfig代码
/**
* 生产者其实在生产过程中是不知道具体有哪些消费者的
* 所以在生产者的程序中只用设置交换机就可以了,
* 如果消费者在生产者启动之前启动,生产者中的交换机也不用定义
*/
@Configuration
public class RabbitmqConfig {
public static final String EXCHANGE_CMS_PAGE_POST = "EXCHANGE_CMS_PAGE_POST";
// 反馈队列
public static final String QUEUE_CMS_PAGE_POST = "QUEUE_CMS_PAGE_POST";
// 构建交换机
@Bean
public Exchange EXCHANGE_CMS_PAGE_POST() {
// durable(true) 持久化 消息队列重启后交换机依旧保留
return ExchangeBuilder.directExchange(EXCHANGE_CMS_PAGE_POST).durable(true).build();
}
// 构建队列
@Bean
public Queue QUEUE_CMS_PAGE_POST() {
return QueueBuilder.durable(QUEUE_CMS_PAGE_POST).build();
}
@Bean
public Binding bindSend(@Qualifier("EXCHANGE_CMS_PAGE_POST") Exchange exchange,
@Qualifier("QUEUE_CMS_PAGE_POST") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with(QUEUE_CMS_PAGE_POST).noargs();
}
}
cms-client中的RabbitmqConfig
@Configuration
public class RabbitmqConfig {
public static final String EXCHANGE_CMS_PAGE_POST = "EXCHANGE_CMS_PAGE_POST";
public static final String QUEUE_CMS_PAGE_POST = "QUEUE_CMS_PAGE_POST";
@Value("${xuecheng.mq.receiveQueue}")
private String RECEIVE_QUEUE;
@Value("${xuecheng.mq.routingKey}")
private String ROUTING_KET;
// 构建交换机
@Bean
public Exchange EXCHANGE_CMS_PAGE_POST() {
// durable(true) 持久化 消息队列重启后交换机依旧保留
return ExchangeBuilder.directExchange(EXCHANGE_CMS_PAGE_POST).durable(true).build();
}
// 构建队列
@Bean
public Queue RECEIVE_QUEUE() {
return QueueBuilder.durable(RECEIVE_QUEUE).build();
}
// 绑定交换机和队列
@Bean
public Binding bindReceive(@Qualifier("EXCHANGE_CMS_PAGE_POST") Exchange exchange,
@Qualifier("RECEIVE_QUEUE") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KET).noargs();
}
// 构建队列
@Bean
public Queue QUEUE_CMS_PAGE_POST() {
return QueueBuilder.durable(QUEUE_CMS_PAGE_POST).build();
}
@Bean
public Binding bindSend(@Qualifier("EXCHANGE_CMS_PAGE_POST") Exchange exchange,
@Qualifier("QUEUE_CMS_PAGE_POST") Queue queue) {
return BindingBuilder.bind(queue).to(exchange).with(QUEUE_CMS_PAGE_POST).noargs();
}
}
cms-client中的消息监听方法实现
对于cms-client而言它只干了2件事情
- 接受消息 下载文件
- 发送反馈消息
@Component
public class CmsPostPage {
@Autowired
private CmsPageService cmsPageService;
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = "${xuecheng.mq.receiveQueue}")
public void postPage(String msg){
CmsPagePostLog log = new CmsPagePostLog();
Map map = JSON.parseObject(msg, Map.class);
String pageId = (String) map.get("pageId");
log.setHost(getLocalHost());
log.setCreateTime(new Date());
log.setPageId(pageId);
try {
cmsPageService.savePageToServerPath(pageId);
log.setSuccess("1");
log.setMsg("发布成功!");
}catch (Exception e){
e.printStackTrace();
log.setSuccess("0");
if(e instanceof CustomException){
log.setMsg(((CustomException) e).getResultCode().toString());
}else{
ResultCode resultCode = ExceptionCatch.EXCEPTIONS.get(e);
if(resultCode != null){
log.setMsg(resultCode.message());
}else{
log.setMsg(e.getMessage());
}
}
}
// 将反馈消息写如反馈队列
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_CMS_PAGE_POST,
RabbitmqConfig.QUEUE_CMS_PAGE_POST, JSONObject.toJSONString(log));
}
// 获取当前cms客户端服务所在的ip
private String getLocalHost(){
String localHost = "";
try {
localHost = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return localHost;
}
}
public void savePageToServerPath(String pageId) {
// 根据页面Id查找页面信息
CmsPage cmsPage = findCmsPageById(pageId);
//根据页面信息获取站点信息
CmsSite cmsSite = findCmsSiteById(cmsPage.getSiteId());
//根据页面信息和站点信息拼接出文件应该下载到服务器的哪个位置
String filePath = cmsSite.getSitePhysicalPath() + cmsPage.getPagePhysicalPath() + cmsPage.getPageName();
//根据页面信息内的静态化界面Id 从gridFS中下载内容
InputStream is = downHtmlContent(cmsPage.getHtmlFileId());
//检查文件是否存在,如果不存在级联创建文件和上级文件夹
FileOutputStream fos = null;
try {
checkFileExits(filePath,true);
fos = new FileOutputStream(new File(filePath));
IOUtils.copy(is,fos);
fos.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
cms系统中页面发布的实现
public ResponseResult postPage(String pageId) {
// 1.获取预览html
String htmlContent = this.previewPage(pageId);
// 2.查找cmsPage信息
CmsPage cmsPage = cmsPageService.getById(pageId).getCmsPage();
String newHtmlFileId = "";
try (InputStream is = IOUtils.toInputStream(htmlContent, "utf-8")) {
// 3..将预览的html存储到gridFS中
ObjectId store = gridFsTemplate.store(is, cmsPage.getPageName());
newHtmlFileId = store.toString();
} catch (IOException e) {
ExceptionCast.cast(CmsCode.CMS_TEMPLATE_FILE_UPLOAD_FAILD);
}
String htmlFileId = cmsPage.getHtmlFileId();
if (StringUtils.isNotEmpty(htmlFileId)) {
// 删除旧的文件信息
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(htmlFileId)));
}
// 4.更新page中的htmlFileId字段
cmsPage.setHtmlFileId(newHtmlFileId);
cmsPageRepository.save(cmsPage);
// 5. 清空上一次的发布日志
cmsPagePostLogRepository.deleteByPageId(pageId);
// 6.发送消息提醒客户端程序响应
Map map = new HashMap();
map.put("pageId", pageId);
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_CMS_PAGE_POST, cmsPage.getSiteId(),
JSON.toJSONString(map));
return new ResponseResult(CommonCode.SUCCESS);
}
cms系统中对于反馈消息的处理
@Component
public class CmsPostPage {
@Autowired
private CmsPagePostLogRepository cmsPagePostLogRepository;
// 接收到日志信息
@RabbitListener(queues = RabbitmqConfig.QUEUE_CMS_PAGE_POST)
public void receiveLog(String msg) {
CmsPagePostLog log = JSON.parseObject(msg, CmsPagePostLog.class);
cmsPagePostLogRepository.save(log);
}
}
总结
到此为止,cms系统部分告一段落,经过页面发布功能的实现,重新梳理了前面几节学到的知识点。
知识点之外的东西也很重要:
- 代码规范问题,良好的编码风格可以让代码靓起来
- 前后端分离优点太多,但是缺点也是有的,比如模块太多,单人开发比较繁琐
- 使用mq进行异步通讯,可以对服务进行解耦
- 发现了很多以前没用过的神器,比如:
- org.apache.commons.io.IOUtils 一个操作Io流很方便的工具类
- swagger
- es6中的箭头函数
所以还是要多看东西,见多才能识广
完整代码