第五课 Spring Cloud分布式微服务实战-文章服务开发RabbitMQ和静态化
tags:
- Java
- 慕课网
文章目录
第一节 文章服务的开发
1.1 文章服务的构建
- 创建新module
imooc-news-dev-service-article。 - pom文件加上api的依赖。
- resource文件夹下复制一份配置文件并修改。端口8001
- 创建启动类
com.imooc.article.Application - 创建mapper文件夹和controller文件夹,并创建HelloController。
- 启动访问测试。http://127.0.0.1:8001/hello
- 看下aritcle的数据库表

1.2 minIO多图片上传
- minIO http://192.168.44.128:6090/login 账号: admin 密码:123456789
- 测试地址:http://admin.imoocnews.com:9090/imooc-news/writer/createArticle.html
- api添加在controller的files中
@ApiOperation(value = "上传多个文件", notes = "上传多个文件", httpMethod = "POST")
@PostMapping("/uploadSomeFiles")
public GraceJSONResult uploadSomeFiles(@RequestParam String userId,
MultipartFile[] files) throws Exception;
- 实现这个接口。
FileUploaderController#uploadSomeFiles
@Override
public GraceJSONResult uploadSomeFiles(String userId, MultipartFile[] files) throws Exception {
// 声明list,用于存放多个图片的地址路径,返回到前端
List<String> imageUrlList = new ArrayList<>();
if (files != null && files.length > 0){
for (MultipartFile file : files){
String finalpath = "";
if (file != null) {
// 获取文件上传的名称
String filename = file.getOriginalFilename();
// 判断文件名不能为空
if (StringUtils.isNotBlank(filename)){
String fileNameArr[] = filename.split("\\.");
// 获得后缀
String suffix = fileNameArr[fileNameArr.length - 1];
// 判断后缀是否符合预定义规范 防止黑客乱传脚本攻击
if (!suffix.equalsIgnoreCase("jpg") &&
!suffix.equalsIgnoreCase("png") &&
!suffix.equalsIgnoreCase("jpeg")){
continue;
}
// 执行上传
finalpath = uploaderService.uploadMinIO(file);
// FIXME: 添加之前可以做一个图片审核
imageUrlList.add(finalpath);
}
} else {
continue;
}
}
}
return GraceJSONResult.ok(imageUrlList);
}
- api拦截器配置。
api.config.InterceptorConfig#addInterceptors
registry.addInterceptor(userTokenInterceptor())
.addPathPatterns("/user/getAccountInfo")
.addPathPatterns("/user/updateUserInfo")
.addPathPatterns("/user/uploadFace")
.addPathPatterns("/user/uploadSomeFiles");
registry.addInterceptor(userActiveInterceptor())
.addPathPatterns("/fs/uploadSomeFiles")
- 测试图片上传功能。
1.3 用户端查询分类列表
- Api的
controller.admin.CategoryMngControllerApi添加
@GetMapping("getCats")
@ApiOperation(value = "用户端查询分类列表", notes = "用户端查询分类列表", httpMethod = "GET")
public GraceJSONResult getCats();
- admin模块实现接口。
admin.controller.CategoryMngController#getCats
@Override
public GraceJSONResult getCats() {
// 先从redis中查询,如果有,则返回,如果没有,则查询数据库库后先放缓存,放返回
String allCatJson = redis.get(REDIS_ALL_CATEGORY);
List<Category> categoryList = null;
if (StringUtils.isBlank(allCatJson)) {
categoryList = categoryService.queryCategoryList();
redis.set(REDIS_ALL_CATEGORY, JsonUtils.objectToJson(categoryList));
} else {
categoryList = JsonUtils.jsonToList(allCatJson, Category.class);
}
return GraceJSONResult.ok(categoryList);
}
com.imooc.api.service.BaseService添加常量。
public static final String REDIS_ALL_CATEGORY = "redis_all_category";
- 先登录首页进行测试:http://admin.imoocnews.com:9090/imooc-news/portal/index.html
- 安装测试文章分类。http://admin.imoocnews.com:9090/imooc-news/writer/createArticle.html
- 到redis中查看缓存。
redis_all_category
1.4 构建定时任务
- 定时表达式生成网站:https://cron.qqe2.com/
- 创建
com.imooc.article.task.TaskPublishArticles
package com.imooc.article.task;
import com.imooc.article.service.ArticleService;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.time.LocalDateTime;
@Configuration // 1. 标记配置类,使得springboot容器扫描到
@EnableScheduling // 2. 开启定时任务
public class TaskPublishArticles {
@Autowired
private ArticleService articleService;
// 添加定时任务,注明定时任务的表达式
@Scheduled(cron = "0/3 * * * * ?")
private void publishArticles() {
System.out.println("执行定时任务:" + LocalDateTime.now());
// 4. 调用文章service,把当前时间应该发布的定时文章,状态改为即时
articleService.updateAppointToPublish();
}
}
1.5 阿里AI智能审核
- 阿里云首页:->内容安全->内容检测api。这里根据阿里官方的api对接既可。
- 剩下看下源码即可。
第二节 文章评论业务的开发
2.1 自定义Mapper完成评论关联查询
- 数据库关联查询sql语句编写。自己关联自己。
SELECT
c.id as commentId,
c.father_id as fatherId,
c.article_id as articleId,
c.comment_user_id as commentUserId,
c.comment_user_nickname as commentUserNickname,
c.content as content,
c.create_time as createTime,
f.comment_user_nickname as quoteUserNickname,
f.content as quoteContent
FROM
comments c
LEFT JOIN
comments f
on
c.father_id = f.id
WHERE
c.article_id = '2006117B57WRZGHH'
order by
c.create_time
desc
- 自定义Mapper编写。
com.imooc.article.mapper.CommentsMapperCustom自定义映射。
package com.imooc.article.mapper;
import com.imooc.pojo.vo.CommentsVO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
public interface CommentsMapperCustom {
/**
* 查询文章评论
*/
public List<CommentsVO> queryArticleCommentList(@Param("paramMap") Map<String, Object> map);
}
- 上面第一步查询出来的对象作为
CommentsVO写到model中。 resources/mapper/CommentsMapperCustom.xml
<?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.imooc.article.mapper.CommentsMapperCustom" >
<select id="queryArticleCommentList"
resultType="com.imooc.pojo.vo.CommentsVO"
parameterType="Map">
SELECT
c.id as commentId,
c.father_id as fatherId,
c.comment_user_id as commentUserId,
c.comment_user_nickname as commentUserNickname,
c.article_id as articleId,
c.content as content,
c.create_time as createTime,
f.comment_user_nickname as quoteUserNickname,
f.content as quoteContent
FROM
comments c
LEFT JOIN
comments f
ON
c.father_id = f.id
WHERE
c.article_id = #{paramMap.articleId}
ORDER BY
c.create_time
DESC
</select>
</mapper>
2.2 Freemarker基本配置
- 静态化技术:JSP、Freemarker和Thymeleaf(官方推荐的)、Velocity
- 官方文档: http://freemarker.foofun.cn/index.html
- 引入依赖:https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-freemarker
- 找到对应的版本号。复制到顶级pom的依赖中。
<springboot-freemarker.version>2.2.5.RELEASE</springboot-freemarker.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${springboot-freemarker.version}</version>
</dependency>
- 在使用的子项目article中使用依赖,引入freemarker。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
- application.yml配置。
freemarker:
charset: UTF-8
content-type: text/html
suffix: .ftl
template-loader-path: classpath:/templates/
- 编写控制器。这里使用
@Controller而不是@RestController。因为@RestController会把返回值变成字符串不是把对应的stu.ftl模板展示出来。
@Controller
@RequestMapping("free")
public class FreemarkerController {
@GetMapping("/hello")
public String hello(Model model) {
// 定义输出到模板的内容
// 输入字符串
String stranger = "慕课网 imooc.com";
model.addAttribute("there", stranger);
makeModel(model);
// 返回的stu是freemarker模板坐在的目录位置 classpath:/templates/
// 匹配 *.ftl
return "stu";
}
}
- 访问:
localhost:8001/free/hello
2.3 Freemarker生成静态html
- 配置生成路径。application.yml
# 定义freemarker生成的html位置
freemarker:
html:
target: /workspace/freemarker_html
article: /Users/leechenxiang/Desktop/apache-tomcat-9.0.22/webapps/imooc-news/portal/a
- 定义静态生产html的函数。
@Controller
@RequestMapping("free")
public class FreemarkerController {
@Value("${freemarker.html.target}")
private String htmlTarget;
@GetMapping("/createHTML")
@ResponseBody
public String createHTML(Model model) throws Exception {
// 0. 配置freemarker基本环境
Configuration cfg = new Configuration(Configuration.getVersion());
// 声明freemarker模板所需要加载的目录的位置
String classpath = this.getClass().getResource("/").getPath();
cfg.setDirectoryForTemplateLoading(new File(classpath + "templates"));
// System.out.println(htmlTarget);
// System.out.println(classpath + "templates");
// 1. 获得现有的模板ftl文件
Template template = cfg.getTemplate("stu.ftl", "utf-8");
// 2. 获得动态数据
String stranger = "慕课网 imooc.com";
model.addAttribute("there", stranger);
model = makeModel(model);
// 3. 融合动态数据和ftl,生成html
File tempDic = new File(htmlTarget);
if (!tempDic.exists()) {
tempDic.mkdirs();
}
Writer out = new FileWriter(htmlTarget + File.separator + "10010" + ".html");
template.process(model, out);
out.close();
return "ok";
}
}
- 把文章详情页的内容抽象出模板,生成静态文件html上传到gridfs中,等待通知前端下载到指定目录。具体实现看下源码。
2.3 RabbitMQ环境搭建
- 安装RabbitMQ
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq
docker ps
# http://192.168.44.128:15672 ,这里的用户名和密码默认都是guest
docker exec -it 镜像ID /bin/bash
rabbitmq-plugins enable rabbitmq_management
# 创建admin用户分配权限
- 引入rabbitMQ。api中pom引入。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
rabbitmq:
host: 192.168.44.128
port: 5672
username: admin
password: 123456
virtual-host: imooc-news-dev
- 在http://192.168.44.128:15672 的admin中创建virtual-host为imooc-news-dev。
2.4 RabbitMQ基本使用
- api的config中
com.imooc.api.config.RabbitMQDelayConfig编写Rabbit的配置类。
package com.imooc.api.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 的配置类
*/
@Configuration
public class RabbitMQConfig {
// 定义交换机的名字
public static final String EXCHANGE_ARTICLE = "exchange_article";
// 定义队列的名字
public static final String QUEUE_DOWNLOAD_HTML = "queue_download_html";
// 创建交换机
@Bean(EXCHANGE_ARTICLE)
public Exchange exchange(){
return ExchangeBuilder
.topicExchange(EXCHANGE_ARTICLE)
.durable(true)
.build();
}
// 创建队列
@Bean(QUEUE_DOWNLOAD_HTML)
public Queue queue(){
return new Queue(QUEUE_DOWNLOAD_HTML);
}
// 队列绑定交换机
@Bean
public Binding binding(
@Qualifier(QUEUE_DOWNLOAD_HTML) Queue queue,
@Qualifier(EXCHANGE_ARTICLE) Exchange exchange){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("article.#.do")
.noargs(); // 执行绑定
}
}
- rabbitMq官网各种模式:https://www.rabbitmq.com/getstarted.html
- 服务端发送消息到rabbitmq测试。
package com.imooc.article.controller;
import com.imooc.api.config.RabbitMQConfig;
import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.grace.result.GraceJSONResult;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RestController
@RequestMapping("producer")
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/hello")
public Object hello() {
/**
* RabbitMQ 的路由规则 routing key
* display.*.* -> * 代表一个占位符
* 例:
* display.do.download 匹配
* display.do.upload.done 不匹配
*
* display.# -> # 代表任意多个占位符
* 例:
* display.do.download 匹配
* display.do.upload.done.over 匹配
*
*
*/
// rabbitTemplate.convertAndSend(
// RabbitMQConfig.EXCHANGE_ARTICLE,
// "article.hello",
// "这是从生产者发送的消息~");
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_ARTICLE,
"article.publish.download.do",
"1001");
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_ARTICLE,
"article.success.do",
"1002");
// 如果先绑定过display.* 要到rabbitmq中解除绑定 否则还是会传到之前的routing key中
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_ARTICLE,
"article.play",
"1003");
return GraceJSONResult.ok();
}
}
- 客户端消费队列中的消息。article-html中。根据routing key的不同值处理不同的业务逻辑
package com.imooc.article.html;
import com.imooc.api.config.RabbitMQConfig;
import com.imooc.article.html.controller.ArticleHTMLComponent;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQConsumer {
@Autowired
private ArticleHTMLComponent articleHTMLComponent;
@RabbitListener(queues = {RabbitMQConfig.QUEUE_DOWNLOAD_HTML})
public void watchQueue(String payload, Message message) {
System.out.println(payload);
String routingKey = message.getMessageProperties().getReceivedRoutingKey();
if (routingKey.equalsIgnoreCase("article.publish.download.do")) {
System.out.println("article.publish.download.do");
} else if (routingKey.equalsIgnoreCase("article.success.do")) {
System.out.println("article.success.do");
} else if (routingKey.equalsIgnoreCase("article.download.do")) {
// articleId + "," + articleMongoId
String articleId = payload.split(",")[0];
String articleMongoId = payload.split(",")[1];
try {
articleHTMLComponent.download(articleId, articleMongoId);
} catch (Exception e) {
e.printStackTrace();
}
} else if (routingKey.equalsIgnoreCase("article.html.download.do")) {
String articleId = payload;
try {
articleHTMLComponent.delete(articleId);
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("不符合的规则:" + routingKey);
}
}
}
2.5 RabbitMQ延时队列插件安装使用
- 安装延时队列插件:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/v3.8.0
- 上传到服务器,复制到容器中。
docker cp /home/rabbitmq_delayed_message_exchange-3.8.0.ez 773067241f96:/plugins
#启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
#查看
rabbitmq-plugins list
#重新启动容器
docker restart 773067241f96
- 构建延时队列rabbitmq的配置类。
package com.imooc.api.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 的配置类
*/
@Configuration
public class RabbitMQDelayConfig {
// 定义交换机的名字
public static final String EXCHANGE_DELAY = "exchange_delay";
// 定义队列的名字
public static final String QUEUE_DELAY = "queue_delay";
// 创建交换机
@Bean(EXCHANGE_DELAY)
public Exchange exchange(){
return ExchangeBuilder
.topicExchange(EXCHANGE_DELAY)
.delayed() // 开启支持延迟消息
.durable(true)
.build();
}
// 创建队列
@Bean(QUEUE_DELAY)
public Queue queue(){
return new Queue(QUEUE_DELAY);
}
// 队列绑定交换机
@Bean
public Binding delayBinding(
@Qualifier(QUEUE_DELAY) Queue queue,
@Qualifier(EXCHANGE_DELAY) Exchange exchange){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("publish.delay.#")
.noargs(); // 执行绑定
}
}
- 延时队列生产者示例。
@GetMapping("/delay")
public Object delay() {
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的持久
message.getMessageProperties()
.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 设置消息延迟的时间,单位ms毫秒
message.getMessageProperties()
.setDelay(5000);
return message;
}
};
rabbitTemplate.convertAndSend(
RabbitMQDelayConfig.EXCHANGE_DELAY,
"delay.demo",
"这是一条延迟消息~~",
messagePostProcessor);
System.out.println("生产者发送的延迟消息:" + new Date());
return "OK";
}
- 延迟队列消费者示例
package com.imooc.article;
import com.imooc.api.config.RabbitMQDelayConfig;
import com.imooc.article.service.ArticleService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class RabbitMQDelayConsumer {
@Autowired
private ArticleService articleService;
@RabbitListener(queues = {RabbitMQDelayConfig.QUEUE_DELAY})
public void watchQueue(String payload, Message message) {
System.out.println(payload);
String routingKey = message.getMessageProperties().getReceivedRoutingKey();
System.out.println(routingKey);
System.out.println("消费者接受的延迟消息:" + new Date());
// 消费者接收到定时发布的延迟消息,修改当前的文章状态为`即时发布`
String articleId = payload;
articleService.updateArticleToPublish(articleId);
}
}
2万+

被折叠的 条评论
为什么被折叠?



